Database conventions
Nexus has three logically separate PostgreSQL databases. Each is on its own Neon project in production and its own Docker container locally.
Database inventory
Section titled “Database inventory”| Database | ORM | App | Neon project |
|---|---|---|---|
| Scheduler | Drizzle ORM | scheduler-api, scheduler-worker |
scheduler-postgres |
| Vendure | TypeORM | vendure-server, vendure-worker |
vendure-postgres |
| Directus | Directus managed | directus |
directus-postgres |
They do not share a connection pool or cross-reference each other via foreign keys. Cross-system consistency is maintained by the convergence workflows.
Scheduler DB (Drizzle)
Section titled “Scheduler DB (Drizzle)”Schema
Section titled “Schema”The entire schema is defined in one file:
apps/scheduler-api/src/db/schema.ts (~108 KB consolidated)All tables, enums, and relations live here. Do not split the schema across multiple files — Drizzle’s relational queries require the schema to be in scope together.
Migrations
Section titled “Migrations”Migrations live in apps/scheduler-api/drizzle/. They are journal-driven:
every migration file must have a corresponding entry in drizzle/meta/journal.json.
CI checks journal sync on every PR:
# Run locally to verifypnpm --filter scheduler-api db:checkTo generate a new migration after schema changes:
pnpm --filter scheduler-api db:generate # generates SQL migrationpnpm --filter scheduler-api db:migrate # applies to local DBEnum split rule
Section titled “Enum split rule”PostgreSQL does not allow using a newly-added enum value in the same transaction:
-- ❌ One migration file — PG rejects at commitALTER TYPE booking_status ADD VALUE 'waitlisted';UPDATE booking SET status = 'waitlisted' WHERE …;
-- ✅ Two migration files-- File 1: ALTER TYPE booking_status ADD VALUE 'waitlisted';-- File 2 (separate file, applied in next run):-- UPDATE booking SET status = 'waitlisted' WHERE …;Drizzle wraps each migration file in a transaction. The ADD VALUE succeeds, but using the new value in the same transaction fails. Always split.
Key tables
Section titled “Key tables”| Table | Description |
|---|---|
business |
Tenant businesses (core entity) |
booking |
Appointment bookings (pending → confirmed → completed/cancelled/no_show) |
staff |
Staff members of a business |
customer |
End customers |
service |
Bookable services (name, duration, price) |
availability_rule |
Staff/service availability windows |
session |
Grouped booking sessions |
notification |
Notification records (all channels) |
chat_thread |
Support ticket threads |
billing_invoice_mirror |
Local Stripe invoice cache |
business_billing_audit |
Billing event log |
platform_config |
Runtime feature toggles |
domain |
Custom domain records + ACME state |
Vendure DB (TypeORM)
Section titled “Vendure DB (TypeORM)”Vendure manages its own schema through TypeORM migrations:
pnpm --filter vendure-server migrate # Generate + applyThe Vendure DB is separate from the scheduler DB. Do not mix them.
Directus DB (Directus managed)
Section titled “Directus DB (Directus managed)”Directus auto-manages its schema. Changes are tracked via snapshots:
# After making schema changes in the Directus admin UI:npx directus schema snapshot ./snapshots/latest.yaml
# On deploy / bootstrap:npx directus schema apply ./snapshots/latest.yamlCommit the snapshot file whenever the Directus schema changes. This is the only way to reproduce schema changes across environments.
Migration in CI
Section titled “Migration in CI”The CI lint-typecheck-test-build job checks Drizzle journal sync before any
tests run. If you add a migration file without updating the journal, CI fails.
Local database management
Section titled “Local database management”# Start all three DBsdocker compose -f docker/docker-compose.yml up -d
# Reset and re-migrate scheduler DBpnpm --filter scheduler-api db:drop && pnpm --filter scheduler-api db:migrate
# Connect to scheduler DBpsql postgresql://postgres:postgres@localhost:5434/scheduler