// Package database constructs the application's Postgres connection pool. // // Two things are wired here that the rest of the app relies on (see docs/STACK_DECISIONS.md §2/§3/§5): // // 1. pgvector codecs are registered on EVERY physical connection via AfterConnect, so vector // columns bind and scan as pgvector.Vector. Registering once on a single conn is a bug under // pool churn, it must be AfterConnect. // 2. The pool is expected to authenticate as the NOBYPASSRLS application role (never the migrator), // so Postgres Row-Level Security is always in force. Which role is used is a property of the // DSN, not of this code. package database import ( "context" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" pgxvec "github.com/pgvector/pgvector-go/pgx" ) // NewPool parses dsn, registers the pgvector codecs per connection, and returns a ready pool. // The caller owns the returned pool's Close(). dsn MUST point at the NOBYPASSRLS application role. func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, fmt.Errorf("parse pool config: %w", err) } // AfterConnect runs once per physical connection the pool opens, the only correct place to // register vector/halfvec/sparsevec so pgvector.Vector works as a bind param and Scan target. cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { if err := pgxvec.RegisterTypes(ctx, conn); err != nil { return fmt.Errorf("register pgvector types: %w", err) } return nil } pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, fmt.Errorf("create pool: %w", err) } return pool, nil } // AssertRole fails closed unless the pool authenticates as wantUser with rolbypassrls=false. It is // the runtime guardrail behind the whole RLS and EEO isolation story: if an operator pastes a // BYPASSRLS or superuser DSN (for example the migrator DSN) into the app or compliance variable, RLS // would be silently bypassed and every tenant's rows, including protected demographic attributes, // would leak cross org. This converts that misconfiguration into a hard boot failure. func AssertRole(ctx context.Context, pool *pgxpool.Pool, wantUser string) error { var user string var bypass bool err := pool.QueryRow(ctx, `SELECT current_user, rolbypassrls FROM pg_roles WHERE rolname = current_user`).Scan(&user, &bypass) if err != nil { return fmt.Errorf("role assertion query: %w", err) } if user != wantUser { return fmt.Errorf("pool authenticates as %q, expected %q", user, wantUser) } if bypass { return fmt.Errorf("pool role %q has BYPASSRLS, which would bypass row level security", user) } return nil } // AssertMigratorRole is the mirror of AssertRole for the privileged batch jobs (the sync and import // commands). Those jobs need the migrator role for one of two reasons: import-candidates writes a // tenant scoped row with an explicit organization_id and no app.current_org_id, so a NOBYPASSRLS role // would be rejected by the RLS WITH CHECK; the catalog syncs (SAM, awards, wages) write GLOBAL // reference tables that the application role is granted read only, so a NOBYPASSRLS role would be // denied INSERT. Both failures are caught here, at boot, by requiring a BYPASSRLS role. If an operator // pastes the application DSN into a job's MM_MIGRATOR_DATABASE_URL this becomes a hard, legible boot // failure instead of an RLS rejection or a permission denied error partway through the run. func AssertMigratorRole(ctx context.Context, pool *pgxpool.Pool) error { var user string var bypass bool err := pool.QueryRow(ctx, `SELECT current_user, rolbypassrls FROM pg_roles WHERE rolname = current_user`).Scan(&user, &bypass) if err != nil { return fmt.Errorf("role assertion query: %w", err) } if !bypass { return fmt.Errorf("pool role %q lacks BYPASSRLS; this job needs the migrator DSN (MM_MIGRATOR_DATABASE_URL)", user) } return nil }