A Postgres wire protocol proxy that validates Supabase JWTs and enforces RLS (Row Level Security) via SET ROLE authenticated + SET request.jwt.claim.sub. Built for the tw CLI (TaskChampion) running in sandbox pods inside the cluster.
PostgREST validates a JWT, sets SET ROLE authenticated and SET request.jwt.claim.sub = $sub, then forwards SQL. This proxy does the same over the pgwire (PostgreSQL binary) protocol instead of HTTP.
pgwire client (tw CLI)
│ StartupMessage → CleartextPassword challenge → JWT as password
▼
pgwire-supabase-proxy
│ Validates JWT using SUPABASE_JWT_SECRET
│ Acquires backend connection from CNPG pool
│ SET ROLE authenticated
│ SET request.jwt.claim.sub = $user_id
▼
CNPG pooler → Postgres (RLS enforces tenant isolation)
The auth.uid() function in Postgres reads current_setting('request.jwt.claim.sub'), so no application-level filtering is needed — RLS does the work.
- Client sends pgwire
StartupMessagewith anyuservalue - Proxy responds with
AuthenticationCleartextPasswordchallenge - Client sends JWT as the password in
PasswordMessage - Proxy validates the JWT, extracts the
subclaim - Proxy acquires a backend connection from the per-user pool
- Proxy runs
SET ROLE authenticatedandSET request.jwt.claim.sub = '$user_id' - Proxy returns
AuthenticationOkand holds the connection for the session - All subsequent queries are forwarded over the same backend connection
- On session end: connection returned to pool;
DISCARD ALLruns on next checkout
When clients use the extended query protocol (prepared statements with $1, $2, ... placeholders), parameters are substituted into the SQL string before forwarding. Parameters are expected in text format (UTF-8 strings). Binary format parameters are not yet supported.
The proxy preserves transaction state per session. BEGIN/COMMIT/ROLLBACK are forwarded to the backend, and pgwire TransactionStart/TransactionEnd responses are returned to the client.
| Variable | Required | Description |
|---|---|---|
SUPABASE_JWT_SECRET |
Yes | Supabase JWT signing secret (HMAC-SHA256) |
DATABASE_URL |
Yes | Backend Postgres connection string (postgresql://user:pass@host:5432/db) |
LISTEN_ADDR |
No | Listen address (default: 0.0.0.0:5432) |
POOL_SIZE |
No | Connections per user pool (default: 10) |
cargo build --releasedocker build -t pgwire-supabase-proxy .Managed via Tanka. Environments:
environments/dev/—supa-devnamespace,0.0.0.0:5432environments/prod/—supa-prodnamespace
cd tanka
tk eval environments/dev # preview manifests
tk show environments/dev # full manifests
tk apply environments/dev # deploy- Extended query binary parameters: Not yet supported. Clients must use text format for parameters.
- Session cleanup:
DISCARD ALLruns at the start of the next connection checkout viaRecyclingMethod::Clean, not immediately on disconnect. - Token refresh: If the JWT expires mid-session, Postgres returns an error. The client must reconnect with a fresh token.
- Backslash escaping:
substitute_paramsescapes string literals assumingstandard_conforming_strings=on. Do not point this proxy at a Postgres instance with that setting disabled.