Design — Per-Consumer Postgres Roles for Supabase
Issue: #2735 · Status: Design (plan only) · Batch: K (#3338)
Eliminate SUPABASE_SERVICE_ROLE_KEY duplication by giving each consumer its own least-privilege
Postgres role instead of sharing the god-mode service key.
1. Problem
SUPABASE_SERVICE_ROLE_KEY is duplicated across 5 SOPS files: et-operational-platform,
brandpilot, n8n-bms4, whatsup, vps-h1. It bypasses RLS entirely. Consequences:
- One key compromise forces atomic rotation across all 5 files + every live env + Vercel + containers — a high-blast-radius, error-prone operation.
- No least privilege: a workflow that only needs
p24_queuecan read/write every table. - No per-consumer audit trail — all traffic is one role.
2. Target design — least-privilege roles
Each consumer connects via Postgres with a dedicated role and a scoped grant. Illustrative (audit in Phase 1 sets the real table list):
CREATE ROLE svc_n8n_queue LOGIN PASSWORD :'pw';
GRANT SELECT, INSERT, UPDATE ON p24_queue TO svc_n8n_queue;
CREATE ROLE svc_etop_backend LOGIN PASSWORD :'pw';
GRANT SELECT, INSERT ON registries, reg_records TO svc_etop_backend;Each service holds only its role’s connection string (a scoped credential), not the service key.
3. Phased plan
| Phase | Work | Risk |
|---|---|---|
| 1. Audit | For each of the 5 consumers, capture the exact tables + operations used (query pg_stat_statements, grep service code for table names). Output: a consumer→tables matrix. | none (read-only) |
| 2. Design roles | Define role hierarchy + grants from the Phase 1 matrix. Prefer table-level grants; use RLS policies where row scoping is needed. | none (paper) |
| 3. Create roles | Migration NNNN_per_consumer_roles.sql creating roles + grants (no consumer switched yet). | low — additive |
| 4. Cut over | Update each service to use its Postgres connection string instead of the REST + service key. One consumer at a time, verify, then next. | medium — per-service |
| 5. Remove key | Drop SUPABASE_SERVICE_ROLE_KEY from consumer SOPS files once no consumer uses it. Keep it only where genuinely needed (e.g. admin/migrations). | medium — irreversible per file |
4. Consumers in scope
| Consumer | SOPS file | Likely scope (confirm in Phase 1) |
|---|---|---|
| n8n (bms-4 + Cloud) | secrets/n8n-bms4.env.sops | p24_queue, dispatch tables |
| et-operational-platform | secrets/et-operational-platform.env.sops | registries, reg_records |
| brandpilot | secrets/brandpilot.env.sops | brandpilot tables |
| whatsup / p24-wa-a | secrets/whatsup.env.sops | wa message/session tables |
| vps-h1 services | secrets/vps-h1.env.sops | confirm — may be removable |
5. Risks & constraints
- Connection method change (REST → direct Postgres) is the biggest per-service risk — connection pooling (pgBouncer/Supavisor), SSL, and prepared-statement mode must be validated per consumer.
- PostgREST-dependent consumers may still need a JWT/anon path for RLS-scoped reads; direct Postgres is for service-to-service only.
- All SOPS edits (Phases 4–5) are secret-manager work — out of scope for this dev-issue.
- Migrations via
mcp__claude_ai_Supabase__apply_migration; timestamp prefixdate +%Y%m%d%H%M%S.
6. Out of scope for this doc
No role, grant, migration, or SOPS change is made by this issue. Deliverable is the phased plan + consumer inventory template.
Design-only deliverable. Related: docs/plans/plan-2427-rotate-secret-system.md, docs/policies/ credential-isolation, docs/supabase-operations.md.