What kind of change does this PR introduce?
New HTTP endpoint.
What is the current behavior?
Supavisor speaks Postgres only over TCP. Serverless JS runtimes (Vercel, Cloudflare Workers) can't keep TCP pools between requests - every call pays a fresh SCRAM handshake. The WebSocket proxy at /v2 helps some clients but not the HTTP variant the JS ecosystem (@neondatabase/serverless, @vercel/postgres, drizzle-orm/neon-http) actually uses.
What is the new behavior?
POST /sql with the Neon wire format. Drop-in for @neondatabase/serverless:
import { neon, neonConfig } from '@neondatabase/serverless'
neonConfig.fetchEndpoint = (host) => `https://${host}/sql`
const sql = neon('postgres://postgres.<external_id>:<pwd>@<host>/postgres')
await sql`SELECT id FROM users WHERE email = ${email}`The controller is itself a Postgres client of Supavisor - it dials 127.0.0.1:proxy_port_transaction via Postgrex, so every HTTP request reuses the existing ClientHandler -> pool -> DbHandler machinery. No changes to the TCP path.
Highlights:
- Single query and batched transaction (queries: [...]).
- Synchronous SCRAM-SHA-256 password check before pool checkout - bad-password attempts don't churn pools and feed the existing CircuitBreaker correctly.
- Per-(tenant, user, password_hash) Postgrex pool registry with idle-TTL eviction, invalidated on tenant updates via
Supavisor.del_all_cache/1. - PromEx metrics and per-error audit log.
Both gates are off by default: HTTP_SQL_ENABLED=false and per-tenant feature_flags.http_sql.
Additional context
Integration suite for the new endpoint is 101/101. A Node script at test/integration/js/neon_http_sql/index.js runs the real upstream @neondatabase/serverless driver against a live instance - release-gate smoke before each ship.
TODO: cache_statement for repeat SQL, per-IP rate limit, JWT-auth path (rejected with 401 today).
@v0idpwn Could you please take a look?