Skip to content

Consolidate deploy on Vercel (make it actually deployable)#5

Merged
drftstatic merged 1 commit into
mainfrom
claude/repo-evaluation-9114e6
Jul 19, 2026
Merged

Consolidate deploy on Vercel (make it actually deployable)#5
drftstatic merged 1 commit into
mainfrom
claude/repo-evaluation-9114e6

Conversation

@drftstatic

Copy link
Copy Markdown
Owner

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.

Note: this branch sits on top of a history rewrite that purged the committed video-cache/*.mp4 runtime videos from all history (repo ~176 MB → ~80 MB). Existing clones must re-sync — see below.

What changed

Vercel wiring (makes it actually deploy):

  • 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(), and 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 (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 to index.html, which 404'd the entire API.
  • tsconfig.json — includes api/** so CI typechecks the function.

Latent DB bug, surfaced by loading .env for the first time:

  • server/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 cleanly.
  • .env.exampleDATABASE_URL commented out by default (was a bogus URL pointing 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.

Serverless limitations (documented in README)

  • Search and video proxying work. Video plays through /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.
  • Disk video cache, transcoding, and HLS routes do not work on Vercel (no persistent filesystem). They're opt-in extras, not on the default playback path. For those, self-host (npm run build && npm start).

Verification

⚠️ Action required: re-sync local clones

This branch's base was rewritten. Any existing clone must reset:

git fetch origin
git checkout main && git reset --hard origin/main
# feature branches: rebase onto the new main, or re-clone

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
staticbuffet Ready Ready Preview, Comment Jul 19, 2026 1:47am

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/app.ts
Comment on lines +33 to +45
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);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
      }
    });

Comment thread server/app.ts
Comment on lines +50 to +59
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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 };
}

Comment thread server/index.ts
Comment on lines +1 to +10
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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();

Comment thread api/index.ts
Comment on lines +9 to +12
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the handler to destructure app from the resolved appPromise since createApp() now returns { app, server }.

Suggested change
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);
}

@drftstatic
drftstatic merged commit 9394659 into main Jul 19, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant