Configuration
Env vars, Postgres setup, and migrations.
The backend is configured entirely via environment variables. No CLI flags.
Required
| Env var | Purpose |
|---|---|
SITE | Site origin this backend serves (e.g. https://example.com). Stamped onto every row. |
DATABASE_URL | Postgres connection string (postgres://user:pass@host:5432/db). |
Optional
| Env var | Default | Purpose |
|---|---|---|
ORIGINS | SITE | Comma-separated CORS allowlist. Exact-match — no wildcards. |
PORT | 8080 | Listen port (1–65535). |
LOG_LEVEL | info | debug / info / warn / error. |
Postgres
The backend works against Postgres 14 or later. It does not create the database for you — provision an empty database and a user with CREATE / INSERT / SELECT privileges, then point DATABASE_URL at it. Managed services (AWS RDS, Neon, Supabase, Fly Postgres, Railway) all work.
Migrations
On startup the backend runs every .sql file in packages/backend/src/db/migrations/ against DATABASE_URL, in sorted order, inside a Postgres advisory lock (key 42) so concurrent instances don't race. Already-applied migrations are skipped via a schema_migrations table.
Safe restart pattern: bring up a new container, wait for GET /health to return {"status":"ok"}, then switch traffic. The lock ensures only one instance runs migrations at a time even if you're rolling multiple replicas.
Schema
CREATE TABLE consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
site TEXT NOT NULL,
consent_id TEXT NOT NULL,
consent JSONB NOT NULL,
ip TEXT,
user_agent TEXT,
country TEXT,
region TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_consents_site_created_at ON consents (site, created_at DESC);
CREATE INDEX idx_consents_consent_id ON consents (consent_id);Per-row fields:
consent_id— client-generated UUID the SDK sends in the POST body.consent— JSON-encoded consent map, e.g.{"necessary":true,"analytics":false}. From the client body.ip— fromX-Forwarded-For(first entry) or the raw socket address. Server-stamped.user_agent— from theUser-Agentheader. Server-stamped.country— from edge headers (cf-ipcountry,x-vercel-ip-country,x-amz-cf-ipcountry,cloudfront-viewer-country). Server-stamped.NULLif none present.region— fromcf-regionorx-vercel-ip-country-region. Server-stamped.NULLotherwise.created_at— server clock. Not trusted from the client.
Only consent_id and consent come from the request body. Everything else is stamped server-side — client-supplied values for those fields are ignored.
Latest state for a user
The table is append-only. To read the current consent for a given visitor:
SELECT consent, created_at
FROM consents
WHERE consent_id = $1
ORDER BY created_at DESC
LIMIT 1;Backups
Managed Postgres: use the provider's point-in-time recovery.
Self-hosted: pg_dump against DATABASE_URL on a schedule:
pg_dump "$DATABASE_URL" > backup-$(date +%F).sqlRestore with psql "$DATABASE_URL" < backup-YYYY-MM-DD.sql.