Skip to content

Topic navigation surfaces (ADR-0012 follow-up)

Status: Approved 2026-05-06 Branch: feature/topic-graph

Goal

Make typed topics (β.1.7) navigable. Today they’re rendered as inline highlights and chips, but those highlights link nowhere. After this slice the user can click any topic anywhere → see all memories that mention it, browse a topic index, search by topic, and read the Universe edge mix at a glance. Web AND mobile, no Pro gate.

Items

1. /topic/[id] detail page

Webapps/web/app/(app)/topic/[id]/page.tsx. Server component.

Query: topics (id, label, kind) + memories joined via memory_topics (id, summary, transcript snippet, recorded_at, recording_id, recordings(…)).

Layout:

  • Header: kind chip (colored per KIND_STYLES) + topic label + “{N} memories”
  • Body: same row layout as Today (audio button + summary + date), each row → /memory/[id]
  • Back link → wherever user came from (use /topics as fallback)

Mobileapps/mobile/app/topic/[id].tsx. Same data shape. SectionList grouped by date bucket (matches Today pattern).

Both surfaces resolve topic by UUID (id), not slug. Labels can collide and contain punctuation.

2. /topics index

Webapps/web/app/(app)/topics/page.tsx. Server component.

Data: SQL view topic_with_counts (added in a small migration) returning (id, user_id, label, kind, memory_count) with security_invoker=true so RLS owns scoping.

Layout:

  • Filter pill row at top: All / Person / Place / Project / Theme / Event (kind filter, query param)
  • List sorted by memory_count desc, label asc
  • Each row: kind chip + label + count, → /topic/[id]
  • Empty state copy if 0 topics

Mobileapps/mobile/app/topics.tsx. Same data via topic_with_counts view.

Nav — Add “Topics” link to web (app)/layout.tsx nav (between Today and Universe). Mobile gets it as a Link from Today’s header (alongside Settings).

3. Today feed entity badges

Web — extend lib/search.ts to also fetch memory_topics(topics(id, label, kind)). Render up to 3 chips per row in today/page.tsx, prefer person/place/project over theme/event. Chips link to /topic/[id] (with e.stopPropagation() so they don’t trigger the row’s navigation).

Mobile — extend app/index.tsx load() to pull memory_topics(topics(id, label, kind)). Render same top-3 chips in MemoryRowItem. Tap → /topic/[id].

4. Universe header edge breakdown

apps/web/app/(app)/universe/page.tsx — split edges into topic vs semantic counts. Render "42 connections (18 topic + 24 semantic)". ~5 lines.

5. topic: search operator

Web — extend lib/search.ts to parse topic:Foo and topic:"Foo Bar" tokens out of the query string. Resolve each via topics.canonical_key lower(label) match for the current user. Filter the candidate memory id set through memory_topics. Free-text remainder still goes through FTS + semantic. Results = intersection.

Mobileapp/index.tsx load() — same parser, applies the topic filter via .in('id', memoryIds) after resolving topic ids.

Bonus: /today?topic=<id> URL param, populated by /topic/[id] “see all in feed” link.

Data layer

One migration: supabase/migrations/20260507000000_topic_with_counts_view.sql. Adds the topic_with_counts view used by item 2.

Order

  1. Spec + view migration
  2. Item 4 (warmup, web only, 5 lines)
  3. Item 1 web → 1 mobile
  4. Item 2 web (+ nav) → 2 mobile
  5. Item 3 web → 3 mobile
  6. Item 5 web → 5 mobile
  7. Wire web inline highlights to /topic/[id]
  8. Type check (pnpm typecheck per app), smoke test

Out of scope

  • User-driven topic merge/split UX (deferred per ADR-0012)
  • Per-kind icons (kind colors only)
  • Universe filter by topic id (Universe already has its own pill)
  • Topic edit/delete

Risk

  • memory_topics(topics(...)) join is many-to-one in supabase-js types but typed as array; same pattern already used on memory detail — copy the cast.
  • Mobile RLS: memory_topics select policy is via parent memory; same path the join already uses.
  • View security_invoker=true is required so auth.uid() resolves from the calling user, not the view owner. Mirror what 20260506000004 did for pipeline_stuck_jobs.