Consolidate deploy on Vercel (make it actually deployable)#5
Conversation
…there Chosen target: Vercel. Removes the four-way deploy-config split and wires the app up to genuinely run on Vercel instead of leaving broken scaffolding. Vercel wiring: - server/app.ts: new createApp() factory that builds the Express app + middleware + routes without listening. Deliberately does not import ./vite, so the serverless bundle stays free of dev-only Vite deps. - server/index.ts: local/self-hosted entry now uses createApp(); also loads dotenv so a local .env is picked up (it never was before). - api/index.ts: mounts the Express app as a single Vercel serverless function; replaces the non-compiling api/videos.ts placeholder. - vercel.json: rewrites /api/* to the function and everything else to the SPA (was rewriting ALL routes to index.html, which 404'd the API). - tsconfig: include api/** so CI typechecks the function. Latent DB bug (surfaced by loading .env for the first time): - db.ts treated any non-'temp:temp@localhost' DATABASE_URL as a real Neon Postgres URL. The sqlite:./local.db placeholder then made the driver dial wss://./v2 and throw ENOTFOUND on every search-cache write. Now only postgres:// URLs activate the DB; anything else runs DB-less. - .env.example: DATABASE_URL commented out by default (was a bogus URL that pointed the driver at a nonexistent localhost:5432). Removed (consolidating away from these): - Dockerfile, docker-compose.cache.yml, nginx.conf, .replit - deployment-guide.md (was entirely Docker/nginx, now inaccurate) - api/videos.ts (broken placeholder) Kept replit.md (it's a project changelog, not deploy config) and optimization-roadmap.md (still-relevant future work). README: replaced the Docker guide with a Vercel deploy section that is honest about serverless limits (disk video cache + transcoding don't run on Vercel; search and Archive.org proxy streaming do). Verified: npm run check = 0 errors, vite build + full build pass, npm ci clean, local server boots and UI search returns results (18 for 'jazz') with no ENOTFOUND noise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request refactors the application to support Vercel serverless deployment by separating the Express app creation logic into a shared createApp factory in server/app.ts, which is used by both the local server entry point and the new Vercel serverless function handler in api/index.ts. It also updates environment configurations to gracefully fall back to in-memory caches when a PostgreSQL database is not configured, and cleans up obsolete Docker and Nginx deployment files. The review feedback suggests optimizing the request logger to avoid full serialization of large JSON payloads, and refactoring createApp to return the existing server instance created by registerRoutes to prevent redundant server creation and preserve WebSocket handlers.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| res.on("finish", () => { | ||
| const duration = Date.now() - start; | ||
| if (path.startsWith("/api")) { | ||
| let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; | ||
| if (capturedJsonResponse) { | ||
| logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; | ||
| } | ||
| if (logLine.length > 80) { | ||
| logLine = logLine.slice(0, 79) + "…"; | ||
| } | ||
| log(logLine); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Stringifying the entire JSON response body on every API request is highly inefficient, especially for large payloads (like search results). Since the log line is truncated to 80 characters anyway, we should avoid full serialization of large objects/arrays to save CPU cycles and memory.
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
let payload = "";
if (typeof capturedJsonResponse === "object" && capturedJsonResponse !== null) {
if (Array.isArray(capturedJsonResponse)) {
payload = `[Array(${capturedJsonResponse.length})]`;
} else {
const keys = Object.keys(capturedJsonResponse);
if (keys.length > 5) {
payload = `{ ${keys.slice(0, 5).join(", ")}... (+${keys.length - 5} keys) }`;
} else {
payload = JSON.stringify(capturedJsonResponse);
}
}
} else {
payload = String(capturedJsonResponse);
}
logLine += ` :: ${payload}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});| await registerRoutes(app); | ||
|
|
||
| app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { | ||
| const status = err.status || err.statusCode || 500; | ||
| const message = err.message || "Internal Server Error"; | ||
| res.status(status).json({ message }); | ||
| }); | ||
|
|
||
| return app; | ||
| } |
There was a problem hiding this comment.
Currently, registerRoutes(app) creates and returns an http.Server instance, which is discarded here. Then, server/index.ts creates another redundant server instance. Returning the server instance from createApp allows us to reuse it, ensuring any attached handlers (like WebSockets) are preserved. Additionally, we should log the error stack trace in the error handler so that API failures are not silently swallowed in local development.
| await registerRoutes(app); | |
| app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { | |
| const status = err.status || err.statusCode || 500; | |
| const message = err.message || "Internal Server Error"; | |
| res.status(status).json({ message }); | |
| }); | |
| return app; | |
| } | |
| const server = await registerRoutes(app); | |
| app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { | |
| const status = err.status || err.statusCode || 500; | |
| const message = err.message || "Internal Server Error"; | |
| console.error(err); | |
| res.status(status).json({ message }); | |
| }); | |
| return { app, server }; | |
| } |
| import "dotenv/config"; | ||
| import { createServer } from "http"; | ||
| import { createApp } from "./app"; | ||
| import { setupVite, serveStatic, log } from "./vite"; | ||
|
|
||
| const app = express(); | ||
| // Trust only the first proxy hop; 'true' would let clients spoof | ||
| // X-Forwarded-For and bypass IP-based rate limiting | ||
| app.set('trust proxy', 1); | ||
| app.use(express.json()); | ||
| app.use(express.urlencoded({ extended: false })); | ||
|
|
||
| app.use((req, res, next) => { | ||
| const start = Date.now(); | ||
| const path = req.path; | ||
| let capturedJsonResponse: Record<string, any> | undefined = undefined; | ||
|
|
||
| const originalResJson = res.json; | ||
| res.json = function (bodyJson, ...args) { | ||
| capturedJsonResponse = bodyJson; | ||
| return originalResJson.apply(res, [bodyJson, ...args]); | ||
| }; | ||
|
|
||
| res.on("finish", () => { | ||
| const duration = Date.now() - start; | ||
| if (path.startsWith("/api")) { | ||
| let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; | ||
| if (capturedJsonResponse) { | ||
| logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; | ||
| } | ||
|
|
||
| if (logLine.length > 80) { | ||
| logLine = logLine.slice(0, 79) + "…"; | ||
| } | ||
|
|
||
| log(logLine); | ||
| } | ||
| }); | ||
|
|
||
| next(); | ||
| }); | ||
|
|
||
| // Local / self-hosted entry point. On Vercel the app is served by | ||
| // api/index.ts instead, which imports the same createApp() factory. | ||
| (async () => { | ||
| const server = await registerRoutes(app); | ||
|
|
||
| app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { | ||
| const status = err.status || err.statusCode || 500; | ||
| const message = err.message || "Internal Server Error"; | ||
|
|
||
| res.status(status).json({ message }); | ||
| throw err; | ||
| }); | ||
| const app = await createApp(); | ||
| const server = createServer(app); |
There was a problem hiding this comment.
Instead of creating a redundant server instance with createServer(app), we should destructure and reuse the server instance returned by createApp(). This also allows us to remove the unused createServer import from http.
| import "dotenv/config"; | |
| import { createServer } from "http"; | |
| import { createApp } from "./app"; | |
| import { setupVite, serveStatic, log } from "./vite"; | |
| const app = express(); | |
| // Trust only the first proxy hop; 'true' would let clients spoof | |
| // X-Forwarded-For and bypass IP-based rate limiting | |
| app.set('trust proxy', 1); | |
| app.use(express.json()); | |
| app.use(express.urlencoded({ extended: false })); | |
| app.use((req, res, next) => { | |
| const start = Date.now(); | |
| const path = req.path; | |
| let capturedJsonResponse: Record<string, any> | undefined = undefined; | |
| const originalResJson = res.json; | |
| res.json = function (bodyJson, ...args) { | |
| capturedJsonResponse = bodyJson; | |
| return originalResJson.apply(res, [bodyJson, ...args]); | |
| }; | |
| res.on("finish", () => { | |
| const duration = Date.now() - start; | |
| if (path.startsWith("/api")) { | |
| let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; | |
| if (capturedJsonResponse) { | |
| logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; | |
| } | |
| if (logLine.length > 80) { | |
| logLine = logLine.slice(0, 79) + "…"; | |
| } | |
| log(logLine); | |
| } | |
| }); | |
| next(); | |
| }); | |
| // Local / self-hosted entry point. On Vercel the app is served by | |
| // api/index.ts instead, which imports the same createApp() factory. | |
| (async () => { | |
| const server = await registerRoutes(app); | |
| app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { | |
| const status = err.status || err.statusCode || 500; | |
| const message = err.message || "Internal Server Error"; | |
| res.status(status).json({ message }); | |
| throw err; | |
| }); | |
| const app = await createApp(); | |
| const server = createServer(app); | |
| import "dotenv/config"; | |
| import { createApp } from "./app"; | |
| import { setupVite, serveStatic, log } from "./vite"; | |
| // Local / self-hosted entry point. On Vercel the app is served by | |
| // api/index.ts instead, which imports the same createApp() factory. | |
| (async () => { | |
| const { app, server } = await createApp(); |
| export default async function handler(req: VercelRequest, res: VercelResponse) { | ||
| const app = await appPromise; | ||
| return (app as unknown as (req: VercelRequest, res: VercelResponse) => void)(req, res); | ||
| } |
There was a problem hiding this comment.
Update the handler to destructure app from the resolved appPromise since createApp() now returns { app, server }.
| export default async function handler(req: VercelRequest, res: VercelResponse) { | |
| const app = await appPromise; | |
| return (app as unknown as (req: VercelRequest, res: VercelResponse) => void)(req, res); | |
| } | |
| export default async function handler(req: VercelRequest, res: VercelResponse) { | |
| const { app } = await appPromise; | |
| return (app as unknown as (req: VercelRequest, res: VercelResponse) => void)(req, res); | |
| } |
Summary
Consolidates the repo's four-way deploy-config split onto Vercel and makes the app genuinely deployable there (the previous Vercel setup was broken scaffolding). Follow-up to PR #4.
What changed
Vercel wiring (makes it actually deploy):
server/app.ts— newcreateApp()factory that builds the Express app + middleware + routes without listening. Deliberately does not import./vite, so the serverless bundle stays free of dev-only Vite deps.server/index.ts— local/self-hosted entry now usescreateApp(), and loadsdotenvso a local.envis picked up (it never was before).api/index.ts— mounts the Express app as a single Vercel serverless function; replaces the non-compilingapi/videos.tsplaceholder (which imported from'../server/<wherever-your-code-is>').vercel.json— rewrites/api/*to the function and everything else to the SPA. The old config rewrote all routes toindex.html, which 404'd the entire API.tsconfig.json— includesapi/**so CI typechecks the function.Latent DB bug, surfaced by loading
.envfor the first time:server/db.tstreated any non-temp:temp@localhostDATABASE_URLas a real Neon Postgres URL. Thesqlite:./local.dbplaceholder then made the driver dialwss://./v2and throwENOTFOUNDon every search-cache write. Now onlypostgres://URLs activate the DB; anything else runs DB-less cleanly..env.example—DATABASE_URLcommented out by default (was a bogus URL pointing at a nonexistentlocalhost:5432).Removed (consolidating away from these):
Dockerfile,docker-compose.cache.yml,nginx.conf,.replitdeployment-guide.md(was entirely Docker/nginx, now inaccurate)api/videos.ts(broken placeholder)Kept
replit.md(it's a project changelog, not deploy config) andoptimization-roadmap.md(still-relevant future work).README — replaced the Docker guide with a Vercel deploy section that is honest about serverless limits.
Serverless limitations (documented in README)
/api/video/..., which streams directly from Archive.org without touching disk. Short-to-medium clips are fine; very long videos may hit the function's max duration.npm run build && npm start).Verification
npm run check→ 0 errors (CI added in PR Revive search, drive typecheck to zero, and repo hygiene #4 runs typecheck + build).vite buildand full build pass;npm ciclean.ENOTFOUNDnoise.This branch's base was rewritten. Any existing clone must reset:
🤖 Generated with Claude Code