diff --git a/VERCEL_DATABASE_SETUP.md b/VERCEL_DATABASE_SETUP.md new file mode 100644 index 00000000..50b592c2 --- /dev/null +++ b/VERCEL_DATABASE_SETUP.md @@ -0,0 +1,88 @@ +# Vercel Database Connection Setup Guide + +## Problem +Your Vercel deployment is losing database connections because the `DATABASE_URL` is incorrectly configured. The error shows Prisma is trying to connect to port 5432 (direct connection) instead of port 6543 (pooled connection). + +## Solution: Configure Supabase Connection Pooling + +### Step 1: Get Your Supabase Connection URLs + +1. Go to your Supabase Dashboard +2. Navigate to **Settings** → **Database** +3. Find the **Connection Pooling** section + +### Step 2: Set Environment Variables in Vercel + +You need to set **two** environment variables in Vercel: + +#### 1. `DATABASE_URL` (for queries - REQUIRED) +- Use the **Connection Pooling** → **Transaction mode** URL +- Format: `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true` +- **Important**: Must use port **6543** (not 5432) +- **Important**: Must include `?pgbouncer=true` parameter + +#### 2. `DIRECT_URL` (for migrations - OPTIONAL but recommended) +- Use the **Connection String** → **URI** (direct connection) +- Format: `postgresql://postgres:[password]@aws-0-[region].pooler.supabase.com:5432/postgres` +- This is used only for migrations (`prisma migrate`) + +### Step 3: Verify Your Configuration + +After setting the environment variables, check your Vercel deployment logs. You should see: + +✅ **Correct configuration:** +- No errors about port 5432 +- Connection pooler URL with port 6543 + +❌ **Wrong configuration (what you likely have now):** +- Error: "DATABASE_URL uses pooler hostname but wrong port (5432)" +- Connection errors: "Can't reach database server" + +### Example Correct URLs + +**DATABASE_URL (pooled - for queries):** +``` +postgresql://postgres.abcdefghijklmnop:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true +``` + +**DIRECT_URL (direct - for migrations):** +``` +postgresql://postgres:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres +``` + +## Why This Matters + +- **Port 6543**: Supabase's connection pooler (PgBouncer) - optimized for serverless +- **Port 5432**: Direct PostgreSQL connection - not suitable for Vercel serverless +- **Connection Pooling**: Prevents connection exhaustion in serverless environments +- **Retry Logic**: The code now includes automatic retry logic for connection failures + +## Additional Improvements Made + +1. ✅ **Connection Retry Logic**: Automatic retry with exponential backoff (3 attempts) +2. ✅ **Connection Health Checks**: Validates connection URL on startup +3. ✅ **Better Error Logging**: Production logs now show connection errors +4. ✅ **Connection Reuse**: Prisma client is reused across serverless invocations + +## Testing + +After updating your Vercel environment variables: + +1. Redeploy your application +2. Check Vercel logs for any connection warnings +3. Test database queries - they should work reliably now +4. Monitor for connection errors in production + +## Troubleshooting + +If you still see connection errors: + +1. **Verify DATABASE_URL format**: Must have `:6543` and `?pgbouncer=true` +2. **Check Supabase Dashboard**: Ensure connection pooling is enabled +3. **Check Vercel Logs**: Look for the validation messages on startup +4. **Test Connection**: Try connecting manually with the pooled URL + +## Need Help? + +Check your Vercel deployment logs for specific error messages. The code now provides detailed warnings about incorrect configuration. + diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts index 34df55b8..a89080cd 100644 --- a/src/pages/api/trpc/[trpc].ts +++ b/src/pages/api/trpc/[trpc].ts @@ -8,12 +8,20 @@ import { createTRPCContext } from "@/server/api/trpc"; export default createNextApiHandler({ router: appRouter, createContext: createTRPCContext, - onError: - env.NODE_ENV === "development" - ? ({ path, error }) => { - console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}` - ); - } - : undefined, + onError: ({ path, error, type }) => { + // Log connection errors in production for debugging + const isConnectionError = + error.message.includes("Can't reach database server") || + error.message.includes("connection") || + error.message.includes("timeout") || + error.message.includes("P1001") || + error.message.includes("P1008") || + error.message.includes("P1017"); + + if (isConnectionError) { + console.error(`Database connection error on ${path ?? ""}: ${error.message}`); + } else if (env.NODE_ENV === "development") { + console.error(`tRPC failed on ${path ?? ""}: ${error.message}`); + } + }, }); diff --git a/src/server/db.ts b/src/server/db.ts index b84d9dcb..c17a9783 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -1,17 +1,110 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient, Prisma } from "@prisma/client"; import { env } from "@/env"; +// Connection retry configuration +const MAX_RETRIES = 3; +const INITIAL_RETRY_DELAY_MS = 500; + +// Check if error is a connection error that should be retried +const isConnectionError = (error: unknown): boolean => { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + // P1001: Can't reach database server + // P1008: Operations timed out + // P1017: Server has closed the connection + return ["P1001", "P1008", "P1017"].includes(error.code); + } + if (error instanceof Prisma.PrismaClientUnknownRequestError) { + const message = error.message.toLowerCase(); + return ( + message.includes("can't reach database server") || + message.includes("connection") || + message.includes("timeout") || + message.includes("econnrefused") + ); + } + // Check for generic connection errors + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return ( + message.includes("can't reach database server") || + message.includes("connection") || + message.includes("timeout") || + message.includes("econnrefused") + ); + } + return false; +}; + +// Retry wrapper for database operations with exponential backoff +const withRetry = async ( + operation: () => Promise, + retries = MAX_RETRIES, +): Promise => { + try { + return await operation(); + } catch (error) { + if (retries > 0 && isConnectionError(error)) { + // Exponential backoff: 500ms, 1000ms, 2000ms + const attempt = MAX_RETRIES - retries + 1; + const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1); + + if (env.NODE_ENV === "development") { + console.warn(`Database connection error, retrying in ${delay}ms (${attempt}/${MAX_RETRIES})`); + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + + // Try to reconnect before retrying + try { + await prismaClient.$connect(); + } catch { + // Ignore connection errors here, let the retry handle it + } + + return withRetry(operation, retries - 1); + } + throw error; + } +}; + const createPrismaClient = () => { + // Validate DATABASE_URL is using pooled connection for Supabase + const dbUrl = env.DATABASE_URL; + if (dbUrl) { + try { + // Properly parse URL to validate hostname instead of substring matching + const url = new URL(dbUrl); + const hostname = url.hostname.toLowerCase(); + const port = url.port ? parseInt(url.port, 10) : (url.protocol === "postgresql:" ? 5432 : null); + const isSupabase = hostname.endsWith(".supabase.com") || hostname === "supabase.com"; + const isPooler = hostname.includes("pooler"); + const searchParams = new URLSearchParams(url.search); + const hasPgbouncer = searchParams.has("pgbouncer") && searchParams.get("pgbouncer") === "true"; + + if (isSupabase) { + if (isPooler && port === 5432) { + console.error("DATABASE_URL: pooler hostname requires port 6543, not 5432"); + } else if (!isPooler && port === 5432) { + console.error("DATABASE_URL: use connection pooler (port 6543) for serverless"); + } else if (isPooler && port === 6543 && !hasPgbouncer) { + console.warn("DATABASE_URL: add ?pgbouncer=true for optimal performance"); + } + } + } catch (error) { + // If URL parsing fails, log warning but don't block initialization + // Prisma will handle invalid URLs with its own error messages + if (env.NODE_ENV === "development") { + console.warn("Could not parse DATABASE_URL for validation:", error); + } + } + } + const client = new PrismaClient({ log: env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], }); - // In serverless environments (Vercel), we want to avoid eager connection - // Prisma will connect lazily on first query, which is better for cold starts - // Don't call $connect() here - let Prisma handle connections on-demand - return client; }; @@ -19,32 +112,60 @@ const globalForPrisma = globalThis as unknown as { prisma: ReturnType | undefined; }; -// Reuse Prisma client across invocations to optimize connection pooling -// In Vercel serverless, the same container may handle multiple requests -// Reusing the client prevents creating new connections for each request -export const db = - globalForPrisma.prisma ?? createPrismaClient(); +// Create or reuse Prisma client +const prismaClient = globalForPrisma.prisma ?? createPrismaClient(); -// Store in globalThis for reuse across all environments -// This is especially important in serverless where the same container -// may handle multiple requests, allowing connection reuse if (!globalForPrisma.prisma) { - globalForPrisma.prisma = db; + globalForPrisma.prisma = prismaClient; } +// Create a wrapper that adds retry logic to all Prisma operations +// We'll intercept model access and wrap query methods +const createRetryProxy = (target: T): T => { + return new Proxy(target, { + get(obj, prop) { + const value = obj[prop as keyof T]; + + // If it's a model (user, wallet, etc.), wrap its methods + if (value && typeof value === "object" && !prop.toString().startsWith("$")) { + return createRetryProxy(value as object); + } + + // If it's a function (query method), wrap it with retry logic + if (typeof value === "function") { + return (...args: unknown[]) => { + return withRetry(() => { + const result = value.apply(obj, args); + return result instanceof Promise ? result : Promise.resolve(result); + }); + }; + } + + return value; + }, + }) as T; +}; + +// Export db with retry logic +export const db = createRetryProxy(prismaClient); + // Graceful shutdown handling if (typeof process !== "undefined") { - process.on("beforeExit", async () => { - await db.$disconnect(); - }); + const disconnect = async () => { + try { + await prismaClient.$disconnect(); + } catch (error) { + // Ignore errors during shutdown + } + }; + process.on("beforeExit", disconnect); process.on("SIGINT", async () => { - await db.$disconnect(); + await disconnect(); process.exit(0); }); - process.on("SIGTERM", async () => { - await db.$disconnect(); + await disconnect(); process.exit(0); }); }