Entity Architecture: Content & Discovery Separation¶
Architecture for independently discoverable entities with loosely coupled discovery mechanisms.
Vocabulary¶
| Entity | Role | DB Table |
|---|---|---|
| Journey | Curated learning path (10-20 hours) | catalog_journeys |
| Chapter | Knowledge cluster within a journey (30-60 mins) | catalog_chapters |
| Insight | Atomic micro-lesson (5-10 mins), independently readable | catalog_insights |
| Topic | User-facing curated discovery lens ("AI Fundamentals") | topics (new) |
| Tag | System-facing classification keyword for programmatic discovery | tags (new) |
Core Design Principle: Content vs Discovery¶
The architecture separates two bounded contexts that must evolve independently:
┌─────────────────────────────────────────────────────────────────────┐
│ DISCOVERY CONTEXT │
│ (how users find content — extensible, loosely coupled) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Topics │ │ Tags │ │ Future Lenses │ │
│ │ (curated │ │ (system │ │ (collections, │ │
│ │ for │ │ classif │ │ playlists, │ │
│ │ users) │ │ ication)│ │ tracks, etc.) │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
└────────┼───────────────────┼───────────────────────┼───────────────┘
│ │ │
┌────▼───────────────────▼───────────────────────▼────┐
│ ASSOCIATION LAYER │
│ (junction tables — the only coupling) │
│ │
│ topic_journeys topic_insights │
│ tag_journeys tag_insights │
│ (future)_journeys (future)_insights │
└────┬───────────────────────────────────┬────────────┘
│ │
┌────────▼───────────────────────────────────▼────────────┐
│ CONTENT CONTEXT │
│ (what users consume — stable, self-contained) │
│ │
│ ┌──────────────┐ │
│ │ Journeys │──── journey_chapters ────┐ │
│ │ (learning │ │ │
│ │ paths) │ ┌──────▼──────┐ │
│ └──────────────┘ │ Chapters │ │
│ │ (sections │ │
│ │ within a │ │
│ ┌──────────────┐ │ journey) │ │
│ │ Insights │◄── chapter_insights──┘ │ │
│ │ (standalone │ │
│ │ lessons) │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
Why this separation matters:
-
Content tables have ZERO foreign keys to discovery tables. A journey doesn't "know" about topics. A topic doesn't own journeys. The junction table is the only coupling point.
-
New discovery mechanisms cost zero content changes. Want to add "Collections" or "Learning Tracks" or "Difficulty Paths"? Create a new discovery entity table, create junction tables to journeys/insights, deploy a new edge function. Content tables never change.
-
Discovery and content can scale independently. Topic queries hit topic tables + junctions. Journey queries hit journey tables + chapter/insight junctions. No cross-context joins needed for primary operations.
Entity Behavior¶
Journeys (independently discoverable)¶
A journey is a curated, structured learning product. Users browse and enroll in journeys.
- Discoverable via: Topics, Tags, Search, Explore, Direct URL
- Contains: Ordered chapters, each containing ordered insights
- User actions: Enroll, track progress, rate
- API access:
GET /journeys,GET /journeys/{slug}
Chapters (NOT independently discoverable)¶
A chapter is a structural section within a journey. It only makes sense in the context of a journey.
- Discoverable via: Only through its parent journey
- Contains: Ordered insights
- No direct API: Only via
GET /journeys/{slug}/chapters - No topic/tag associations: Chapters aren't tagged because users don't search for them
- Reusable across journeys: The same chapter can appear in multiple journeys via the
catalog_journey_chaptersjunction table (e.g., "Attention Mechanisms" chapter in both "NLP Mastery" and "Transformer Deep Dive")
Insights (independently discoverable)¶
An insight is an atomic micro-lesson. Users can read insights standalone (outside any journey) or within a journey's chapter structure.
- Discoverable via: Topics, Tags, Search, Explore, Direct URL
- Standalone reading: Users can find and read an insight directly
- Journey context: The same insight can appear in multiple chapters across multiple journeys
- API access:
GET /insights,GET /insights/{slug},GET /content/insights/{slug}
Topics (user-facing discovery)¶
A topic is a curated, editorially managed category. It's the primary way users browse content by subject area. Think of it as a "featured collection" with rich metadata.
- User behavior: "I want to learn about AI Fundamentals" → browse the topic → see curated journeys and standalone insights
- Many-to-many with journeys: "AI Fundamentals" might feature journeys from practitioner, executive, and designer perspectives
- Many-to-many with insights: "AI Fundamentals" might also surface standalone insights that aren't part of any journey
- Curated, not automatic: Topics are editorially managed, not auto-generated from content
- API access:
GET /topics,GET /topics/{slug},GET /topics/{slug}/journeys,GET /topics/{slug}/insights
Tags (system-facing discovery)¶
A tag is a classification keyword used for programmatic discovery, filtering, and faceted search. Users don't browse tags directly — the system uses them to power search facets, recommendations, and cross-cutting filters.
- System behavior: Filter search results by "python" tag, power "related content" recommendations, populate facet dropdowns
- Many-to-many with journeys and insights: A journey can have multiple tags; a tag can apply to many journeys
- Automatic or semi-automatic: Tags can be assigned programmatically (e.g., by content analysis) or manually
- API access:
GET /tags,GET /tags/{slug},GET /tags/{slug}/journeys,GET /tags/{slug}/insights
Database Schema¶
Existing Tables (Content Context — no changes needed)¶
These tables already exist and form the content backbone:
| Table | Purpose | Status |
|---|---|---|
catalog_journeys |
Journey content entities | Exists |
catalog_chapters |
Chapter entities (independent, no journey FK) | Exists |
catalog_insights |
Insight/lesson entities | Exists |
catalog_journey_chapters |
Journey ↔ Chapter junction (with position) |
Exists |
catalog_chapter_insights |
Chapter ↔ Insight junction (with position) |
Exists |
catalog_journey_enrollments |
User enrollment tracking | Exists |
catalog_journey_ratings |
User ratings | Exists |
New Tables (Discovery Context)¶
topics — Curated discovery categories¶
CREATE TABLE topics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
icon VARCHAR(50),
color VARCHAR(7),
hero_image_url TEXT,
display_order INTEGER NOT NULL DEFAULT 0,
learning_outcomes JSONB DEFAULT '[]'::jsonb,
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT topics_slug_format CHECK (slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
CONSTRAINT topics_color_format CHECK (color IS NULL OR color ~ '^#[0-9A-Fa-f]{6}$')
);
CREATE INDEX idx_topics_slug ON topics(slug);
CREATE INDEX idx_topics_status ON topics(status) WHERE status = 'published';
CREATE INDEX idx_topics_display_order ON topics(display_order) WHERE status = 'published';
COMMENT ON TABLE topics IS 'Curated, user-facing discovery categories for browsing content by subject area';
tags — System classification keywords¶
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
description VARCHAR(200),
category VARCHAR(50) NOT NULL DEFAULT 'general'
CHECK (category IN ('technology', 'concept', 'application', 'level', 'format', 'general')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT tags_slug_format CHECK (slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$')
);
CREATE INDEX idx_tags_slug ON tags(slug);
CREATE INDEX idx_tags_category ON tags(category);
CREATE INDEX idx_tags_name_trgm ON tags USING GIN(name gin_trgm_ops);
COMMENT ON TABLE tags IS 'System-facing classification keywords for programmatic discovery and faceted search';
New Tables (Association Layer)¶
topic_journeys — Topics ↔ Journeys¶
CREATE TABLE topic_journeys (
topic_id UUID NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
journey_id UUID NOT NULL REFERENCES catalog_journeys(id) ON DELETE CASCADE,
display_order INTEGER NOT NULL DEFAULT 0,
is_featured BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (topic_id, journey_id)
);
-- Both directions need to be fast:
-- "Get journeys for topic X" and "Get topics for journey Y"
CREATE INDEX idx_topic_journeys_journey ON topic_journeys(journey_id);
CREATE INDEX idx_topic_journeys_order ON topic_journeys(topic_id, display_order);
COMMENT ON TABLE topic_journeys IS 'Many-to-many: which journeys appear under which topics';
topic_insights — Topics ↔ Insights¶
CREATE TABLE topic_insights (
topic_id UUID NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
insight_id UUID NOT NULL REFERENCES catalog_insights(id) ON DELETE CASCADE,
display_order INTEGER NOT NULL DEFAULT 0,
is_featured BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (topic_id, insight_id)
);
CREATE INDEX idx_topic_insights_insight ON topic_insights(insight_id);
CREATE INDEX idx_topic_insights_order ON topic_insights(topic_id, display_order);
COMMENT ON TABLE topic_insights IS 'Many-to-many: which standalone insights appear under which topics';
tag_journeys — Tags ↔ Journeys¶
CREATE TABLE tag_journeys (
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
journey_id UUID NOT NULL REFERENCES catalog_journeys(id) ON DELETE CASCADE,
PRIMARY KEY (tag_id, journey_id)
);
CREATE INDEX idx_tag_journeys_journey ON tag_journeys(journey_id);
COMMENT ON TABLE tag_journeys IS 'Many-to-many: which tags classify which journeys';
tag_insights — Tags ↔ Insights¶
CREATE TABLE tag_insights (
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
insight_id UUID NOT NULL REFERENCES catalog_insights(id) ON DELETE CASCADE,
PRIMARY KEY (tag_id, insight_id)
);
CREATE INDEX idx_tag_insights_insight ON tag_insights(insight_id);
COMMENT ON TABLE tag_insights IS 'Many-to-many: which tags classify which insights';
Row Level Security (all new tables)¶
-- Topics: publicly readable if published
ALTER TABLE topics ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view published topics"
ON topics FOR SELECT USING (status = 'published');
CREATE POLICY "Service role has full access to topics"
ON topics FOR ALL TO service_role USING (true) WITH CHECK (true);
-- Tags: publicly readable
ALTER TABLE tags ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view tags"
ON tags FOR SELECT USING (true);
CREATE POLICY "Service role has full access to tags"
ON tags FOR ALL TO service_role USING (true) WITH CHECK (true);
-- All junction tables: publicly readable, service role writable
ALTER TABLE topic_journeys ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view topic_journeys" ON topic_journeys FOR SELECT USING (true);
CREATE POLICY "Service role manages topic_journeys" ON topic_journeys FOR ALL TO service_role USING (true) WITH CHECK (true);
ALTER TABLE topic_insights ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view topic_insights" ON topic_insights FOR SELECT USING (true);
CREATE POLICY "Service role manages topic_insights" ON topic_insights FOR ALL TO service_role USING (true) WITH CHECK (true);
ALTER TABLE tag_journeys ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view tag_journeys" ON tag_journeys FOR SELECT USING (true);
CREATE POLICY "Service role manages tag_journeys" ON tag_journeys FOR ALL TO service_role USING (true) WITH CHECK (true);
ALTER TABLE tag_insights ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view tag_insights" ON tag_insights FOR SELECT USING (true);
CREATE POLICY "Service role manages tag_insights" ON tag_insights FOR ALL TO service_role USING (true) WITH CHECK (true);
Entity-Relationship Diagram¶
DISCOVERY CONTEXT CONTENT CONTEXT
┌──────────┐ ┌──────────────────┐
│ topics │ │ catalog_journeys │
│──────────│ topic_journeys │──────────────────│
│ id │─────────────────────────▶│ id │
│ slug │ (many-to-many) │ slug │
│ name │ │ title │
│ icon │ topic_insights │ difficulty │
│ color │─────────┐ │ status │
│ status │ │ └────────┬─────────┘
└──────────┘ │ │
│ catalog_journey_ │
│ chapters │
┌──────────┐ │ ┌───────▼────────┐
│ tags │ │ │catalog_chapters│
│──────────│ │ │────────────────│
│ id │ tag_journeys │ id │
│ slug │──────────────────┐ │ slug │
│ name │ │ │ title │
│ category │ tag_insights│ └───────┬────────┘
└──────────┘──────┐ │ catalog_│
│ │ ▲ chapter_ │
│ │ │ insights │
▼ ▼ │ ▼
┌──────────────────┐
│ catalog_insights │
│──────────────────│
│ id │
│ slug │
│ title │
│ difficulty │
│ status │
└──────────────────┘
Key observation: Zero arrows point FROM content tables TO discovery tables. All arrows from discovery TO content go through junction tables. Content is self-contained.
Edge Functions & API Surface¶
Function 1: topics — Curated Discovery¶
functions/topics/index.ts
GET /topics/ List all published topics
GET /topics/{slug} Get topic details (name, icon, color, outcomes)
GET /topics/{slug}/journeys Journeys curated under this topic
GET /topics/{slug}/insights Standalone insights curated under this topic
| Route | Auth | Cache | Description |
|---|---|---|---|
GET /topics |
Public | Aggressive (10 min) | Rarely changes, high traffic |
GET /topics/{slug} |
Public | Aggressive (10 min) | Topic metadata |
GET /topics/{slug}/journeys |
Public | Moderate (2 min) | Paginated, ordered by display_order |
GET /topics/{slug}/insights |
Public | Moderate (2 min) | Paginated, ordered by display_order |
Example queries:
// GET /topics — List published topics
supabase.from("topics")
.select("id, slug, name, description, icon, color, hero_image_url, learning_outcomes")
.eq("status", "published")
.order("display_order")
// GET /topics/{slug}/journeys — Journeys for a topic
supabase.from("topic_journeys")
.select(`
display_order, is_featured,
journey:catalog_journeys!inner(
id, slug, title, subtitle, difficulty,
total_duration_minutes, status
)
`)
.eq("topic_id", topicId)
.eq("catalog_journeys.status", "published")
.order("display_order")
// GET /topics/{slug}/insights — Insights for a topic
supabase.from("topic_insights")
.select(`
display_order, is_featured,
insight:catalog_insights!inner(
id, slug, title, description, difficulty,
duration_minutes, status
)
`)
.eq("topic_id", topicId)
.order("display_order")
Function 2: tags — System Classification¶
functions/tags/index.ts
GET /tags/ List all tags (with optional category filter)
GET /tags/{slug} Get tag details + usage counts
GET /tags/{slug}/journeys Journeys classified with this tag
GET /tags/{slug}/insights Insights classified with this tag
| Route | Auth | Cache | Description |
|---|---|---|---|
GET /tags |
Public | Aggressive (10 min) | Tag catalog, filterable by category |
GET /tags/{slug} |
Public | Moderate (2 min) | Tag details with journey/insight counts |
GET /tags/{slug}/journeys |
Public | Moderate (2 min) | Paginated journeys |
GET /tags/{slug}/insights |
Public | Moderate (2 min) | Paginated insights |
Example queries:
// GET /tags — List tags with usage counts
supabase.from("tags")
.select("id, slug, name, description, category")
.order("name")
// GET /tags/{slug}/journeys — Journeys with this tag
supabase.from("tag_journeys")
.select(`
journey:catalog_journeys!inner(
id, slug, title, subtitle, difficulty,
total_duration_minutes, status
)
`)
.eq("tag_id", tagId)
.eq("catalog_journeys.status", "published")
Function 3: journey-catalog — Learning Paths (extend existing)¶
functions/journey-catalog/index.ts (ALREADY EXISTS — extend)
GET /journeys/ List all published journeys (NEW)
GET /journeys/{id} Journey landing page (EXISTS)
GET /journeys/{id}/chapters Chapters with insights (EXISTS)
GET /journeys/{id}/topics Topics this journey belongs to (NEW)
GET /journeys/{id}/tags Tags on this journey (NEW)
| Route | Auth | Cache | Description |
|---|---|---|---|
GET /journeys |
Public | Moderate (2 min) | Paginated list, filterable by difficulty |
GET /journeys/{id} |
Optional | Moderate (2 min) | Landing page with stats (exists) |
GET /journeys/{id}/chapters |
Optional | Moderate (2 min) | Ordered chapters → insights (exists) |
GET /journeys/{id}/topics |
Public | Moderate (2 min) | Reverse lookup: which topics feature this |
GET /journeys/{id}/tags |
Public | Moderate (2 min) | Reverse lookup: which tags classify this |
New reverse-lookup queries:
// GET /journeys/{id}/topics — Topics for a journey
supabase.from("topic_journeys")
.select(`
topic:topics!inner(id, slug, name, icon, color, status)
`)
.eq("journey_id", journeyId)
.eq("topics.status", "published")
// GET /journeys/{id}/tags — Tags for a journey
supabase.from("tag_journeys")
.select(`
tag:tags(id, slug, name, category)
`)
.eq("journey_id", journeyId)
Function 4: insights — Standalone Lesson Metadata (new)¶
functions/insights/index.ts (NEW)
GET /insights/ List all published insights
GET /insights/{slug} Insight metadata (title, difficulty, duration)
GET /insights/{slug}/topics Topics this insight belongs to
GET /insights/{slug}/tags Tags on this insight
| Route | Auth | Cache | Description |
|---|---|---|---|
GET /insights |
Public | Moderate (2 min) | Paginated, filterable by difficulty |
GET /insights/{slug} |
Public | Moderate (2 min) | Metadata only (not content body) |
GET /insights/{slug}/topics |
Public | Moderate (2 min) | Reverse lookup |
GET /insights/{slug}/tags |
Public | Moderate (2 min) | Reverse lookup |
Example queries:
// GET /insights — List published insights
supabase.from("catalog_insights")
.select("id, slug, title, description, difficulty, duration_minutes")
.eq("status", "published")
// GET /insights/{slug}/topics — Topics for an insight
supabase.from("topic_insights")
.select(`
topic:topics!inner(id, slug, name, icon, color, status)
`)
.eq("insight_id", insightId)
.eq("topics.status", "published")
Function 5: content — Lesson Body (exists, extend path)¶
functions/content/index.ts (EXISTS — update path to use /insights/)
GET /content/insights/{slug} Markdown/HTML content
GET /content/insights/{slug}/versions Version history
GET /content/insights/{slug}/versions/{v} Specific version
Existing Functions (no changes)¶
| Function | Path | Notes |
|---|---|---|
explore |
/explore/* |
Extend facets to include topic/tag filters |
search |
/search/* |
Extend to search catalog entities |
learning-path |
/path/* |
Operates on insight prerequisites, no hierarchy dependency |
user |
/me/* |
User profile, progress, events |
auth |
/auth/* |
Authentication |
snapshots |
/snapshots/* |
Content versioning |
metadata |
/metadata/* |
Static enumerations |
Complete API Surface¶
/topics # CURATED DISCOVERY
├── GET / # List published topics
├── GET /{slug} # Topic details
├── GET /{slug}/journeys # Journeys in this topic
└── GET /{slug}/insights # Insights in this topic
/tags # SYSTEM CLASSIFICATION
├── GET / # List tags (?category=technology)
├── GET /{slug} # Tag details + counts
├── GET /{slug}/journeys # Journeys with this tag
└── GET /{slug}/insights # Insights with this tag
/journeys # LEARNING PATHS
├── GET / # List published journeys
├── GET /{id} # Journey landing page + stats
├── GET /{id}/chapters # Ordered chapters → insights
├── GET /{id}/topics # ← reverse: what topics feature this
└── GET /{id}/tags # ← reverse: what tags classify this
/insights # LESSON METADATA
├── GET / # List published insights
├── GET /{slug} # Insight metadata
├── GET /{slug}/topics # ← reverse: what topics feature this
└── GET /{slug}/tags # ← reverse: what tags classify this
/content/insights/{slug} # LESSON BODY
├── GET / # Markdown/HTML content
├── GET /versions # Version history
└── GET /versions/{v} # Specific version
/explore # FACETED DISCOVERY (extend)
├── GET / # Mixed-type search
├── GET /facets # Available filters (add topic, tag)
└── GET /featured # Curated sections
/search # FULL-TEXT SEARCH
├── GET / # Search across entities
└── GET /autocomplete # Suggestions
/path # LEARNING PATH CALCULATION
/me # USER PROFILE & PROGRESS
/auth # AUTHENTICATION
/snapshots # CONTENT VERSIONING
/metadata # STATIC ENUMERATIONS
Module Structure¶
Each new module follows the established pattern:
src/modules/
├── topics/ # NEW — curated discovery
│ ├── schema.ts # TopicSchema, TopicListQuerySchema, etc.
│ ├── types.ts # ITopicService interface
│ ├── service.ts # TopicService (queries topics + junction tables)
│ ├── routes.ts # OpenAPI routes for all /topics/* endpoints
│ └── index.ts # export { default } from "./routes.ts"
│
├── tags/ # NEW — system classification
│ ├── schema.ts # TagSchema, TagListQuerySchema, etc.
│ ├── types.ts # ITagService interface
│ ├── service.ts # TagService (queries tags + junction tables)
│ ├── routes.ts # OpenAPI routes for all /tags/* endpoints
│ └── index.ts # export { default } from "./routes.ts"
│
├── insights/ # NEW — standalone insight metadata
│ ├── schema.ts # InsightSchema, InsightListQuerySchema, etc.
│ ├── types.ts # IInsightService interface
│ ├── service.ts # InsightService (queries catalog_insights + junctions)
│ ├── routes.ts # OpenAPI routes for all /insights/* endpoints
│ └── index.ts # export { default } from "./routes.ts"
│
├── journeys/ # EXISTS — extend with topic/tag sub-routes
│ ├── schema.ts # Extend with TopicRef, TagRef schemas
│ ├── types.ts # Extend IJourneyService
│ ├── service.ts # Extend with getTopics(), getTags()
│ ├── routes.ts # Add GET /{id}/topics, GET /{id}/tags
│ └── index.ts
Why This Architecture Supports Future Evolution¶
The loose coupling between discovery and content means new discovery patterns cost zero content changes:
| Future Discovery Pattern | What You Create | Content Changes |
|---|---|---|
| "Collections" (user-curated playlists) | collections table + collection_journeys + collection_insights + edge function |
None |
| "Learning Tracks" (multi-journey paths) | tracks table + track_journeys + edge function |
None |
| "Difficulty Paths" (beginner → expert) | difficulty_paths table + junction tables + edge function |
None |
| "Instructor Profiles" | instructors table + instructor_journeys + edge function |
None |
| "Seasonal Picks" | seasonal_picks table + junction tables + edge function |
None |
Each new discovery type follows the same pattern:
1. Create entity table
2. Create junction tables to catalog_journeys and/or catalog_insights
3. Create module in src/modules/
4. Create edge function in functions/
5. No changes to existing content tables, services, or edge functions
Implementation Phases¶
Phase 1: Database Migration¶
Create the 6 new tables (topics, tags, topic_journeys, topic_insights, tag_journeys, tag_insights) with indexes, RLS policies, and update triggers. Seed initial topics from existing domains data.
Files:
- supabase/migrations/YYYYMMDD_add_topics_tags_discovery.sql
Phase 2: Topics Module + Edge Function¶
Build the topics module and deploy as an independent edge function.
Files:
- src/modules/topics/ (schema, types, service, routes, index)
- functions/topics/index.ts
Phase 3: Tags Module + Edge Function¶
Build the tags module and deploy as an independent edge function.
Files:
- src/modules/tags/ (schema, types, service, routes, index)
- functions/tags/index.ts
Phase 4: Insights Module + Edge Function¶
Build the standalone insights module (metadata endpoints, distinct from content delivery).
Files:
- src/modules/insights/ (schema, types, service, routes, index)
- functions/insights/index.ts
Phase 5: Extend Journey Module¶
Add reverse-lookup routes (/journeys/{id}/topics, /journeys/{id}/tags) and a list endpoint (GET /journeys).
Files:
- src/modules/journeys/ (extend schema, service, routes)
- functions/journey-catalog/index.ts (mount new routes)
Phase 6: Extend Explore + Search¶
Update the explore module to support topic and tag facets. Update search to include catalog entities.
Files:
- src/modules/explore/ (extend service and schema)
- src/services/search.ts (add catalog entity search)
Phase 7: SDK Regeneration + Legacy Deprecation¶
Regenerate the OpenAPI spec and SDK. Mark legacy /graph/* endpoints as deprecated.
Files:
- mcp/openapi.json (regenerate)
- sdk/ (rebuild)
- src/routes/graph/ (add deprecation headers)
Summary of All Database Tables (Target State)¶
Content Context (stable, self-contained)¶
| Table | Purpose | Status |
|---|---|---|
catalog_journeys |
Journey content entities | Exists |
catalog_chapters |
Chapter entities | Exists |
catalog_insights |
Insight/lesson entities | Exists |
catalog_journey_chapters |
Journey ↔ Chapter (ordered) | Exists |
catalog_chapter_insights |
Chapter ↔ Insight (ordered) | Exists |
catalog_journey_enrollments |
User enrollment tracking | Exists |
catalog_journey_ratings |
User ratings | Exists |
Discovery Context (extensible, loosely coupled)¶
| Table | Purpose | Status |
|---|---|---|
topics |
Curated user-facing discovery categories | New |
tags |
System classification keywords | New |
Association Layer (the only coupling between contexts)¶
| Table | Purpose | Status |
|---|---|---|
topic_journeys |
Topics ↔ Journeys (many-to-many) | New |
topic_insights |
Topics ↔ Insights (many-to-many) | New |
tag_journeys |
Tags ↔ Journeys (many-to-many) | New |
tag_insights |
Tags ↔ Insights (many-to-many) | New |
User Context (unchanged)¶
| Table | Purpose | Status |
|---|---|---|
user_profiles |
User profile data | Exists |
learning_events |
Activity tracking | Exists |
milestones |
Achievements | Exists |
review_schedules |
Spaced repetition | Exists |
subscriptions |
Plan management | Exists |
Legacy Context (to be deprecated)¶
| Table | Purpose | Action |
|---|---|---|
domains |
Old parent-level entity | Migrate data → topics, then deprecate |
trails |
Old journey entity | Superseded by catalog_journeys |
concepts |
Old chapter entity | Superseded by catalog_chapters |
sparks |
Old insight entity | Superseded by catalog_insights |
beacons |
Old tag entity | Migrate data → tags, then deprecate |
concept_sparks |
Old chapter ↔ insight junction | Superseded by catalog_chapter_insights |
spark_beacons |
Old insight ↔ tag junction | Superseded by tag_insights |
domain_categories |
Old category grouping | Evaluate if needed for topics |
Summary of All Edge Functions (Target State)¶
| Function | Path | Auth | New/Exists |
|---|---|---|---|
topics |
/topics/* |
Public | New |
tags |
/tags/* |
Public | New |
journey-catalog |
/journeys/* |
Optional | Exists (extend) |
insights |
/insights/* |
Public | New |
content |
/content/* |
Optional | Exists (update paths) |
explore |
/explore/* |
Public | Exists (extend facets) |
search |
/search/* |
Required | Exists (extend to catalog) |
learning-path |
/path/* |
Optional | Exists (no changes) |
user |
/me/* |
Required | Exists (no changes) |
auth |
/auth/* |
Public | Exists (no changes) |
snapshots |
/snapshots/* |
Required | Exists (no changes) |
metadata |
/metadata/* |
Public | Exists (no changes) |
health |
/health |
Public | Exists (no changes) |