File size: 4,129 Bytes
c09f67c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | # Database Connection Pooling
Technical documentation for the database connection setup across Supabase and Railway.
## Overview
The application connects to Supabase Postgres through **Supavisor** (Supabase's shared connection pooler) in **transaction mode**. Each Railway region's API instance reads from the closest Supabase read replica and writes to the primary database in EU.
## Connection Modes
Supabase offers two pooling modes via `pooler.supabase.com`:
| Mode | Port | Behavior |
|------|------|----------|
| **Session mode** | `5432` | 1:1 client-to-backend mapping. No real pooling β each app connection holds a dedicated Postgres connection for its entire lifetime. |
| **Transaction mode** | `6543` | Real connection pooling. Backend connections are shared between clients and only held during a transaction, then returned to the pool. |
**We use transaction mode (port 6543).** This is critical β session mode on port 5432 provides zero pooling benefit despite routing through `pooler.supabase.com`.
### Connection String Format
```
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
```
## Dedicated Pooler (PgBouncer)
Supabase also offers a dedicated PgBouncer co-located on the database machine at `db.<ref>.supabase.co:6543`. This has lower latency (no network hop to a separate server) but requires IPv6 connectivity or the Supabase IPv4 add-on ($4/mo per database).
Railway does **not** support IPv6 to Supabase's direct endpoints, so the shared Supavisor pooler is the correct choice for our infrastructure.
## Multi-Region Replica Mapping
The API runs in 3 Railway regions. Each instance reads from the closest Supabase read replica via the `RAILWAY_REPLICA_REGION` environment variable:
| Railway Region | Env Var | Supabase Region | Role |
|----------------|---------|-----------------|------|
| `europe-west4-drams3a` | `DATABASE_FRA_URL` | `eu-central-1` | Primary (reads + writes) |
| `us-east4-eqdc4a` | `DATABASE_IAD_URL` | `us-east-1` | Read replica |
| `us-west2` | `DATABASE_SJC_URL` | `us-west-1` | Read replica |
- **Reads** are routed to the regional replica via `executeOnReplica` and the `withReplicas` wrapper in `packages/db/src/replicas.ts`.
- **Writes** always go to `DATABASE_PRIMARY_URL` (the primary in `eu-central-1`).
## Pool Configuration
Defined in `packages/db/src/client.ts`:
| Setting | Development | Production |
|---------|-------------|------------|
| `max` | 8 | 12 |
| `idleTimeoutMillis` | 5,000ms | 60,000ms |
| `connectionTimeoutMillis` | 5,000ms | 5,000ms |
| `maxUses` | 100 | 0 (unlimited) |
| `ssl` | disabled | enabled (rejectUnauthorized: false) |
Each API instance creates up to 2 pools (primary + 1 regional replica), so the maximum backend connections per instance is `12 Γ 2 = 24`. With Supavisor transaction mode, these are multiplexed into a much smaller number of actual Postgres backend connections.
## Prepared Statements
Transaction mode does **not** support named prepared statements. The `node-postgres` (`pg`) package with Drizzle ORM uses unnamed parameterized queries (extended query protocol without a `name` field), which are compatible with transaction mode. No special configuration is needed.
**Do not** add named queries like `pool.query({ name: 'my-query', text: '...' })` β these will fail through the pooler.
## Environment Variables
### API Service
- `DATABASE_PRIMARY_URL` β Primary database (EU, writes + reads)
- `DATABASE_FRA_URL` β EU read replica (same as primary)
- `DATABASE_IAD_URL` β US East read replica
- `DATABASE_SJC_URL` β US West read replica
### Worker Service
- `DATABASE_PRIMARY_POOLER_URL` β Primary database (EU)
### Dashboard Service
- No database variables β connects to the API via tRPC, not directly to Postgres.
## Railway Deploy Configuration
- **API**: 3 regions Γ 1 replica each (production), 3 regions Γ 1 replica each (staging)
- **Dashboard**: 3 regions Γ 2 replicas each (production), 3 regions Γ 1 replica each (staging, with serverless/sleep enabled)
- **Worker**: 1 region (EU) Γ 3 replicas (production)
|