Database
PostgreSQL connection, schema push & multi-tenancy
The database block is the one piece of configuration no Nucleus app can boot without. It tells the framework where your PostgreSQL instance lives and how to project your entity definitions onto real tables.
Nucleus speaks to Postgres through Drizzle ORM. On startup it loads the generated schema file, ensures the target schema exists, then runs drizzle-kit's pushSchema to reconcile the live database with your declarations — creating tables and columns so you never hand-write a migration for routine changes.
The same block also unlocks multi-tenancy: flip one boolean and every tenant gets its own fully isolated Postgres schema, resolved per-request from the subdomain or a header.
Connection#
How Nucleus reaches your database. The connection string is never written inline — you store the name of an environment variable, and Nucleus resolves it at boot. If that variable is missing the process fails fast with a clear error, so a misconfigured deploy never starts in a half-broken state.
1{2 "database": {3 "url": "DATABASE_URL",4 "type": "postgres",5 "schemas": ["main"]6 }7}urlstringOptionalThe NAME of an environment variable that holds the full PostgreSQL connection string (e.g. "DATABASE_URL"). Nucleus reads process.env[url] at startup — it does not treat this value as the literal DSN. Before connecting it calls ensureDatabaseExists so a brand-new database is created automatically if absent.
Example: "DATABASE_URL" → postgres://user:pass@host:5432/db
type'postgres'OptionalThe database engine. PostgreSQL is the only supported and tested target — the entire schema-push, multi-schema and JSON column strategy assumes Postgres.
"postgres"Schema synchronization#
You never write CREATE TABLE by hand. You declare entities (see the Entities section); a generator turns them into a Drizzle schema file, and on every boot Nucleus pushes that schema to the chosen Postgres schema. The first element of schemas is the primary schema everything lands in.
schemasstring[]OptionalThe list of Postgres schemas Nucleus manages. schemas[0] is the primary (target) schema — all system and entity tables are created there, and it defaults to "main" when omitted. In multi-tenant mode this primary acts as the control-plane schema while each tenant receives its own.
Example: ["main"]
["main"]allowDataLossbooleanOptionalSafety switch for destructive schema changes. Left false, the startup push stays additive — it adds tables and columns but will not silently drop data. Set true only when you deliberately want the sync to apply destructive DDL (dropping removed columns/tables) to match your entities exactly.
falseMulti-tenancy#
Nucleus implements tenancy as full per-schema isolation: every tenant gets its own Postgres schema containing the complete set of tables (users, sessions, roles, your entities — everything). There is no shared users table and no row-level tenant_id smell. The primary schema doubles as the control plane that tracks all tenants.
isMultiTenantbooleanOptionalMaster switch for tenancy. When true, Nucleus initialises a TenantRegistry after the main schema syncs, provisions a schema per tenant (pushing the full table set into each), seeds claims and the godmin into every tenant, and exposes tenant-management routes. It also lights up the tenant endpoints in the generated client.
falsetenantResolution'subdomain' | 'header' | 'both'OptionalHow an incoming request is mapped to a tenant. subdomain inspects the host (acme.yourapp.com → tenant acme); header reads a request header; both tries the subdomain first and falls back to the header. Resolution results are cached in Redis (~5 min) to avoid a lookup on every request.
subdomain— Resolve purely from the host's leftmost label.header— Resolve from the tenant header only.both— Subdomain first, then header as a fallback (default).
"both"tenantHeaderstringOptionalThe header inspected when tenantResolution includes "header". Its value may be either a tenant ID or a subdomain — the registry tries both interpretations before giving up.
"x-tenant-id"sharedSchemastringOptionalOptional name of a schema holding data shared across every tenant (reference/catalog tables that should not be duplicated per tenant). Tenant schemas remain fully isolated for their own records; the shared schema is the deliberate exception.
Under the hood — the managers#
Two pieces do the real work behind this config: a connection-pool manager and, in multi-tenant mode, the TenantRegistry. You never instantiate them yourself, but knowing how they behave explains the config's guarantees.
PostgreSQLManagersingleton-per-config poolOptionalWraps a node-postgres Pool, keyed by host/port/database/user — two plugins pointing at the same database transparently share one pool. Its password may be a string OR an async () => Promise<string>, so AAD / rotating credentials are fetched (and refreshed) at connect time. execute<T>(sql, params) returns a { success, data } | { success:false, error } envelope rather than throwing. ensureDatabaseExists() runs first, creating the database if it is absent.
TenantRegistry.initialize()startupOptionalRegisters the main schema context, loads every row from the tenants and tenant_features tables, builds in-memory indices (by subdomain, schema name and id) and constructs a Drizzle schema context for each active tenant — each context is a pgSchema(name) carrying the complete table set produced by createAllTablesForSchema.
syncSchemaToDb() / syncAllSchemas()DDL pushOptionalSchema reconciliation dynamically imports drizzle-kit/api's pushSchema, scopes it to the target schema and calls push.apply(). At boot syncAllSchemas() ensures every schema exists and pushes the main plus every active tenant schema; a push failure is logged as a warning, not a fatal error.
provisionTenant()runtimeOptionalCreating a tenant runs ensureSchemaExists (CREATE SCHEMA) → buildSchemaContext → index → syncSchemaToDb, so a brand-new tenant gets the full isolated table set on the fly. Resolution results are cached in Redis under tenant:{subdomain}; refreshTenant()/invalidateCache() keep that cache and the in-memory indices coherent after edits.
Related sections