-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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