infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- app_roles_tracksolid_db.sql — get the tracksolid_db apps off the postgres SUPERUSER.
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- DESIGN (validated against the live DB, 2026-06-20):
|
|
|
|
|
-- * webhook_receiver, ingest_worker, worker each run `run_migrations.py` (DDL) and
|
|
|
|
|
-- write telemetry. `worker` is a second copy of the ingest_worker image. Because
|
|
|
|
|
-- they run migrations, they need to OWN the objects they ALTER. They therefore
|
|
|
|
|
-- connect as the shared, NON-SUPERUSER **tracksolid_owner** (the role the repo
|
|
|
|
|
-- already intends to own these schemas — see analytics_ro_role.sql default privs).
|
|
|
|
|
-- * the prod dashboard_api backend only reads → its own read role `dashboard_app`
|
|
|
|
|
-- (or reuse the existing dashboard_ro).
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
--
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- This file is idempotent. Section 2 (ownership reassignment) is Timescale-aware:
|
|
|
|
|
-- it skips table-linked sequences, uses ALTER MATERIALIZED VIEW for continuous
|
|
|
|
|
-- aggregates, and leaves reporting.v_trips with reporting_refresher. Reassigning
|
|
|
|
|
-- while the apps still run as postgres is SAFE — superuser bypasses ownership, so
|
|
|
|
|
-- nothing breaks until you flip each app's DATABASE_URL (see the runbook).
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
--
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- Run as the postgres SUPERUSER, on tracksolid_db:
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
-- docker exec -i <timescale_db> psql -U postgres -d tracksolid_db -v ON_ERROR_STOP=1 \
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- -v owner_pw="$(cat ~/.tracksolid_owner.pw)" \
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
-- -v dash_pw="$(cat ~/.dashboard_app.pw)" \
|
|
|
|
|
-- < scripts/app_roles_tracksolid_db.sql
|
|
|
|
|
|
|
|
|
|
\set ON_ERROR_STOP on
|
|
|
|
|
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- ── 1. tracksolid_owner: the shared owner/migrator login for the ingestion apps ──
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
DO $$ BEGIN
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='tracksolid_owner') THEN
|
|
|
|
|
CREATE ROLE tracksolid_owner LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
END IF; END $$;
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- LOGIN + password + a HARD connection cap (the real budget control). No
|
|
|
|
|
-- statement_timeout: migrations (e.g. CREATE INDEX on a hypertable) can run long.
|
|
|
|
|
ALTER ROLE tracksolid_owner WITH LOGIN PASSWORD :'owner_pw' CONNECTION LIMIT 30;
|
|
|
|
|
ALTER ROLE tracksolid_owner SET idle_in_transaction_session_timeout = '5min';
|
|
|
|
|
ALTER ROLE tracksolid_owner SET idle_session_timeout = '10min';
|
|
|
|
|
ALTER ROLE tracksolid_owner SET lock_timeout = '10s';
|
|
|
|
|
GRANT CONNECT ON DATABASE tracksolid_db TO tracksolid_owner;
|
|
|
|
|
GRANT USAGE, CREATE ON SCHEMA tracksolid, reporting, tickets, fuel TO tracksolid_owner;
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- ── 2. Reassign the app objects to tracksolid_owner (Timescale-aware, idempotent) ─
|
|
|
|
|
DO $reassign$
|
|
|
|
|
DECLARE r record; k text;
|
|
|
|
|
BEGIN
|
|
|
|
|
FOR r IN
|
|
|
|
|
SELECT n.nspname, c.relname, c.relkind,
|
|
|
|
|
EXISTS (SELECT 1 FROM timescaledb_information.continuous_aggregates ca
|
|
|
|
|
WHERE ca.view_schema=n.nspname AND ca.view_name=c.relname) AS is_cagg
|
|
|
|
|
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
|
|
|
WHERE n.nspname IN ('tracksolid','reporting','tickets','fuel')
|
|
|
|
|
AND c.relkind IN ('r','p','v','m','S')
|
|
|
|
|
AND pg_get_userbyid(c.relowner) <> 'tracksolid_owner' -- idempotent
|
|
|
|
|
AND NOT (n.nspname='reporting' AND c.relname='v_trips') -- keep with refresher
|
|
|
|
|
AND NOT (c.relkind='S' AND EXISTS ( -- skip linked seqs
|
|
|
|
|
SELECT 1 FROM pg_depend d WHERE d.objid=c.oid AND d.deptype IN ('a','i')))
|
|
|
|
|
LOOP
|
|
|
|
|
k := CASE WHEN r.is_cagg OR r.relkind='m' THEN 'MATERIALIZED VIEW'
|
|
|
|
|
WHEN r.relkind='v' THEN 'VIEW' WHEN r.relkind='S' THEN 'SEQUENCE' ELSE 'TABLE' END;
|
|
|
|
|
EXECUTE format('ALTER %s %I.%I OWNER TO tracksolid_owner', k, r.nspname, r.relname);
|
|
|
|
|
END LOOP;
|
|
|
|
|
END $reassign$;
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
DO $fns$
|
|
|
|
|
DECLARE r record;
|
|
|
|
|
BEGIN
|
|
|
|
|
FOR r IN SELECT p.oid::regprocedure AS sig
|
|
|
|
|
FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
|
|
|
|
|
WHERE n.nspname IN ('tracksolid','reporting','tickets','fuel')
|
|
|
|
|
AND pg_get_userbyid(p.proowner) <> 'tracksolid_owner'
|
|
|
|
|
LOOP EXECUTE format('ALTER FUNCTION %s OWNER TO tracksolid_owner', r.sig); END LOOP;
|
|
|
|
|
END $fns$;
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- ── 3. dashboard_app: read-only role for the prod dashboard_api backend ──────────
|
|
|
|
|
-- (If that backend turns out to also WRITE app state, widen via a write group like
|
|
|
|
|
-- the fleet_platform file; start read-only.)
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
DO $$ BEGIN
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dashboard_app') THEN
|
|
|
|
|
CREATE ROLE dashboard_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
|
|
|
|
END IF; END $$;
|
|
|
|
|
ALTER ROLE dashboard_app WITH LOGIN PASSWORD :'dash_pw' CONNECTION LIMIT 8;
|
|
|
|
|
GRANT CONNECT ON DATABASE tracksolid_db TO dashboard_app;
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
GRANT USAGE ON SCHEMA tracksolid, reporting, tickets, fuel TO dashboard_app;
|
|
|
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA tracksolid, reporting, tickets, fuel TO dashboard_app;
|
|
|
|
|
GRANT SELECT ON reporting.v_trips TO dashboard_app;
|
|
|
|
|
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA reporting TO dashboard_app;
|
|
|
|
|
ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA tracksolid, reporting, tickets, fuel
|
|
|
|
|
GRANT SELECT ON TABLES TO dashboard_app; -- future objects (now owned by tracksolid_owner)
|
infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.
Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
object-ownership step for migration-running apps (reassign app schemas to the
existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
cutover, and instant rollback (DATABASE_URL only).
Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:51:52 +00:00
|
|
|
ALTER ROLE dashboard_app SET statement_timeout = '30s';
|
|
|
|
|
ALTER ROLE dashboard_app SET idle_in_transaction_session_timeout = '60s';
|
|
|
|
|
ALTER ROLE dashboard_app SET idle_session_timeout = '5min';
|
|
|
|
|
ALTER ROLE dashboard_app SET lock_timeout = '5s';
|
|
|
|
|
|
infra(db-roles): validated Option A — shared tracksolid_owner for migrators
Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker
all run run_migrations.py (DDL) and write telemetry — worker is the same image as
ingest_worker, not a reader. Because they ALTER objects they must own them, so all
three connect as the shared non-superuser tracksolid_owner (the role the repo already
intends to own these schemas). dashboard_api backend stays a reader (dashboard_app).
- app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30
+ GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked
sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips
with reporting_refresher, reassigns functions); dashboard_app read role.
- Reassignment validated in a rolled-back transaction on the live DB: reassigns the
31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate,
and as tracksolid_owner can ALTER the hypertable and create/drop tables.
- Runbook updated: discovery marked done, ownership folded into the apply (safe while
apps still run as postgres — superuser bypasses ownership), corrected cutover order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:08:52 +00:00
|
|
|
-- ── 4. Verify ────────────────────────────────────────────────────────────────────
|
|
|
|
|
-- \du+ tracksolid_owner -- LOGIN + CONNECTION LIMIT 30
|
|
|
|
|
-- SELECT pg_get_userbyid(relowner), count(*) FROM pg_class
|
|
|
|
|
-- WHERE relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname='tracksolid') GROUP BY 1;
|