ADR-0014: Neon + Drizzle as the shared data layer
Status: Accepted
Date: 2026-05-31
Approved by: Kevin Wong (Principal)
Related: ADR-0006 (intranet static), ADR-0008 (Vercel deploy), ADR-0009 (deploy budget), ADR-0013 (learning platform stack — database deferred here)
Context
Neon (serverless Postgres) has been connected to the Vercel project. The env vars
DATABASE_URL (pooled) and DATABASE_URL_UNPOOLED (direct) are scoped to Production
and Preview environments.
Two product surfaces need a shared data layer:
-
Intranet (
intranet/) — static Astro 6 portal. Reads data at build time viascripts/sync-content.mjs. Currently uses repo files (JSON, Markdown) as the source of truth. Deferred: live chatter store (ADR-0006). -
Learning platform (
platform/) — Next.js 15 App Router, not yet built (ADR-0013). Will need: chunk retrieval, learner progress, rubric/scoring, eventually vector search. ADR-0013 explicitly deferred the database decision here.
The authoritative data model is already specified:
docs/instrumentation.md§1 (LessonSession, 18 fields) and §2 (ChunkRecord, 10 fields)docs/skill-ontology.md(SkillNode interface, 25 nodes across 5 domains)
Decision
Place Drizzle + Neon at db/ in the repository root — not inside any product directory.
Both intranet/ and platform/ import from ../../db/ (or via path alias @db). This
prevents schema duplication and ensures the data model stays in one place as the org grows.
The intranet stays output: "static". No adapter, no SSR, no server functions are added.
sync-content.mjs optionally queries Neon at build time and falls back to local files when
DATABASE_URL is absent (local dev, CI). This preserves the intranet’s performance and
deployment simplicity while giving Vercel production builds authoritative data.
The learning platform will use Neon at runtime via the same db/client.ts export,
adding its own tables in a second migration (0001_platform.sql) when the first route is built.
Schema — v1 (four tables)
chunks — mirrors instrumentation.md §2
The canonical chunk library. Powers the intranet’s chunk stat card today; powers the platform’s retrieval-first lesson assembly tomorrow.
skill_nodes — mirrors skill-ontology.md SkillNode interface
Reference data for skill resolution and misconception matching. Seeded once from the 25-node ontology.
chatter_channels and chatter_messages
Replace the seed JSON at intranet/src/data/chatter.json. Queried at intranet build time.
Deferred to 0001_platform.sql
lesson_sessions(18-field schema frominstrumentation.md§1)chunk_outcomes_agg(rolling weight store for the feedback loop)user_profiles(requires auth ADR)quiz_questions(stay in.mduntil library grows)embedding vector(1536)column onchunks(add before first ingestion run; defer dimension choice until the ingestion pipeline is specified)
Env var discipline
Only two vars are used by application code:
| Var | Used by | Purpose |
|---|---|---|
DATABASE_URL |
db/client.ts, sync-content.mjs |
Pooled connection for queries |
DATABASE_URL_UNPOOLED |
drizzle-kit migrate (manual only) |
Direct connection for migrations |
The remaining 16 Neon-wired vars (POSTGRES_*, PG*, NEON_*, VITE_NEON_AUTH_URL)
are auto-wired by Vercel and are not referenced by application code.
Migrations are applied manually by the operator (npm run db:migrate from db/).
They are never auto-triggered from a Vercel build step or CI hook. This prevents schema
corruption from concurrent deploys.
Consequences
Positive:
db/schema is the single source of truth for both surfaces; no fork needed when platform ships- Intranet build degrades gracefully when
DATABASE_URLis absent (CI passes, local dev works) - Migration discipline (manual only) eliminates accidental schema changes on deploy
Negative / trade-offs:
sync-content.mjsgains a Neon dependency; adds a few hundred ms to build time whenneon=hitintranet/package.jsongains@neondatabase/serverlessanddrizzle-ormas runtime deps (they are only invoked in Node.js build context, never in the browser bundle)
Not decided here:
- Vector search /
pgvector— defer to the ADR for the first retrieval route - Authentication for the learning platform — defer to a future auth ADR
- The full
lesson_sessionsschema is specified but not created untilplatform/is built