Skip to content

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

import app from '../src/app.js';

export default app;
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

{
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/api/index.js"
    }
  ]
}
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

 datasource db {
   provider = "postgresql"
   url      = env("DATABASE_URL")
+  directUrl = env("DIRECT_URL")
 }
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;
Purpose: - Singleton pattern already implemented (good for serverless) - Added explicit 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:

// Only run cleanup in non-serverless environments
if (!IS_SERVERLESS) {
  cleanupTempFiles();
}

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)
  • cloudinary or local
  • Defaults to cloudinary on Vercel, local otherwise

  • 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)
  • development or production
  • 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:

ALLOWED_ORIGINS=https://your-website.com,https://www.your-website.com,exp://192.168.1.1:19006


7. Long-Running Operations (>10s timeout risk)

Analysis

Vercel Serverless Function Timeout: 10 seconds (Hobby plan), 60 seconds (Pro plan)

Potentially Long Operations:

  1. Image Upload & Processing (/api/listings POST/PATCH with images)
  2. Risk: Medium-High
  3. Factors: Number of images (up to 10), image sizes, Cloudinary upload speed
  4. Estimated Time: 2-8 seconds for typical uploads
  5. Mitigation: Already using Cloudinary (fast), images resized before upload
  6. Recommendation: Monitor in production, consider background jobs if timeouts occur

  7. Telegram Syndication (triggered via EventEmitter after listing creation/update)

  8. Risk: Low-Medium
  9. Factors: Telegram API response time, image upload to Telegram
  10. Estimated Time: 1-5 seconds
  11. Note: This runs asynchronously via EventEmitter, doesn't block the HTTP response
  12. Recommendation: Current implementation is fine (non-blocking)

  13. Email Sending (password reset via Brevo)

  14. Risk: Low
  15. Estimated Time: 1-3 seconds
  16. Note: Email sending happens after password reset token is saved
  17. Recommendation: Current implementation is fine

  18. Database Queries (listing search with filters)

  19. Risk: Low
  20. Factors: Query complexity, database load
  21. Estimated Time: <1 second with proper indexing
  22. Note: Schema has compound indexes on frequently queried fields
  23. 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/register
  • POST /api/auth/login
  • GET /api/auth/me
  • PUT /api/auth/me
  • POST /api/auth/forgot-password
  • POST /api/auth/reset-password

Categories (/api/categories)

  • GET /api/categories
  • GET /api/categories/:id
  • POST /api/categories
  • PATCH /api/categories/:id
  • DELETE /api/categories/:id

Listings (/api/listings)

  • GET /api/listings
  • GET /api/listings/:id
  • POST /api/listings
  • PATCH /api/listings/:id
  • DELETE /api/listings/:id

Public (/api/public)

  • GET /api/public/listings
  • GET /api/public/listings/:id

Syndication (/api/syndication)

  • GET /api/syndication/config
  • GET /api/syndication/config/:platform
  • POST /api/syndication/config
  • DELETE /api/syndication/config/:platform
  • GET /api/syndication/telegram/info
  • POST /api/syndication/delete-message/:messageId
  • POST /api/syndication/edit-message/:messageId
  • GET /api/syndication/logs
  • POST /api/syndication/retry/:id

Notifications (/api/notifications)

  • GET /api/notifications
  • PUT /api/notifications/read-all
  • PUT /api/notifications/:id/read

Commissions (/api/commissions)

  • GET /api/commissions/summary
  • GET /api/commissions/listings
  • PATCH /api/commissions/listing/:id

Static Files (conditional)

  • GET /uploads/* (only if STORAGE_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

  1. Run Prisma migrations to ensure schema is up to date:

    npx prisma migrate deploy
    

  2. Ensure Supabase connection pooling is enabled (port 6543)

Local Development

Local development workflow unchanged:

npm run dev

This uses server.js which calls app.listen() as before.

Testing

Run the test suite:

npm test


10. Uncertainties / Notes

Items to Verify

  1. Supabase Connection Pooling:
  2. Ensure your Supabase project has connection pooling enabled
  3. Verify the DATABASE_URL uses port 6543 with pgbouncer=true

  4. Cloudinary Configuration:

  5. Ensure Cloudinary account is set up for unsigned uploads or configure signed uploads if needed
  6. Current implementation uses unsigned uploads (simpler for serverless)

  7. Telegram Syndication in Serverless:

  8. EventEmitter-based syndication works in serverless but may not fire if the function exits before the event is processed
  9. Current implementation: Syndication is triggered synchronously in the request handler via listingEmitter.emit(), so it should complete before the response
  10. Monitor: Check if syndication consistently works in production

  11. Static File Serving:

  12. The /uploads static route is disabled when using Cloudinary (which is the default on Vercel)
  13. Ensure all image URLs in the database are full Cloudinary URLs, not relative paths

  14. Image Processing in Serverless:

  15. Sharp (image processing library) works in Lambda but may have memory constraints with very large images
  16. Current limit: 10MB per file, 10 files max
  17. Monitor: Check for memory errors in Vercel logs

Potential Future Improvements

  1. Background Jobs: If image uploads or Telegram syndication become timeout issues, consider:
  2. Vercel Cron Jobs for periodic tasks
  3. Supabase Edge Functions for background processing
  4. External queue service (e.g., AWS SQS, Redis Queue)

  5. Caching: Add Redis caching for frequently accessed data (listings, categories) to reduce database load

  6. 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:

  1. Entry point split for Vercel vs local development
  2. Database connection configured for Supabase pooling
  3. File uploads refactored to use memory storage and Cloudinary in serverless
  4. CORS made configurable via environment variables
  5. No WebSockets or other incompatible features found

The application should deploy successfully to Vercel with the environment variables configured as documented.