PostgreSQL operations
Lumaft on PostgreSQL is customer-operated: you run, size, patch, back up, and make the server available; Lumaft owns only its schema and migrations. This page is the operating contract for that split.
Two things are gated by license, and only two: beginning an adoption from SQLite or a fresh installation on PostgreSQL requires Business, and serving from more than one replica requires Enterprise. Continuing to run an adopted database never depends on the license.
When to choose PostgreSQL
| Situation | PostgreSQL? |
|---|---|
| Compute that cannot keep an exclusive block volume (Fargate, Container Apps, scale-to-zero) | Required |
| You want zero downtime across restarts and rollouts | Yes, with Enterprise replicas |
| SQLite read p95 or cold readiness repeatedly exceeds the measured envelope at your sizing | Consider it, and measure — a provider switch alone does not guarantee more throughput |
| One replica on a VM with an EBS volume, tens of thousands of operations | SQLite is complete and supported; stay |
Requirements
- A PostgreSQL server on a currently supported major version, reachable from every Lumaft replica. Managed services (Amazon RDS, Aurora PostgreSQL, Azure Database for PostgreSQL, Cloud SQL) are fine for everything except the transcript capacity measurement described below.
- A dedicated database (or schema) and a role that owns it, so Lumaft can create and alter its own tables. It needs no superuser, replication, or server-filesystem privileges.
- TLS.
sslmode=requireat minimum;sslmode=verify-fullwith your CA bundle where you can supply it. - A connection budget that the formula below fits inside.
CREATE ROLE lumaft LOGIN PASSWORD '<password>';
CREATE DATABASE lumaft OWNER lumaft;
The connection URL:
postgresql://lumaft:<password>@db.internal.example:5432/lumaft?sslmode=verify-full
Put it in a protected file and name the file with LUMAFT_POSTGRES_URL_FILE. Connection values
never appear in logs, health responses, or the console.
Starting on PostgreSQL
Fresh installation
Set exactly one of LUMAFT_POSTGRES_URL or LUMAFT_POSTGRES_URL_FILE and nothing else that
selects a database. Do not also set LUMAFT_SQLITE_PATH. Conflicting sources refuse startup.
Lumaft creates its schema on first start and migrates it forward on every later one.
Adopting from SQLite
Adoption is a console workflow on Administration → Database that transfers and verifies the durable record, then makes PostgreSQL authoritative:
- Take and verify a cold SQLite backup and keep it; a SQLite file is not a PostgreSQL backup.
- Enter the PostgreSQL connection string in the write-only form and confirm your username and current password.
- Review the preflight.
- Confirm. Lumaft pauses writes, transfers the data, verifies it, and saves the provider configuration atomically in the data directory.
- Restart. PostgreSQL is now authoritative.
Guards you can rely on:
- A failed or cancelled adoption leaves SQLite authoritative, never activates the provider file, and removes temporary target credentials.
- Starting an image with a PostgreSQL variable against a non-empty SQLite installation that has no completed-transfer stamp refuses. Changing a variable is not a migration.
- The UI-managed provider file is mode
0600and contains credentials; treat the data directory as you treat a backup. Environment-managed PostgreSQL cannot be changed through the console.
Connections
Each replica opens a pool of LUMAFT_POSTGRES_POOL_MAX connections plus two dedicated
sessions: one that holds job leases through advisory locks, and one that LISTENs for change
notifications. At startup Lumaft reads max_connections and the reserved-connection settings
and refuses with pool-topology-exceeds-server unless:
LUMAFT_REPLICAS_MAX × (LUMAFT_POSTGRES_POOL_MAX + 2) + LUMAFT_POSTGRES_CONNECTION_HEADROOM
≤ max_connections − reserved connections
| Setting | Default | Meaning |
|---|---|---|
LUMAFT_REPLICAS_MAX |
1 |
Honest ceiling, including rollout surge |
LUMAFT_POSTGRES_POOL_MAX |
1 |
Pooled connections per replica |
LUMAFT_POSTGRES_CONNECTION_HEADROOM |
10 |
Connections reserved for your own tools, backups, and monitoring |
Example: three replicas with a pool of two and ten headroom need 3 × 4 + 10 = 22 non-reserved
connections. A managed instance whose max_connections is derived from memory may need a larger
class or a parameter-group change; check the number before you scale.
Replicas (Enterprise)
All replicas must share the same release, database, backend definitions, authentication, and licensing settings. A replica started from different configuration is a different installation sharing a database, which nothing makes safe.
How the topology behaves:
- Every durable thing is a row, read per request. Replicas cache nothing across requests.
Change notifications flow through PostgreSQL
LISTEN/NOTIFY, with a fifteen-second poll fallback if the listener drops. - Periodic jobs run on exactly one replica at a time — observation sweeps, retention expiry, purge, forget, notification dispatch, license renewal, schema migration — each under a session-level advisory lock with a fencing token. Loss of the lock session is detected within ten seconds and a job never writes under a lost grant.
- One replica is installation-primary and serves at any license level. Every other replica
serves only while the Enterprise entitlement verifies. On lapse a standby stays alive, holds
no job, returns
503readiness, and keeps contending to become primary. A lapse degrades to one serving replica, never to zero. - Application transactions serialize installation-wide through a deployment-row lock with a five-second contention bound. Ordinary reads and object-store work stay concurrent. More replicas give coordination and failover, not proportional write throughput.
- Sign-in throttling is per replica. At N replicas the combined attempt budget is N times one replica's. Rate-limit sign-in at your ingress.
- Diagnostic reports are per replica, stamped with the replica identity. Never read one as an installation total.
Operational rules that follow:
| Rule | Why |
|---|---|
| Route traffic on readiness; restart on liveness | A standby is alive and not ready, by design |
| While lapsed, scale to one replica | A rolling update waiting on standby readiness cannot complete |
Rolling deploys only when every pending migration is startup-safe |
Gated migrations need one replica and the explicit apply |
| After a point-in-time restore, restart every replica | Replicas re-read the schema lineage and re-elect job ownership |
Keep LUMAFT_REPLICAS_MAX honest |
It sizes both the connection check and the sign-in budget |
Exactly one replica migrates, under the schema-migration lease; the others wait up to
LUMAFT_MIGRATION_WAIT_SECONDS (default 300) and then serve at the same revision or refuse. On
PostgreSQL the recovery point for a gated migration is your backup — Lumaft copies nothing.
Maintenance you own
- Vacuum and bloat. Retention expiry, purge, and forget delete rows; PostgreSQL reclaims the space through vacuum. Leave autovacuum on and watch table bloat on the evidence tables after large purges. Lumaft makes no claim that deleting rows returns disk to the host.
- Disk. Lumaft's
/api/v1/storage-healthmeasures the application's own data directory, not the database server's disk. Monitor the server's storage with the server's tools. - Connections. Alert when used connections approach
max_connections − headroom. - Upgrades of PostgreSQL itself. Stop Lumaft, upgrade the server, start Lumaft. Lumaft needs nothing from a major-version upgrade beyond a reachable database.
- Role and password rotation. Update the URL file on every replica and restart them.
Backups and recovery
Use your server's tooling. A physical base backup with WAL archiving gives point-in-time
recovery; pg_dump of the Lumaft database gives a logical copy. Take logical dumps while Lumaft
is stopped, or accept that in-flight writes may straddle the dump.
Two things live in the database that make its backup a credential:
- Connected-licensing state — the instance identity, credential revision, and signed lease.
- Every account's credential hash and every integration token digest.
Protect backups accordingly, and never restore a copy into a second live installation and run both: connected licensing binds one agreement to one installation identity, and two installations sharing it are indistinguishable clones.
After any restore, restart every replica. Restoring rewinds Lumaft's record to the backup point; current projections converge forward from live state, and durable history earlier than the backup is whatever the backup held. See Backup and disaster recovery.
Transcript and state capture on PostgreSQL
Optional transcript capture and whole-state retention need a fresh measurement of the PostgreSQL server's actual storage before each admission. SQLite measures its own filesystem; PostgreSQL cannot be measured from inside the application container, and Lumaft refuses to substitute a guess.
Lumaft supplies a producer script that runs on the PostgreSQL host (or in its mount
namespace) with psql and Node.js, uses read-only SQL and statfs, and atomically writes a
report file every five seconds. Mount that report read-only into every replica and set:
LUMAFT_TRANSCRIPT_POSTGRES_CAPACITY_FILE=/run/lumaft-capacity/capacity.json
Requirements and limits:
- The database role must be able to execute
pg_catalog.pg_control_system(); stock PostgreSQL grants this toPUBLIC. The producer role additionally needspg_read_all_settingsto readdata_directory, plusUSAGEon the schema andSELECTontranscript_postgres_capacity. - Supported layout: the default tablespace with data and WAL on one filesystem. Separate WAL devices, external tablespaces, and standbys refuse collection.
- A report expires fifteen seconds after its database-clock read. Missing, stale, malformed, writable, or mismatched reports keep capture closed — operation evidence is unaffected.
- Managed providers are not covered. Without host filesystem access there is nothing to measure, so transcript and state capture stay closed on RDS, Aurora, Azure Flexible Server, and Cloud SQL. Everything else on this page works there.
Capture preserves the greater of 2 GiB or 10 % of measured space, plus a further 1 GiB or 5 % margin, and reserves capacity in the same transaction as the evidence it admits. A healthy status is not a reservation; every admission re-checks.
Startup refusals specific to PostgreSQL
| Log line contains | Meaning | Fix |
|---|---|---|
pool-topology-exceeds-server |
The connection formula does not fit the server | Lower the ceiling or pool, or raise max_connections |
multi-replica-requires-postgresql |
LUMAFT_REPLICAS_MAX above 1 on SQLite |
Adopt PostgreSQL first |
| Conflicting database sources | Both SQLite and PostgreSQL selected, or both URL forms | Set exactly one |
Readiness stays 503, process alive |
The database cannot be queried: network, TLS mode, credentials, or a standby awaiting entitlement | Check the URL file, reachability, and the license state |
schema-newer-than-binary |
A newer release migrated the database | Start the newer image, or restore |
The general startup table is in Database integration.