The role split previously required a one-time manual script run on
existing volumes (initdb.d only fires on fresh ones). This folds the
bootstrap into the compose file the way split-account deployments
standardly do it: a one-shot db-init service runs z_rls_role.sh
(idempotent, network mode via PGHOST=db + PGPASSWORD from DBPASS) on
every 'compose up', and the app gates on
db-init: condition: service_completed_successfully.
- Zero manual steps for fresh AND existing deployments: update compose,
add DBSRVPASS to .env, 'compose pull && up -d'.
- Security posture improves: the superuser password is now held ONLY by
the init service — the app container connects exclusively as the
non-superuser runtime role.
- z_rls_role.sh gains dual connection modes (local socket for docker
exec, network for the init service) with the DBPASS requirement
enforced only in network mode.
- Verified on the dev stack: init runs and exits clean, app gates on it
and comes up healthy, second 'up' re-converges idempotently, invariant
test passes through the new dependency chain.
The initdb.d mount stays as-is (harmless double coverage on fresh
volumes; both paths are idempotent).
The app connected as 'postgres' — the bootstrap SUPERUSER — which
bypasses row-level security unconditionally (FORCE RLS does not apply
to superusers), making any future policy a silent no-op. This is the
prerequisite commit for the RLS visibility work (plan
~/.opencode/plan/server-rls-library-visibility.md REV 2, §4.0).
What changed:
- database/schema/z_rls_role.sh: creates the bookhoard_srv role
(LOGIN, non-superuser, non-bypassrls) and transfers ownership of
every public table/sequence/function + schema + database to it.
Ownership (not mere grants) because executeSchema re-runs the whole
schema.sql on EVERY boot and needs owner DDL. Idempotent, catalog-
only — no user data is read or written.
- docker-compose.yml app service + docker-compose.dev.yml tests
service: DATABASE_USER postgres -> bookhoard_srv, DATABASE_PASSWORD
DBPASS -> DBSRVPASS (new .env key, generated locally).
- POSTGRES_USER: postgres stays as the container bootstrap account;
on FRESH volumes z_rls_role.sh runs automatically via
docker-entrypoint-initdb.d AFTER schema.sql (alphabetical: z > s).
Verified on the dev stack: role has rolsuper=f, rolbypassrls=f, owns
all 40 tables and every public function; single boot as bookhoard_srv
executed schema.sql cleanly (CREATE EXTENSION IF NOT EXISTS pg_trgm
skips fine as non-superuser when the extension exists); health +
authenticated smoke pass. Behavior otherwise unchanged — no policies
yet.