Deploying a Full-Stack App: Vercel, Render and Supabase
Deployment is where a project stops being code and starts being a system. Most surprises are configuration problems in disguise.
By Uttam Thapa · · Deployment
⚡ Executive Summary (TL;DR)
Deployment is where a project stops being code and becomes a system, and nearly every surprise traces back to the environment differing from the one you built in. The fixes are unglamorous and permanent: validate configuration at startup so a bad deploy fails immediately, remember that frontend variables are baked in at build time and need a rebuild rather than a restart, normalise CORS origins instead of trusting dashboard input, and generate your sitemap rather than maintaining it by hand.
Figure 1: Frontend, API and database each fail differently at deploy time — configuration is the common cause.
Introduction
Deployment is where a project stops being code and starts being a system. It is also where most of the surprises happen, because the environment differs from the one everything was built and tested in.
This article covers the deployment setup I use for full-stack applications, and the problems that appear consistently across projects.
The Split
For most applications I separate the frontend, the API and the database across three platforms.
Frontend -> Vercel (static build, edge delivered)
Backend -> Render (long-running Node process)
Database -> Supabase (managed PostgreSQL)
Each part is deployed independently, which keeps failures isolated and makes rollbacks straightforward. A broken API deploy does not take the marketing pages down with it.
Environment Variables
This is the single largest source of production-only bugs.
The Pattern That Prevents Them
- Keep a committed example file listing every required variable.
- Validate all of them at startup and refuse to boot if any are missing.
- Fail with a message that names the missing variable.
Without validation, a missing variable surfaces as a 500 error the first time a user triggers the code path that needs it, which may be days after deploy. With validation, it fails during deployment where it is obvious.
Build-Time Versus Runtime
This distinction matters and is easy to miss.
- Frontend variables are baked into the bundle at build time. Changing one requires a rebuild, not a restart.
- Backend variables are read at runtime. Changing one requires a restart.
Updating a frontend variable in a dashboard and expecting it to take effect without redeploying is a common false start.
The Devil Is in the Trailing Slash
CORS origins are compared as exact strings, and browsers send the origin without a trailing slash.
Configured: https://example.com/
Browser sends: https://example.com
Result: no match, request blocked
The defensive fix is to normalise the value when reading configuration, rather than relying on it being entered perfectly in a dashboard.
Dev Dependencies in Production Builds
A TypeScript service has to be compiled during deploy, which means the build needs TypeScript and the type packages. Those are correctly declared as dev dependencies.
The problem is that most hosts set the environment to production, and package managers skip dev dependencies in that mode. The build then fails with a missing type definition or a missing compiler.
Options
- Configure the install step to include dev dependencies explicitly.
- Commit an npmrc that forces it, so it works without dashboard configuration.
- Move the compiler into runtime dependencies, which works but is semantically wrong.
The second option is usually cleanest because it travels with the repository.
Client-Side Routing and Rewrites
A single-page application serves one HTML file, and the router handles the rest in the browser. A static host does not know this by default.
Visiting the site root works. Refreshing on a nested route returns a 404, because there is no file at that path.
The fix is a rewrite rule sending all paths to the index file. Without it, every deep link and every refresh on an internal page fails, which is easy to miss in testing because navigating within the app works fine.
Cold Starts
Free and low tier backend hosting usually spins the service down when idle. The first request afterwards has to wait for it to start.
Practical mitigations:
- Set generous timeouts on the client for the first request.
- Show a loading state rather than appearing frozen.
- Use a health endpoint for uptime checks if the tier allows it.
- Move to a paid tier once real users are involved.
Database Connections
Managed PostgreSQL has a connection limit that is lower than most people expect, and each application instance opens its own pool.
- Use the pooled connection string where the provider offers one.
- Limit pool size per instance.
- Retry the initial connection with backoff rather than crashing on a transient failure.
A connection retry loop at startup is worth the small amount of code. Databases occasionally take a moment to accept connections after a platform restart, and without a retry the service dies instead of waiting.
Generating Deployment Artefacts
Files like a sitemap and a robots file are easy to write by hand and easy to let drift.
Generating them during the build from the same route data the application uses removes an entire category of mistake. A new page is added once, and the sitemap includes it automatically.
The same applies to the canonical domain. Keeping it in one constant that the build reads means a domain change is a single edit rather than a search across static files.
A Deployment Checklist
- Every environment variable set on every platform.
- Frontend rebuilt after changing any build-time variable.
- CORS origin matches exactly, without a trailing slash.
- Rewrite rule in place for client-side routes.
- Health endpoint responding.
- Database reachable, with connection retry in place.
- Error responses returning JSON rather than an HTML error page.
- No credentials in the repository or its history.
Key Takeaways
- ✓Validate configuration at startup so failures happen at deploy time.
- ✓Remember that frontend variables need a rebuild, not a restart.
- ✓Normalise origins rather than trusting dashboard input.
- ✓Make the build self-sufficient so it does not depend on host settings.
- ✓Generate sitemap and robots files rather than maintaining them.
Most deployment problems are configuration problems, and most configuration problems can be turned into a startup error instead of a runtime one.
Frequently asked questions
Why do my environment variables not work after deployment?
Frontend variables are compiled into the bundle at build time, so changing them requires a rebuild rather than a restart. Backend variables are read at runtime and do take effect on restart. Mixing the two models up is the most common deployment surprise.
How do you catch bad configuration before users do?
Validate every required variable at startup and refuse to boot without them. A missing key should fail loudly at deploy time, not produce a confusing 500 for the first customer who hits that code path.
What causes CORS errors in production but not locally?
The deployed frontend origin is not in the allowlist, often because of a trailing slash or a protocol mismatch. Normalise origins in code rather than trusting exactly what was typed into a hosting dashboard.
Home · Projects · Blog · Services · Résumé · Contact