◆ ForgeVis
Skip to content

Shared database ​

One PostgreSQL database sits between the platform and the recorders. The backend owns it; the recorders read two tables and write one.

Knowing which side owns what is not trivia — it decides the order things have to start in, and getting that order wrong fails quietly.

Who touches what ​

TableBackendRecorder
camerasfull ownershipreads only
recordingsowns the schemainserts and reads segments
everything elsefull ownershipnever touched

The recorder issues no DDL at all — no table, no function, no trigger. Its entire database surface is a handful of queries against those two tables, so SELECT on cameras and INSERT/SELECT on recordings is the whole grant it needs.

The order matters ​

Migrations first, recorders second. Not a recommendation — a requirement, and one that fails without saying so.

A recorder that cannot find the cameras table does not stop. It logs a warning, then an error, and carries on starting. The result answers health checks, records nothing, and never picks up cameras later — because re-reading depends on a notification trigger that also failed to install. Everything looks alive.

WARNING

When cameras stop appearing on a node that reports itself healthy, check the log from startup before you check anything else.

Notification trigger ​

Changes to cameras reach a running recorder through LISTEN/NOTIFY on the cameras_update channel. The trigger that emits them is part of the schema and arrives with the migrations, like everything else here. The recorder does not create it, and does not check for it — applying the schema is what puts it there.

A camera changed in the database and the node ignored it

This is what a missing trigger looks like, and it is the only thing that looks like it. The node is otherwise healthy: it keeps recording the cameras it already had, answers its API, and reports nothing wrong — because nothing went wrong on its side. The notification it waits for was never sent.

sql
SELECT tgname FROM pg_trigger WHERE tgname = 'cameras_change';

Nothing returned means the schema was applied without it, or something has dropped it since. Re-apply the migrations, or the statements from dump_schema.py below.

Restarting the node changes nothing — it never created the trigger and will not now.

Two other things produce the same symptom and are worth ruling out first: database.watch set to false on the node, which turns the listener off entirely, and a node that failed to start against the database at all — check its log from startup.

A database without a backend ​

A recorder can use a database with no platform behind it — one node, its own PostgreSQL, cameras managed in the table directly. There is no Alembic there to run, so each release also publishes the same schema as plain SQL:

bash
python scripts/dump_schema.py                     # the whole schema, from empty
python scripts/dump_schema.py --from <revision>   # only what is missing since then
psql "$DSN" -v ON_ERROR_STOP=1 -f schema.sql

Rendered from the migrations rather than written beside them, so there is no second description to drift. It ends by stamping alembic_version, so a database created this way is one that a backend can pick up and carry forward later, should one arrive.

Cameras and clusters ​

A camera row has no cluster column. A recorder in cluster mode selects every row where enabled = true, and consensus decides which node takes which camera.

This works cleanly for one cluster per database. Two clusters sharing a database would both record every camera — neither is wrong, they simply cannot see each other's claim. Until a camera can name its cluster, give each cluster its own database.

Credentials ​

The recorder connects with its own DSN, configured on the node:

yaml
database:
  enabled: true
  url: postgres://user:password@host:5432/dbname
  watch: true

watch: true turns on the notification listener. Without it a camera change is only picked up on restart.

Cluster mode requires a database — a node with cluster.enabled: true and no database refuses to start, because camera placement is decided from the camera list.

Proprietary software.