Skip to content

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:

  1. 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.

  2. 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.

  3. 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_chapters junction 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)

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)