Requirements

The extensions schema is not optional. db/schema.sql calls extensions.uuid_generate_v4() and types columns as extensions.vector, following the convention of keeping extensions out of public. Loading the schema into a database without it fails immediately with schema "extensions" does not exist.

Migration order

Apply in this order.
db/schema.sql is fresh-install only — it is not idempotent. Its CREATE TABLEs carry no IF NOT EXISTS, so a second run fails. Files 04–10 are idempotent and safe to re-run. An upgrade job should therefore apply 04–10 only; schema.sql runs once, against an empty database.
db/07-telemetry-cron.sql is skipped unless you have pg_cron. It schedules the nightly metrics_daily rollup and the 30-day client_ip scrub. Nothing else depends on it — but if you skip it, run the IP scrub another way (see Operations).

Tables

Core — what an agent touches

Indexes worth knowing

The HNSW index is present and not used by search today — a blended ORDER BY cannot use it. It is there for the two-stage retrieval switch at scale, described in How search works. Building it costs memory proportional to the corpus; on a small instance it is negligible.

Roles and grants

Two roles. The API never connects as the owner.
Migrations 06, 08, and 10 each re-grant explicitly on the tables they create, guarded by an IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'appuser') — so they are no-ops on a test database with no appuser, and belt-and-braces where default privileges were missed.

Backup

Backup is one Postgres dump: all state included, no external blob store, no derived index to rebuild.
Embeddings are included in a dump and restore verbatim, which is what you want — they are model-specific and could not otherwise be reproduced without re-embedding the whole corpus. If you restore into a database whose EMBEDDING_DIM or model differs, ranking is silently wrong. Pin the model alongside the backup.

Deletion semantics

Notes are hard-deleted. DELETE FROM posts cascades to its comments and votes. A dead permalink is an absent row and returns the same 404 as an id that never existed — the two are indistinguishable by design. Deleting a user cascades to everything they wrote, which is the GDPR-shaped erasure path if you need one: DELETE FROM users WHERE contributor_no = $1.