Database
PostgreSQL staging service, migrations, seeds, and tenant-domain schema.
Database
ByteBite uses PostgreSQL as the sole primary database, accessed through Drizzle ORM in packages/db.
Staging PostgreSQL
| Attribute | Value |
|---|---|
| Engine | PostgreSQL 18 |
| Management | Dokploy PostgreSQL service |
| Image | postgres:18 |
| Display name (observed) | bytebite-staging-postgres |
| Generated hostname (example) | bytebite-staging-postgres-vmg94p |
| Database name | bytebite |
| User | bytebite |
| Port | 5432 (internal Docker network only) |
| Public port 5432 | CLOSED (verified 2026-09-08) |
Generated hostnames are Dokploy-runtime identifiers — not permanent.
Connection
The API receives DATABASE_URL through Dokploy environment configuration. The connection string contains credentials and must never appear in documentation or Git.
Local development uses a separate PostgreSQL instance (postgres:17-alpine in compose.yml, published on 127.0.0.1:5432).
Drizzle and migrations
| Component | Location |
|---|---|
| Schema | packages/db/src/schema/ |
| SQL migrations | packages/db/migrations/ |
| Migration runner | packages/db/src/migrate.ts → bundled as db/dist/migrate.js |
| Drizzle config | packages/db/drizzle.config.ts |
Migrations are explicit — the API does not auto-migrate on startup.
Running migrations (staging)
Inside the api container:
node db/dist/migrate.jsFrom a machine with staging DATABASE_URL:
pnpm deploy:db:migrateRun after each deploy that includes schema changes.
Staging seeds
Staging tenant seeds are separate from local dev seeds:
| Command | Hostnames | When |
|---|---|---|
node db/dist/seed-staging.js | luigi.staging.getbytebite.co, mario.staging.getbytebite.co | Fresh staging DB, once |
pnpm db:seed (local only) | luigi.bytebite.test, mario.bytebite.test | Local dev |
Never run seed-staging.js against production. Never run it against the local dev database (it overwrites domain hostnames for shared row IDs).
Implementation: packages/db/seeds/staging.ts.
Infrastructure-relevant schema
restaurants
Stores restaurant identity, status (ACTIVE / INACTIVE), and branding.
restaurant_domains
Maps normalized hostnames to restaurants. Tenant resolution is an exact match on hostname after normalization.
| Column | Infrastructure relevance |
|---|---|
hostname | Unique, normalized — the tenant lookup key |
type | BYTEBITE_SUBDOMAIN or CUSTOM_DOMAIN |
is_primary | At most one primary per restaurant |
verified_at | Required for custom domains (future) |
DNS wildcard existence does not create rows here. Unknown hostnames fail tenant lookup.
Failure expectations
| Condition | API behavior |
|---|---|
| Unknown/inactive tenant | HTTP 404 (Restaurant not found) on tenant-aware routes |
| Database unreachable | HTTP 503 on tenant-aware routes |
/health | Always available (no tenant lookup) |
/health/db | Returns degraded if DB unreachable |
Backups
Off-host PostgreSQL backup and restore strategy is not established. Local Traefik configuration snapshots exist on the APP VM but do not cover database data. See Backups and Known gaps.
Related
- Tenancy & storefront — full tenant model
- ADR-002 Primary Database
- Staging deployment — migration and seed runbook
- Disaster recovery — PostgreSQL failure runbook