Skip to content

ADR-0014: Neon + Drizzle as the shared data layer

Guide

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:

  1. Intranet (intranet/) — static Astro 6 portal. Reads data at build time via scripts/sync-content.mjs. Currently uses repo files (JSON, Markdown) as the source of truth. Deferred: live chatter store (ADR-0006).

  2. 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 from instrumentation.md §1)
  • chunk_outcomes_agg (rolling weight store for the feedback loop)
  • user_profiles (requires auth ADR)
  • quiz_questions (stay in .md until library grows)
  • embedding vector(1536) column on chunks (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_URL is absent (CI passes, local dev works)
  • Migration discipline (manual only) eliminates accidental schema changes on deploy

Negative / trade-offs:

  • sync-content.mjs gains a Neon dependency; adds a few hundred ms to build time when neon=hit
  • intranet/package.json gains @neondatabase/serverless and drizzle-orm as 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_sessions schema is specified but not created until platform/ is built