Vercel Serverless Migration Documentation¶
Summary¶
This document describes the refactoring of the Express.js backend for deployment on Vercel serverless functions with Supabase Postgres database.
Stack Information¶
- ORM: Prisma (v6.9.0)
- Database: PostgreSQL (targeting Supabase with PgBouncer)
- Target Platform: Vercel Serverless Functions
- Current Storage: Cloudinary (with local fallback for development)
1. Entry Point Restructure¶
Changes Made¶
Created: /backend/api/index.js¶
Purpose: Vercel entry point that exports the Express app without calling app.listen().
Modified: /backend/src/server.js¶
Removed: export default app; at the end of the file
Purpose: Local development entry point that calls app.listen() - unchanged otherwise.
Created: /backend/vercel.json¶
Purpose: Routes all incoming requests to the Vercel serverless handler, preserving existing route paths.
2. Database Connection for Serverless¶
Changes Made¶
Modified: /backend/prisma/schema.prisma¶
Purpose: Added support for Prisma's connection pooling with directUrl for direct connections when needed (e.g., migrations).
Modified: /backend/src/config/prisma.js¶
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis;
const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
+ datasources: {
+ db: {
+ url: process.env.DATABASE_URL,
+ },
+ },
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
+// Handle cleanup for serverless environments
+if (process.env.NODE_ENV === 'production') {
+ process.on('beforeExit', async () => {
+ await prisma.$disconnect();
+ });
+}
export default prisma;
DATABASE_URL datasource configuration
- Added cleanup handler for serverless environments
Database Connection String Requirements¶
For Vercel/Supabase:
- DATABASE_URL: Should use Supabase pooled connection (port 6543, pgbouncer=true)
- Format: postgres://postgres:[password]@db.[project-ref].supabase.co:6543/postgres?pgbouncer=true
- DIRECT_URL: Should use direct connection for migrations (port 5432)
- Format: postgres://postgres:[password]@db.[project-ref].supabase.co:5432/postgres
3. File System / Uploads Refactoring¶
Changes Made¶
Modified: /backend/src/utils/imageProcessor.js¶
Added serverless detection:
const IS_SERVERLESS = process.env.VERCEL === '1' || process.env.AWS_LAMBDA_FUNCTION_NAME;
const STORAGE_PROVIDER = process.env.STORAGE_PROVIDER || (process.env.VERCEL ? 'cloudinary' : 'local');
Modified storage configuration:
// Use memory storage in serverless environments, disk storage otherwise
const storage = IS_SERVERLESS
? multer.memoryStorage()
: multer.diskStorage({
// ... disk storage config
});
Modified processSingleImageLocal:
async function processSingleImageLocal(file) {
// In serverless environments, local storage is not available
if (IS_SERVERLESS) {
throw new Error('Local storage is not available in serverless environments. Please configure STORAGE_PROVIDER=cloudinary');
}
// ... rest of function
}
Modified processSingleImageCloudinary:
async function processSingleImageCloudinary(file) {
let inputPath = file.path;
let buffer = file.buffer;
// Handle memory storage (serverless)
if (IS_SERVERLESS && buffer) {
try {
// Create a temporary file path for Cloudinary upload
const tempDir = '/tmp';
const ext = getExtensionForMime(file.mimetype);
const timestamp = Date.now();
const random = crypto.randomBytes(8).toString('hex');
const tempFilename = `temp-${timestamp}-${random}${ext}`;
inputPath = path.join(tempDir, tempFilename);
await fs.writeFile(inputPath, buffer);
const result = await uploadToCloudinary(inputPath);
await fs.unlink(inputPath).catch(() => {});
return {
path: result.url,
publicId: result.publicId,
width: result.width,
height: result.height,
originalName: file.originalname,
size: result.bytes,
};
} catch (err) {
if (inputPath && inputPath.startsWith('/tmp')) {
await fs.unlink(inputPath).catch(() => {});
}
throw new Error(`Failed to upload "${file.originalname}" to Cloudinary: ${err.message}`);
}
}
// ... rest of function for disk storage
}
Modified cleanup:
Modified: /backend/src/modules/users/user.routes.js¶
Added serverless detection and memory storage:
const IS_SERVERLESS = process.env.VERCEL === '1' || process.env.AWS_LAMBDA_FUNCTION_NAME;
// Use memory storage in serverless environments, disk storage otherwise
const storage = IS_SERVERLESS
? multer.memoryStorage()
: multer.diskStorage({
// ... disk storage config
});
File System Operations Summary¶
Files with disk operations:
1. /backend/src/utils/imageProcessor.js - Image processing and upload handling
2. /backend/src/modules/users/user.routes.js - Profile image upload
Changes:
- In serverless environments (Vercel), multer now uses memoryStorage() instead of diskStorage()
- Cloudinary uploads write to /tmp (temporary directory available in Lambda) then delete
- Local storage is explicitly blocked in serverless environments with clear error message
- Temp file cleanup disabled in serverless environments (not needed)
Static file serving:
- /backend/src/app.js line 67-69: Static file serving for /uploads route is conditionally disabled when STORAGE_PROVIDER !== 'cloudinary'
- In Vercel deployment, this should use Cloudinary URLs, so static file serving won't be needed
4. WebSocket / Long-Lived Connections¶
Findings¶
No WebSocket libraries found.
- No socket.io, ws, or WebSocket imports detected
- No createServer usage for WebSocket servers
EventEmitter usage (NOT a WebSocket):
- /backend/src/core/listingEmitter.js - Uses Node.js EventEmitter for in-process event handling
- /backend/src/modules/syndication/listeners/telegramListener.js - Listens to EventEmitter for Telegram syndication
- Status: This is fine for serverless - it's in-process event handling, not a network WebSocket connection
Note: The Telegram syndication uses HTTP API calls to Telegram Bot API, not WebSockets.
5. Environment Variables Audit¶
Required Environment Variables¶
Database¶
DATABASE_URL(Required)- Supabase pooled connection string (port 6543, pgbouncer=true)
-
Example:
postgres://postgres:[password]@db.[project-ref].supabase.co:6543/postgres?pgbouncer=true -
DIRECT_URL(Required for migrations) - Supabase direct connection string (port 5432)
- Example:
postgres://postgres:[password]@db.[project-ref].supabase.co:5432/postgres
Authentication¶
JWT_SECRET(Required)- Secret key for JWT token signing
-
Must be set in production
-
JWT_EXPIRES_IN(Optional, default:7d) - JWT token expiration time
CORS¶
ALLOWED_ORIGINS(Optional, but recommended for production)- Comma-separated list of allowed CORS origins
- Example:
https://yourdomain.com,https://www.yourdomain.com,exp://192.168.1.1:19006 - If not set, defaults to localhost development origins
Storage (Cloudinary)¶
STORAGE_PROVIDER(Optional, auto-detected)cloudinaryorlocal-
Defaults to
cloudinaryon Vercel,localotherwise -
CLOUDINARY_CLOUD_NAME(Required if using Cloudinary) -
Cloudinary cloud name
-
CLOUDINARY_API_KEY(Required if using Cloudinary) -
Cloudinary API key
-
CLOUDINARY_API_SECRET(Required if using Cloudinary) -
Cloudinary API secret
-
CLOUDINARY_FOLDER(Optional, default:brostechnology/listings) - Cloudinary folder for uploads
Email (Brevo)¶
BREVO_API_KEY(Optional)- Brevo API key for email sending
-
If not set, emails are simulated (logged only)
-
BREVO_FROM_EMAIL(Optional, default:[email protected]) - From email address for Brevo emails
Telegram Syndication¶
TELEGRAM_CHANNEL_ID(Optional)- Default Telegram channel ID for syndication
- Can also be configured via database
General¶
NODE_ENV(Optional)developmentorproduction-
Affects logging, error messages, and some behaviors
-
PORT(Optional, default:5000) - Port for local development server
-
Not used in Vercel
-
API_BASE_URL(Optional, default:http://localhost:5000) - Base URL for API responses (used in image URL construction)
-
Should be set to production domain in Vercel
-
UPLOAD_DIR(Optional, default:./uploads) - Directory for local file uploads (development only)
- Not used in serverless environments
6. CORS Configuration¶
Changes Made¶
Modified: /backend/src/app.js¶
Added configurable ALLOWED_ORIGINS:
// Parse allowed origins from environment variable
const allowedOriginsEnv = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',').map(origin => origin.trim())
: [];
const defaultOrigins = [
'http://localhost:5000',
'http://localhost:19006',
'http://localhost:3000',
'http://localhost:5173',
'http://10.0.2.2:5000',
'http://10.0.2.2:19006',
'http://127.0.0.1:5000',
];
const allowedOrigins = allowedOriginsEnv.length > 0 ? allowedOriginsEnv : defaultOrigins;
Purpose: Allows production domains to be configured via environment variable while maintaining development defaults.
Recommended Vercel Environment Variable:
7. Long-Running Operations (>10s timeout risk)¶
Analysis¶
Vercel Serverless Function Timeout: 10 seconds (Hobby plan), 60 seconds (Pro plan)
Potentially Long Operations:¶
- Image Upload & Processing (
/api/listingsPOST/PATCH with images) - Risk: Medium-High
- Factors: Number of images (up to 10), image sizes, Cloudinary upload speed
- Estimated Time: 2-8 seconds for typical uploads
- Mitigation: Already using Cloudinary (fast), images resized before upload
-
Recommendation: Monitor in production, consider background jobs if timeouts occur
-
Telegram Syndication (triggered via EventEmitter after listing creation/update)
- Risk: Low-Medium
- Factors: Telegram API response time, image upload to Telegram
- Estimated Time: 1-5 seconds
- Note: This runs asynchronously via EventEmitter, doesn't block the HTTP response
-
Recommendation: Current implementation is fine (non-blocking)
-
Email Sending (password reset via Brevo)
- Risk: Low
- Estimated Time: 1-3 seconds
- Note: Email sending happens after password reset token is saved
-
Recommendation: Current implementation is fine
-
Database Queries (listing search with filters)
- Risk: Low
- Factors: Query complexity, database load
- Estimated Time: <1 second with proper indexing
- Note: Schema has compound indexes on frequently queried fields
- Recommendation: Ensure Supabase has proper indexing
No Critical Long-Running Operations Found¶
All operations should complete within Vercel's timeout limits under normal conditions.
8. Route Sanity Check¶
All Routes (Method + Path)¶
Health¶
GET /health
Authentication (/api/auth)¶
POST /api/auth/registerPOST /api/auth/loginGET /api/auth/mePUT /api/auth/mePOST /api/auth/forgot-passwordPOST /api/auth/reset-password
Categories (/api/categories)¶
GET /api/categoriesGET /api/categories/:idPOST /api/categoriesPATCH /api/categories/:idDELETE /api/categories/:id
Listings (/api/listings)¶
GET /api/listingsGET /api/listings/:idPOST /api/listingsPATCH /api/listings/:idDELETE /api/listings/:id
Public (/api/public)¶
GET /api/public/listingsGET /api/public/listings/:id
Syndication (/api/syndication)¶
GET /api/syndication/configGET /api/syndication/config/:platformPOST /api/syndication/configDELETE /api/syndication/config/:platformGET /api/syndication/telegram/infoPOST /api/syndication/delete-message/:messageIdPOST /api/syndication/edit-message/:messageIdGET /api/syndication/logsPOST /api/syndication/retry/:id
Notifications (/api/notifications)¶
GET /api/notificationsPUT /api/notifications/read-allPUT /api/notifications/:id/read
Commissions (/api/commissions)¶
GET /api/commissions/summaryGET /api/commissions/listingsPATCH /api/commissions/listing/:id
Static Files (conditional)¶
GET /uploads/*(only ifSTORAGE_PROVIDER !== 'cloudinary')
Verification¶
- No route paths changed - all existing routes preserved
- No API contracts changed - request/response shapes unchanged
- Middleware order preserved - authentication, validation, image processing all maintained
- Error handling preserved - all error handlers maintained
9. Deployment Checklist¶
Vercel Project Settings¶
Add these environment variables in Vercel:
Required:
- DATABASE_URL - Supabase pooled connection
- DIRECT_URL - Supabase direct connection
- JWT_SECRET - Generate a secure random string
- CLOUDINARY_CLOUD_NAME
- CLOUDINARY_API_KEY
- CLOUDINARY_API_SECRET
- ALLOWED_ORIGINS - Your production domains
Optional but Recommended:
- API_BASE_URL - Your production API URL
- BREVO_API_KEY - For email functionality
- BREVO_FROM_EMAIL - Your from email address
- TELEGRAM_CHANNEL_ID - For Telegram syndication
Database Setup¶
-
Run Prisma migrations to ensure schema is up to date:
-
Ensure Supabase connection pooling is enabled (port 6543)
Local Development¶
Local development workflow unchanged:
This uses server.js which calls app.listen() as before.
Testing¶
Run the test suite:
10. Uncertainties / Notes¶
Items to Verify¶
- Supabase Connection Pooling:
- Ensure your Supabase project has connection pooling enabled
-
Verify the
DATABASE_URLuses port 6543 withpgbouncer=true -
Cloudinary Configuration:
- Ensure Cloudinary account is set up for unsigned uploads or configure signed uploads if needed
-
Current implementation uses unsigned uploads (simpler for serverless)
-
Telegram Syndication in Serverless:
- EventEmitter-based syndication works in serverless but may not fire if the function exits before the event is processed
- Current implementation: Syndication is triggered synchronously in the request handler via
listingEmitter.emit(), so it should complete before the response -
Monitor: Check if syndication consistently works in production
-
Static File Serving:
- The
/uploadsstatic route is disabled when using Cloudinary (which is the default on Vercel) -
Ensure all image URLs in the database are full Cloudinary URLs, not relative paths
-
Image Processing in Serverless:
- Sharp (image processing library) works in Lambda but may have memory constraints with very large images
- Current limit: 10MB per file, 10 files max
- Monitor: Check for memory errors in Vercel logs
Potential Future Improvements¶
- Background Jobs: If image uploads or Telegram syndication become timeout issues, consider:
- Vercel Cron Jobs for periodic tasks
- Supabase Edge Functions for background processing
-
External queue service (e.g., AWS SQS, Redis Queue)
-
Caching: Add Redis caching for frequently accessed data (listings, categories) to reduce database load
-
CDN for Images: Cloudinary already provides CDN, but ensure proper cache headers are set
11. Summary of Changes¶
Files Created¶
/backend/api/index.js- Vercel entry point/backend/vercel.json- Vercel configuration
Files Modified¶
/backend/src/server.js- Removed export (local dev only)/backend/src/app.js- Added configurable CORS origins/backend/src/config/prisma.js- Added explicit datasource config and cleanup handler/backend/prisma/schema.prisma- Added directUrl support/backend/src/utils/imageProcessor.js- Serverless-aware storage and processing/backend/src/modules/users/user.routes.js- Serverless-aware storage
Files Unchanged¶
- All route files (API contracts preserved)
- All controller files (business logic preserved)
- All middleware files (authentication, validation preserved)
/backend/src/core/listingEmitter.js(EventEmitter unchanged)/backend/src/modules/syndication/(Telegram integration unchanged)
Conclusion¶
The backend has been successfully refactored for Vercel serverless deployment while preserving all existing functionality, API contracts, and business logic. The key changes are:
- Entry point split for Vercel vs local development
- Database connection configured for Supabase pooling
- File uploads refactored to use memory storage and Cloudinary in serverless
- CORS made configurable via environment variables
- No WebSockets or other incompatible features found
The application should deploy successfully to Vercel with the environment variables configured as documented.