← All posts

We Moved Our Production App Off Vercel. What Broke.

August 5, 2026

Before we ask anyone to move a production app onto Candor Deploy, we figured we should do it ourselves. So we took codewright — a real application with a React frontend, a FastAPI backend, Postgres, Redis, and paying-attention users — off Vercel and onto our own platform. It’s serving at its own domain right now.

It did not go smoothly. That’s the useful part, so here’s the whole story: what broke, what we built because of it, and what we’d tell you before you try the same move.

Why leave Vercel at all?

Credit first: Vercel is genuinely excellent. The deploy experience is the best in the industry, and most of what we built this month exists because Vercel set the bar for what “it just works” should mean.

The reasons to move were about model, not quality. Vercel’s pricing is usage-based — bandwidth, function invocations, analytics events are metered, and a traffic spike shows up on your bill. Candor Deploy is flat-rate, same number at renewal, because that’s the whole premise of this company. And we’re a hosting company: our apps should run on our own hardware, on the platform we sell, or we have no business selling it.

There’s also an architectural difference that matters for the rest of this story. Vercel runs your app as serverless functions. Candor Deploy runs containers on our own servers. Containers give you a real, always-on process — no cold starts, workers and databases next to your app — but they also surface problems serverless quietly absorbs. We hit every one of them.

What broke, honestly

The deploy hung forever, and our platform said nothing. The backend’s entrypoint was written for Vercel, which discovers your app object itself. Run that same code in a container and it starts, does nothing, and exits — and Kubernetes can’t tell “exited immediately” from “still starting.” Our portal showed “deploying” indefinitely with no error. That was our bug, not the app’s. A platform that can’t tell you why a deploy is stuck isn’t done.

The frontend couldn’t talk to the backend. On Vercel, both halves served from one domain — the frontend at /, the API at /server. Naive container platforms (ours included, at the time) give every service its own domain, which means CORS configuration, a public API URL baked into the frontend at build time, and a class of “invalid credentials” bugs that are actually routing bugs. We hit exactly that.

Build-time environment variables didn’t exist. Frontend frameworks bake NEXT_PUBLIC_*-style variables into the bundle when the app is built. Our platform only injected variables at runtime — so the setting existed in the portal, and did nothing. The kind of gap you only find by shipping a real app.

What we built because of it

Each of those became a platform feature, not a workaround for our app:

  • Deploys fail honestly now. A container that exits or crashes fails the deploy within seconds with the actual reason — “built successfully but exited immediately: set a Start command,” with a framework-specific example, or the exit code and the last log lines. An app that never becomes ready fails with “is your app listening on $PORT?” instead of spinning forever. No silent hangs, and no guessing on your behalf: if we can’t determine how to start your app, we say so and show you the one field that fixes it.
  • One origin, path-routed. A project can serve all its services under a single domain — frontend at /, API at /server, prefix stripped before it reaches your code. Same-origin means no CORS, no baked-in API URLs, and codewright’s browser code ran unmodified.
  • Environment variables reach the build. What you set in the portal is available when your bundle is built, not just when it runs.
  • Your domain points at the project, not a service. Add yourapp.com, set the one DNS record we show you, and the certificate issues itself. The whole app — frontend, API, everything — serves there over HTTPS.

Terminal log of two deploys on Candor Deploy. The first fails out loud: build succeeds in 41 seconds, then "deploy failed — the container exited immediately (exit 0): it isn't starting a long-running server," followed by the fix in green: "Set a Start command, e.g. uvicorn main:app --host 0.0.0.0 --port $PORT." The second deploy builds and goes live at https://codewright.app, with / routed to the frontend and /server to the API.

The end state for codewright: one domain, one DNS record, one flat monthly price, and a deploy pipeline that tells the truth when something’s wrong.

What we’re not claiming

Candor Deploy is in beta, and this migration was our own app — that’s one production data point, deliberately ours before it’s ever yours. Some honest gaps against Vercel today: no preview deployments per branch yet, no global edge network — your app runs in containers in one place, close to your database, which is the right shape for an app with a backend and the wrong shape for a static site that needs to be 20ms from everyone. And a container platform will always ask slightly more of you than serverless does — there’s a real process, and occasionally you’ll need to tell us how to start it.

What you get in exchange is a bill that doesn’t move when your traffic does, an app that runs the same way at 2am as in the demo, and a platform that fails out loud.

If that trade sounds right for something you’re running, Candor Deploy is open in beta — and if you’re coming from Vercel specifically, we’ve been through the exact move and will help with it. That’s not a metaphor; it’s what we did last week.