Service Layer Runbook
The repeatable procedure for taking a service-layer module from its locked design through to built-and-live. Run once per module, in dependency order.
Applies to: platform, identity, shared, multi_loc, payments, integrations, files, ai, search — and any other module whose primary deliverable is a service contract and schema rather than a rich UI surface.
Context
Starting reality: the database is a blank Supabase instance — no tables exist. The locked designs in docs/old/ are the starting point for each module's schema, not confirmed final designs. Migration (creating tables in the live DB) happens in this runbook, per module, in dependency order. Platform migrates first because every other module FKs to it.
Two gates: the Schema Lock (Step 6) and the Close-out gate (Step 13). A module is not done until Step 13's gate passes. Everything between the gates is work-in-progress.
Companion docs — read these before you begin:
docs/database/SCHEMA_CONVENTIONS.md— the schema rules every migration must followdocs/database/SCHEMA_DESIGN_RUNBOOK.md— the Section 4 audit invoked at Steps 5–6docs/ai/AI_CAPABILITY_PLANE.md— the AI review at Step 3docs/modules/MODULE_INDEX.md— updated at Step 13docs/modules/CROSS_MODULE_CONTRACTS.md— updated at Step 13
Phase 1 — Design (before lock)
Step 1 — Review existing module spec
What you do: read the module's existing spec in docs/old/ if one exists. Understand what the module is, what it owns, what it does, and what seams it has with other modules. If no spec exists, note that and proceed from the schema alone.
Read: docs/old/modules/module_specs/<module>.md (if present)
Produce: a working understanding of the module's scope, ownership, and seams. Note anything that looks stale or that conflicts with current architecture decisions in docs/decisions/PROJECT_DECISIONS.md.
Step 2 — Review existing schema spec
What you do: read the module's locked schema design in docs/old/. Study every table, column, type, constraint, FK, and index. This is the pre-AI-plane design — it is the starting point, not the final schema. The schema will change in Step 3.
Read: docs/old/schema/schema_modules/schema_<module>.md
Produce: a clear picture of the pre-plane schema and a note of any obvious gaps or stale references (cross-module FKs that have changed, superseded table names, etc.).
Step 3 — AI-plane / AI-feature review
What you do: walk the module against the AI Capability Plane. For each relevant capability (B-series entries in docs/ai/AI_CAPABILITY_PLANE.md), decide whether this module needs it and at what authority level. Identify every schema change those AI features require — new fields, flags, state tables, audit columns, consent columns, or AI-output caches. Record each needed change explicitly before touching the schema.
Read: docs/ai/AI_CAPABILITY_PLANE.md, docs/ai/AI_CAPABILITY_PLANE_READING.md
Produce: a list of AI features this module uses, the authority level for each, and the concrete schema additions each requires. This list drives Step 5.
Service-layer modules vary widely here.
aiandintegrationshave deep AI involvement;sharedandmulti_locmay have none. Do not skip this step — even a null result is a confirmed decision.
Step 4 — Decide lean page structure
What you do: decide what online or maintenance pages this module needs and what data each page must surface. For service-layer modules this is often minimal — a status page, a configuration panel, or nothing at all. The purpose of this step is to identify any data the UI needs that the schema must hold. If there are no pages, confirm that and move on.
Read: docs/portal/ (existing page specs), docs/decisions/PROJECT_DECISIONS.md (architecture decisions that affect page structure)
Produce: a list of pages (may be empty) and, for each page, the data it requires. Flag any data requirement that is not yet covered by the schema as designed.
Step 5 — Recheck schema coverage
What you do: verify that the schema — original (Step 2) plus AI deltas (Step 3) plus any page-data needs (Step 4) — holds everything the module requires. Add any missing fields, flags, or tables. The schema is design-complete when all three layers are satisfied.
Read: outputs of Steps 2–4
Produce: the final schema definition for this module, with all AI deltas and page-data additions folded in. This is what gets locked in Step 6 and documented in Step 7.
Step 6 — Lock schema 🔒
What you do: run the SCHEMA_DESIGN_RUNBOOK Section 4 audit (items A–P) against the design-complete schema from Step 5. Triage every finding — fix blockers before locking, log acceptable deferrals in docs/open-items/OPEN_ITEMS.md. Once the audit passes, the schema is locked. Update docs/modules/MODULE_BUILD_STATUS.md → Schema locked = ✅ for this module.
Read: docs/database/SCHEMA_DESIGN_RUNBOOK.md Section 4
Produce: a passing Section 4 audit; a locked schema definition; any audit findings logged in OPEN_ITEMS.
GATE — do not proceed past Step 6 until:
- Steps 3, 4, and 5 are complete (AI review done, page-data needs identified, schema coverage confirmed)
- The Section 4 audit passes (or all blockers are resolved)
- Schema locked column is marked ✅ in MODULE_BUILD_STATUS
Phase 2 — Document
Step 7 — Author clean specs
What you do: author the two definitive spec files for this module — the module spec and the schema spec — reflecting the locked design from Step 6.
- Module spec →
docs/modules/module_spec/<module>.md: what the module is, what it owns, its features, and its service contract (the API surface other modules call). Module-specific decisions and design rationale go here. - Schema spec →
docs/database/schema_docs/<module>.md: every table, column, type, constraint, index, and FK — including the AI deltas from Step 3. This is the migration source of truth.
Cross-cutting decisions (anything that spans modules or sets project-wide rules) do not go in these files — they go to docs/decisions/PROJECT_DECISIONS.md.
Read: locked schema from Step 6; AI feature list from Step 3; docs/modules/CROSS_MODULE_CONTRACTS.md for seam references
Produce: docs/modules/module_spec/<module>.md, docs/database/schema_docs/<module>.md
Step 8 — Document open items
What you do: record every unresolved item in docs/open-items/OPEN_ITEMS.md. This includes cross-module dependencies this module introduces, deferred FKs (forward-refs that can't be enforced yet because the target module isn't migrated), deferred API seams, deferred integrations, and any issues found during the audit or spec-writing. One row per item.
Read: audit findings from Step 6; seams in docs/modules/CROSS_MODULE_CONTRACTS.md; outputs of Steps 3–7
Produce: new rows in OPEN_ITEMS for every unresolved item. Use the columns: Module | Type | What's needed | Status | Trigger. Type values: dependency · FK · API · integration · issue.
Phase 3 — Build (after lock)
Step 9 — Migrate DB tables
What you do: create the module's tables in the blank Supabase database. Follow docs/database/SCHEMA_CONVENTIONS.md → Migrations exactly: Drizzle Kit (drizzle-kit generate / drizzle-kit migrate) for schema structure and migrations; raw SQL alongside (or via Drizzle's native pgPolicy/.enableRLS()) for RLS policies, triggers, CHECK constraints, partial unique indexes, and seed data; every migration includes a -- DOWN section (authored by hand — Drizzle does not auto-generate down migrations). Use direct port 5432 for application migrations; pooled port 6543 only for service-role operations. Honor dependency order — every upstream module this one FKs to must already be migrated. After migrating, close any deferred FKs from OPEN_ITEMS that this migration now enables (update their Status to closed).
Read: docs/database/SCHEMA_CONVENTIONS.md, docs/database/schema_docs/<module>.md, docs/open-items/OPEN_ITEMS.md (for FKs to close)
Produce: a working migration in the correct order; closed FK rows in OPEN_ITEMS; tables live in Supabase.
Verification checklist — CHECK constraint VALUES (lesson logged 2026-06-29): After migration, verify CHECK constraint values (enums), not just column counts. Column counts were verified at Batch B Pass 2 lock; the
identity_access_event.event_typeCHECK value set was not enumerated — the Drizzle schema was authored with a completely different set of values (22 auth-only vs. 23 doc app-level), and this drift was not caught until Phase 3 build. For every table with aCHECK (col IN (...))orCHECK (col = ANY(...))constraint, run:SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = '<constraint_name>';and compare each value against the schema doc. Add this step to the post-migration sign-off.Verification checklist — COLUMN-LEVEL DIFF, not count diff (lesson logged 2026-06-29): Column count matching is necessary but not sufficient. A column dropped in one place and added in another nets to zero — the count passes but drift exists. After every migration, run a per-table column diff:
SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = '<schema>' AND table_name = '<table>' ORDER BY ordinal_position;and compare each row against the schema doc. Do not sign off on "column count matches" — sign off on "column names and types match." See PROJECT_DECISIONS.md §13 for the DB-authoritative reconciliation direction rule.
Step 10 — Build API
What you do: build the module's service class — the typed API surface that other modules and pages call. For service-layer modules this is the primary deliverable. The service class encapsulates all reads and writes to this module's tables; no other module queries these tables directly. Implement the service contract defined in the module spec from Step 7, per docs/modules/CROSS_MODULE_CONTRACTS.md.
Read: docs/modules/module_spec/<module>.md (service contract), docs/modules/CROSS_MODULE_CONTRACTS.md (seams)
Produce: the module's service class (e.g. PlatformService, FilesService), tested and callable by upstream modules.
Step 11 — Build pages (conditional)
What you do: if the module has maintenance or admin pages (identified in Step 4), build them now. Pages call the Step 10 service API — they do not query the DB directly. If the module has no pages, skip this step.
Read: page list from Step 4; docs/modules/module_spec/<module>.md (service contract — pages call this, not the DB)
Produce: working pages (if any). If skipped, note N/A in MODULE_BUILD_STATUS.
Step 12 — Branding + portal (conditional, if pages exist)
What you do: apply branding to any pages built in Step 11. Update portal navigation and document each page in docs/portal/ — what it is, what data it surfaces, what actions it supports, which service methods it calls. Do not restate the service contract; reference it. If Step 11 was skipped, skip this step.
Read: docs/portal/ (existing portal docs), branding reference
Produce: branded pages; updated portal docs. If skipped, note N/A in MODULE_BUILD_STATUS.
Phase 4 — Close-out
Step 13 — Validate + close-out gate ✅
What you do: re-check docs/open-items/OPEN_ITEMS.md. Resolve or close every item this module's completion now unblocks. Confirm the module's own open items are either resolved or properly deferred with a concrete trigger. Then update all status and system docs:
| Doc | What to update |
|---|---|
docs/modules/MODULE_BUILD_STATUS.md |
Set this module's row: Schema locked ✅, Docs ✅, API ✅, Pages ✅ or N/A, Issues count, Done ✅ |
docs/modules/MODULE_INDEX.md |
Reflect any schema changes (table/col counts if AI deltas added tables or columns) |
docs/modules/CROSS_MODULE_CONTRACTS.md |
Add this module's seams to the catalog |
docs/decisions/PROJECT_DECISIONS.md |
Add any cross-cutting decision this module surfaced |
CLAUDE.md |
Update if module status is tracked there |
Read: docs/open-items/OPEN_ITEMS.md; all docs listed above
Produce: updated OPEN_ITEMS (closed rows); updated MODULE_BUILD_STATUS, MODULE_INDEX, CROSS_MODULE_CONTRACTS, PROJECT_DECISIONS, CLAUDE.md as applicable.
GATE — the module is not done until:
- All docs above are updated
- MODULE_BUILD_STATUS Done column is ✅
- No OPEN_ITEMS rows for this module remain with Status = open (or all open items have a concrete trigger logged)