PostgreSQL
The server supports two datastores and a deployment uses exactly one. MongoDB is selected by
MONGODB_URI; PostgreSQL is selected by POSTGRES_URL. Everything above the storage layer is
identical: the same endpoints, the same admin console, the same agent surface. In practice the
choice usually comes down to which database your organisation already runs well.
Setting it up
Section titled “Setting it up”-
Create a database and a role that can create tables in it.
CREATEis needed at provisioning time and also while the server runs: creating a user bucket in the console provisions that bucket’s table and its unique-email constraint at that moment, so a bucket added later is constrained exactly like one that came with the schema. -
Set the connection string. The database name is part of the URL, so
DATABASE_NAMEis not used:Terminal window POSTGRES_URL=postgres://user:password@host:5432/oauthISSUER=https://auth.example.com -
Provision the schema, before the first start:
Terminal window bun run db:setup:pgThis creates every table and index the release declares, seeds the reserved administrator records, and provisions an initial RS256 signing key. It is idempotent: running it again reports no work. It is also how a newly declared table reaches an existing database after an upgrade.
-
Start the server. It refuses to start if the database is unreachable or if its schema is not the one this release expects, instead of accepting a request and discovering that afterwards.
The shortest version
Section titled “The shortest version”A Compose file ships for exactly this, and it is probably the fastest way to see the server on PostgreSQL:
docker compose -f docker-compose.postgres.yml upIt runs three services: the database, a one-shot db:setup:pg that must finish first, and the
server. The host port is 5433, so it does not collide with a PostgreSQL you already run.
docker-compose.yml beside it is the MongoDB equivalent. Run one or the other, never both.
Checking a database without changing it
Section titled “Checking a database without changing it”bun run db:setup:pg --checkReports every table or index that is missing and changes nothing. Useful in a deployment pipeline before a release, and after one if something looks wrong.
Upgrading
Section titled “Upgrading”Two steps, in this order, on every upgrade:
bun run db:setup:pg # adds anything newly declaredbun run db:migrate # applies any schema migrations the release declaresdb:migrate reports current and exits when there is nothing to do, which is the ordinary case.
--plan lists what would run without running it, and exits non-zero when work is outstanding, so a
pipeline can gate on it.
What differs from MongoDB
Section titled “What differs from MongoDB”Nothing an application or an end user can observe. Two operational differences are worth knowing:
- The server reclaims expired records itself. PostgreSQL has no TTL index, so the server sweeps them on roughly the same interval MongoDB’s monitor uses. Expiry itself is enforced on every read regardless, on both backends; the sweep only reclaims space.
DATABASE_NAMEis unused. The database name is in the connection URL.
Health and readiness
Section titled “Health and readiness”Two probes, and they answer different questions:
| Endpoint | Question | On failure |
|---|---|---|
/health |
Is this process alive? | Restart it. |
/ready |
Can it serve? | Stop routing to it; do not restart. |
/ready reports failure when the datastore is unreachable and recovers on its own when it returns.
Point a liveness probe at the first and a readiness probe at the second. Wiring readiness to
/health makes an outage invisible — the process is alive, so the probe passes while requests keep
arriving at an instance that cannot serve them; wiring liveness to /ready restarts a healthy
process for its dependency’s outage.