Skip to content

Instantly share code, notes, and snippets.

@adithep
Forked from kincaidoneil/prisma.ts
Created September 3, 2024 06:27
Show Gist options
  • Select an option

  • Save adithep/5dc6c9bc59a975832fe8fc80e1843862 to your computer and use it in GitHub Desktop.

Select an option

Save adithep/5dc6c9bc59a975832fe8fc80e1843862 to your computer and use it in GitHub Desktop.
Connect to Supabase with Prisma on the Edge (e.g. Vercel Edge functions). Test locally by running the Supabase connection pooler
import { neonConfig, Pool } from '@neondatabase/serverless'
import { PrismaNeon } from '@prisma/adapter-neon'
import { PrismaClient } from '@prisma/client'
import { WebSocket } from 'ws'
// Example Supabase pooled connection string (must use Supavisor)
const connectionString =
'postgres://[username].[id]:[password]@aws-0-us-east-1.pooler.supabase.com:5432/[db]'
const url = new URL(connectionString)
if (url.hostname === 'localhost') {
// Disable SSL for local connections
neonConfig.useSecureWebSocket = false
// WebSocket proxy is hosted on `4000` locally, so add port. Does not work in production.
neonConfig.wsProxy = (host, port) => `${host}:${port}/v2`
}
// Only Neon hosts support this -- non-deterministic errors otherwise
neonConfig.pipelineConnect = false
// So it can also work in Node.js
neonConfig.webSocketConstructor = WebSocket
const pool = new Pool({
connectionString,
})
const adapter = new PrismaNeon(pool)
const prisma = new PrismaClient({
adapter,
})
import { GenericContainer, Network, Wait } from 'testcontainers'
import logger from pino
async function setup() {
/**
* Supabase's Supavisor, their Postgres connection pooler, operates WS proxy for use in edge functions, just like Neon does!
* *And it's fully compatible with Neon's serverless driver!*
*
* AFAICT, there's no public example of operating this locally with the WS proxy.
*
* Adapt code for Postgres and Supavisor from this Docker Compose config:
* https://github.com/supabase/storage/blob/master/.docker/docker-compose-infra.yml
* Here's the Dockerfile for Supavisor:
* https://github.com/supabase/supavisor/blob/main/Dockerfile
*
* In the future, we might be able to use Supabase CLI, but it still uses PgBouncer and not Supavisor.
*
* Here's how it works:
* - Supavisor has a "metadata" database to track each tenant and their own remote database. We're just using a single Postgres instance
* for both this meta database and our tenant database.
* - Use Supavisor's admin API to create a new tenant. This includes the credentials of the tenant's own Postgres database.
* - Supavisor runs the WS proxy on `localhost:4000/v2`
* - Supavisor connection string is "postgres://[PG_USER].[TENANT_ID]:[PG_PASSWORD]@localhost:5432/[PG_DATABASE]"
* - Use Neon's serverless driver and Prisma's adapter to connect over WS
*/
logger.info('Starting Postgres...')
const network = await new Network().start()
const postgresContainer = await new GenericContainer('postgres')
.withName('db') // Hostname in Docker network
.withNetwork(network)
.withCopyContentToContainer([
{
content: 'CREATE SCHEMA IF NOT EXISTS _supavisor;',
target: '/docker-entrypoint-initdb.d/init.sql',
},
])
.withEnvironment({
POSTGRES_DB: 'public',
POSTGRES_USER: 'user',
POSTGRES_PASSWORD: 'password',
})
.withHealthCheck({
test: ['CMD-SHELL', 'pg_isready -d postgres'],
interval: 1000,
timeout: 30000,
retries: 20,
startPeriod: 0,
})
.withWaitStrategy(Wait.forHealthCheck())
.start()
logger.info('Started Postgres.')
logger.info('Starting Supavisor...')
const API_JWT_SECRET = 'dev'
const JWT =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNjQ1MTkyODI0LCJleHAiOjE5NjA3Njg4MjR9.M9jrxyvPLkUxWgOYSf5dNdJ8v_eRrq810ShFRT8N-6M'
const supavisorContainer = await new GenericContainer(
'supabase/supavisor:1.1.39',
)
.withExposedPorts(
// Tenant API and WS proxy
{
container: 4000,
host: 4000,
},
// Direct pooled Postgres connections (for Prisma migrations, etc)
{
container: 5432,
host: 5432,
},
)
.withNetwork(network)
.withEnvironment({
PORT: '4000',
PROXY_PORT_SESSION: '5432',
PROXY_PORT_TRANSACTION: '6543',
DATABASE_URL: 'postgres://user:password@db:5432/public',
SECRET_KEY_BASE: randomBytes(64).toString('base64'),
VAULT_ENC_KEY: '12345678901234567890123456789032',
API_JWT_SECRET,
REGION: 'local',
ERL_AFLAGS: ' -proto_dist inet_tcp', // Defaults to inet6_tcp, fails without this. I suspect this changes it to IPv4 or something
})
.withWaitStrategy(Wait.forHttp('/api/health', 4000).forStatusCode(204))
.withCommand(['/bin/sh', '-c', '/app/bin/migrate && /app/bin/server'])
.start()
logger.info('Started Supavisor.')
logger.info('Configuring Supavisor tenant...')
const TENANT_ID = 'dev_tenant'
const response = await fetch(
`http://localhost:4000/api/tenants/${TENANT_ID}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${JWT}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenant: {
db_host: 'db',
db_port: 5432,
db_database: 'public',
ip_version: 'auto',
require_user: true,
upstream_ssl: false,
enforce_ssl: false,
users: [
{
db_user: 'user',
db_password: 'password',
mode_type: 'transaction',
pool_checkout_timeout: 10000,
pool_size: 10,
},
],
},
}),
},
)
if (!response.ok) {
throw new Error('Error creating Supavisor tenant')
}
logger.info('Configured Supavisor tenant.')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment