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— firescontent/seed.requested, full Vault→Convex sync via Inngestjoelclaw content verify— firescontent/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)
- ✅ Created
packages/system-bus/src/lib/convex-content-sync.ts— shared upsert logic for ADRs and posts - ✅ Added
sync-to-convexstep to content-sync Inngest function (upserts changed files after vault sync) - ✅ Added
gray-matterdependency to system-bus - ✅ Manual full seed: 175 ADRs + 21 posts synced to Convex
Phase 2 — Remove repo content, simplify pipeline (✅ shipped 2026-02-28)
- ✅ Rewrote
content-sync.ts— Vault→Convex direct, no file copy/git/push - ✅ Cleaned
apps/web/content/— deleted 175 ADR .md + 22 post .mdx files, added README + .gitignore - ✅ Removed filesystem fallback from
adrs.tsandposts.ts(Convex-only) - ✅ Added
joelclaw content seedandjoelclaw content verifyCLI commands - ✅ Added
content-verifyInngest function (diffs Vault vs Convex) - ✅ Fixed post frontmatter — store type, tags, source, channel, duration, updated in Convex
- ✅ Updated Convex
postFieldsValidatorto accept full frontmatter - ✅ Reseeded Convex: 175 ADRs + 21 posts with complete fields
Phase 3 — Write durability (✅ shipped 2026-02-28)
- ✅ SHA-256 content hash on every upsert — stored in
contentHashfield - ✅ Convex mutation compares hash, returns
skippedwhen content unchanged (saves write units) - ✅ Verified: second seed skips all 175 ADRs (0 writes)
- Anti-regression guardrails in
content-review-apply— out of scope (belongs to ADR-0106)
Phase 4 — Observability & realtime (✅ shipped 2026-02-28)
- ✅ content-sync emits OTEL start/finish events with upserted/skipped/error counts
- ✅ content-verify emits OTEL event with gap report
- ✅
totalSkippedmetric tracked in OTEL metadata - ✅ ISR cache revalidation step in content-sync — purges tags for changed slugs via
/api/revalidate - ✅
ContentLiveclient component — subscribes to ConvexcontentHash, triggersrouter.refresh()on change - ✅
ConvexReaderProvider— lightweight read-only Convex client for content pages - ✅
getContentHashConvex query — minimal subscription endpoint (hash + updatedAt only) - ✅ Shared
revalidateContentCachelib (packages/system-bus/src/lib/revalidate.ts) - 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 seedandjoelclaw content verifywork 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
Related
- ADR-0084 Unified ContentResource schema
- ADR-0106 ADR Review Pipeline
- ADR-0112 Unified caching layer
- ADR-0154 Content migration MDX to Convex