Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions VERCEL_DATABASE_SETUP.md
Original file line number Diff line number Diff line change
@@ -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.

24 changes: 16 additions & 8 deletions src/pages/api/trpc/[trpc].ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "<no-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 ?? "<no-path>"}: ${error.message}`);
} else if (env.NODE_ENV === "development") {
console.error(`tRPC failed on ${path ?? "<no-path>"}: ${error.message}`);
}
},
});
161 changes: 141 additions & 20 deletions src/server/db.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,171 @@
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 <T>(
operation: () => Promise<T>,
retries = MAX_RETRIES,
): Promise<T> => {
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;
};

const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof createPrismaClient> | 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 = <T extends object>(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);
});
}
Loading