ADR-0168shipped

Convex-Canonical Content Lifecycle (No Repo MDX Sources)

Status

shipped — fully verified end-to-end 2026-02-28

Context

The drafting → publishing → feedback → update → publish loop regressed in production.

Observed failure modes:

  • feedback was accepted and marked applied; a later review run rewrote the same resource, silently reverting edits
  • mixed authority between filesystem content and Convex content
  • whole-document rewrites without regression guards against prior accepted feedback
  • cache invalidation and deployment timing can hide stale output long enough to look like successful publish
  • new ADRs written to Vault were invisible on the live site because content-sync committed to git but never pushed to Convex (the only production read path)

Root cause: two sources of truth — Vault files and Convex records — with no guaranteed sync between them.

Decision

1) Vault is write-side, Convex is read-side

Vault (author/edit) → content-sync (Inngest) → Convex (production reads)
                                               → Typesense (search index)

For all runtime content classes, source of truth is Convex contentResources:

  • article:*
  • adr:*
  • discovery:*

Vault (~/Vault/docs/decisions/, ~/Vault/Resources/) is the canonical authoring surface. Authors edit markdown there. The pipeline reads Vault directly and upserts to Convex — no intermediate repo copy.

2) No content files in the repo

apps/web/content/ retains an empty skeleton with a README explaining content lives in Convex. No markdown/MDX files committed. The content-sync pipeline no longer copies files to the repo, commits, or pushes.

3) Durable write contract

Every update/write must include a baseline guard (hash or revision ID) and reject stale writes. No blind overwrite of content that has moved since read.

4) Feedback application must preserve prior accepted intent

content-review-apply must include prior resolved/applied feedback context and fail if a rewrite regresses previously accepted replacement assertions unless explicitly reversed by new feedback.

5) Publish is immediate and observable

Publish/update action must:

  • persist content + revision metadata in Convex
  • emit structured telemetry (resourceId, contentSlug, contentHashBefore, contentHashAfter, revisionResourceId, runId)
  • revalidate both tags and paths immediately

Required post revalidation targets:

  • tags: post:<slug>, article:<slug>, articles
  • paths: /, /<slug>, /<slug>.md, /<slug>/md, /feed.xml, /sitemap.md

6) CLI seeding and verification

  • joelclaw content seed — fires content/seed.requested, full Vault→Convex sync via Inngest
  • joelclaw content verify — fires content/verify.requested, diffs Vault file list vs Convex records, reports gaps

Consequences

Good

  • Single source of truth removes file-vs-database drift bugs
  • Feedback durability becomes enforceable
  • Faster incident forensics through revision/hash lineage
  • Predictable publish semantics

Tradeoffs

  • Convex becomes hard dependency for content runtime
  • Migration work required for ADR/discovery readers currently on filesystem
  • Stricter write guards can produce explicit failures until all callers are updated

Implementation Plan

Phase 1 — Convex sync wired in (✅ shipped 2026-02-28)

  1. ✅ Created packages/system-bus/src/lib/convex-content-sync.ts — shared upsert logic for ADRs and posts
  2. ✅ Added sync-to-convex step to content-sync Inngest function (upserts changed files after vault sync)
  3. ✅ Added gray-matter dependency to system-bus
  4. ✅ Manual full seed: 175 ADRs + 21 posts synced to Convex

Phase 2 — Remove repo content, simplify pipeline (✅ shipped 2026-02-28)

  1. ✅ Rewrote content-sync.ts — Vault→Convex direct, no file copy/git/push
  2. ✅ Cleaned apps/web/content/ — deleted 175 ADR .md + 22 post .mdx files, added README + .gitignore
  3. ✅ Removed filesystem fallback from adrs.ts and posts.ts (Convex-only)
  4. ✅ Added joelclaw content seed and joelclaw content verify CLI commands
  5. ✅ Added content-verify Inngest function (diffs Vault vs Convex)
  6. ✅ Fixed post frontmatter — store type, tags, source, channel, duration, updated in Convex
  7. ✅ Updated Convex postFieldsValidator to accept full frontmatter
  8. ✅ Reseeded Convex: 175 ADRs + 21 posts with complete fields

Phase 3 — Write durability (✅ shipped 2026-02-28)

  1. ✅ SHA-256 content hash on every upsert — stored in contentHash field
  2. ✅ Convex mutation compares hash, returns skipped when content unchanged (saves write units)
  3. ✅ Verified: second seed skips all 175 ADRs (0 writes)
  4. Anti-regression guardrails in content-review-apply — out of scope (belongs to ADR-0106)

Phase 4 — Observability & realtime (✅ shipped 2026-02-28)

  1. ✅ content-sync emits OTEL start/finish events with upserted/skipped/error counts
  2. ✅ content-verify emits OTEL event with gap report
  3. totalSkipped metric tracked in OTEL metadata
  4. ✅ ISR cache revalidation step in content-sync — purges tags for changed slugs via /api/revalidate
  5. ContentLive client component — subscribes to Convex contentHash, triggers router.refresh() on change
  6. ConvexReaderProvider — lightweight read-only Convex client for content pages
  7. getContentHash Convex query — minimal subscription endpoint (hash + updatedAt only)
  8. ✅ Shared revalidateContentCache lib (packages/system-bus/src/lib/revalidate.ts)
  9. Publish flow revalidation telemetry — out of scope (tracked separately when publish flow is built)

Verification

All items verified end-to-end on 2026-02-28. Full loop tested: Vault edit → joelclaw content seed → Inngest content-sync → Convex upsert (1 written, 176 skipped) → ISR cache revalidation (200) → gateway notified → OTEL emitted → page renders from Convex → joelclaw content verify reports 0 gaps.

  • Convex sync step runs as part of content-sync pipeline
  • Full seed populates all Vault content in Convex
  • content-sync reads Vault directly, no repo file copy
  • apps/web/content/ contains no markdown files
  • Stale-write attempts skip with hash guard (no wasted writes)
  • Articles/ADRs render from Convex-only runtime path
  • No runtime content reads from apps/web/content/**
  • joelclaw content seed and joelclaw content verify work end-to-end
  • Second seed skips all 177 ADRs (hash guard verified)
  • ISR cache revalidation succeeds after upserts (tags: adrs, adr:<slug>)
  • ContentLive client component wired into ADR + post pages (Convex subscription → router.refresh())
  • ConvexReaderProvider provides lightweight read-only Convex client

Out of scope (tracked elsewhere)

  • Anti-regression guardrails for content-review-apply → ADR-0106
  • Publish flow revalidation telemetry → future ADR when publish flow is built
  • ADR-0084 Unified ContentResource schema
  • ADR-0106 ADR Review Pipeline
  • ADR-0112 Unified caching layer
  • ADR-0154 Content migration MDX to Convex