What maps to what
| On Vercel | On the box |
|---|---|
| Deploy on git push | A GitHub App per box: every push to the production branch deploys (GitHub) |
| Preview deployments | One per pull request at its own address, with its own copy of the database; removed when the pull request closes |
| Deployment URLs | Every production deploy keeps an address, d-<id>--<name>, read-only and behind your dashboard sign-in by default |
| Instant Rollback | tiffin rollback <app>, to any of the last 20 production deploys |
| Environment variables | Plain values in tiffin.config.ts, secrets with tiffin secrets set or the dashboard |
| Cron jobs (vercel.json) | Run by the box: a GET to the path with CRON_SECRET, as Vercel calls it (jobs) |
| Function duration | timeoutSeconds: 15 minutes by default, up to 24 hours |
| ISR and revalidateTag | Next.js's cache, shared by every instance through the project's KV |
| Vercel Postgres, Neon | Postgres 18 for each project, behind a pooler, with branches (data) |
| Vercel KV, Upstash Redis | @vercel/kv and @upstash/redis run unchanged against a compatible endpoint on the box |
| Vercel Blob | S3-compatible buckets; code that calls @vercel/blob moves to an S3 client |
| Workflow DevKit | Runs unchanged, on the project's Postgres |
| Web Analytics | Cookieless page views counted at the box's edge, with no script for page views |
| Observability | Sentry-compatible error tracking, logs and traces; @vercel/otel sends to the box |
| Spend management | A flat fee, and a hard limit per project for memory, CPU, database and KV |
| Team seats | People are free |
| Domains and HTTPS | tiffin domains add, with Let’s Encrypt certificates (domains) |
| Firewall | Rate limits, a proof-of-work bot check, CrowdSec and a firewall, on by default (protection) |
| Edge network and regions | One server, in the place you picked |
1. Get a box
A box is one server in your own Hetzner account with Tiffin on it. Get one managed at shiptiffin.com/start ($19 a month, plus the server at Hetzner’s price), or make one yourself for free with tiffin up. Both are step 1 of Deploy Next.js to Hetzner. One box holds many projects, so several Vercel projects can move to the same one.
2. Bring the app
From GitHub
In the dashboard, Settings › Git › Connect GitHub, then New project › Import from GitHub. Pick the repository, its production branch and the app’s folder (monorepos list each app found, with the framework the box would use), add environment variables and create. From then on it works like Vercel: every push to the branch deploys, and every pull request gets a preview with one comment kept up to date.
From your computer
$ tiffin init $ tiffin plan $ tiffin apply --confirm <hash> -m "Move from Vercel" $ tiffin deploy
What the box reads
- vercel.json in the app’s folder, at every deploy. The build log lists what was taken and what wasn’t used, and
tiffin plansays the same. A broken vercel.json fails the deploy, saying why; the live version keeps serving. - Monorepos. An app in a JavaScript workspace goes up with the whole workspace, as on Vercel. Dependencies install at the top with the workspace’s package manager, only for the app, the workspace packages it uses and the root; then the app builds and starts in its own folder. If that install fails, the box installs the whole workspace instead.
- Static exports. A Next.js app with
output: "export"builds with its ownnext build, and the edge servesout/as a static site, with no container.
| vercel.json | On the box |
|---|---|
buildCommand, installCommand | Replace the build's own: build in the app's folder, install at the top of its workspace |
outputDirectory | The folder a static site or export serves |
crons | Crons that call the app with GET and its CRON_SECRET, as Vercel does |
headers | Set at the edge on matching responses; they win over the app's own and the edge's defaults |
redirects | Answered at the edge, query string kept; permanent: false is 307, statusCode is kept |
rewrites | Static sites: another path of the site answers when no file matches |
cleanUrls, trailingSlash | Static sites: /page.html redirects to /page; paths get (or lose) their trailing slash |
/blog/:slug, /docs/:path*). For a Next.js server app, rewrites, cleanUrls and trailingSlash stay with the app, and next.config’s own redirects, rewrites and headers run inside it as before. From Coming from Vercel.3. Copy environment variables
Read them from your Vercel project’s settings and set each one on the box. Secrets are stored encrypted, and setting one is a change in History you can undo:
$ tiffin secrets set hello STRIPE_SECRET_KEY --value "$STRIPE_SECRET_KEY" $ tiffin secrets list hello # names only, never values
Plain values that aren’t secret can live in the app’s config instead:
apps: {
web: { framework: "next", env: { LOG_LEVEL: "info" } },
},- Leave out what the box sets. Every app gets
DATABASE_URL,REDIS_URL, the Upstash and Vercel KV variables,S3_*,SMTP_URL,SENTRY_DSNand more. A value you set yourself wins over the box’s, so don’t copy the old ones across. If your code readsPOSTGRES_URL, point it atDATABASE_URL. - Builds see the same env as the app, as on Vercel, so prerendered pages can query the database. At build time the database login is read-only.
- Browser variables (
NEXT_PUBLIC_*) are built into client code, so changing one rebuilds the app. - Vercel’s own variables. The box sets
VERCEL_PROJECT_PRODUCTION_URLto the app’s host, soopengraph-imageand other metadata resolve without ametadataBase.VERCELandVERCEL_URLstay unset, since libraries take them to mean the app runs on Vercel.
The full list is in what your app gets.
4. Move your data
- Postgres (Vercel Postgres, Neon, Supabase or another host): dump it with
pg_dump, open a tunnel withtiffin db tunnel hello, and restore into thepostgresql://URL it prints withpg_restoreorpsql. The tunnel needs an API key with full access to the project. - KV (Vercel KV, Upstash): apps that use
@vercel/kv,@upstash/redisor@upstash/ratelimitrun unchanged;Redis.fromEnv()picks up the box’s values. Delete the old Upstash values from your env, since yours would win. The box’s KV starts empty. - Files (Vercel Blob): every project has S3-compatible buckets. Copy your files with any S3 tool and move upload code to
Bun.s3or an AWS SDK. See files. - Workflows built on the Workflow DevKit deploy unchanged and run on the project’s Postgres. See jobs.
$ tiffin sql hello "select count(*) from users" # one read-only statement
5. Test on the box's address
Before DNS moves, the app is already live at https://hello.<box domain>, with a real certificate. Sign in, run the flows that matter, and check the logs with tiffin logs web -f. tiffin queue crons list shows each cron and whether it came from tiffin.config.ts or vercel.json.
If your app has its own sign-in (Auth.js, Clerk, your own OAuth apps), add the new host to each provider’s allowed redirect URIs. If you turn on the box’s sign-in instead, it answers /api/auth/* on every app address, so routes of your own there stop reaching your app.
6. Switch the domain
$ tiffin domains add hello --domain example.com --app web --www # shows the plan, then lists the DNS records to add $ tiffin domains list hello # waiting_for_dns, issuing, then live
- A day ahead, lower the TTL on the records you’ll change, so the switch spreads quickly.
- Add the domain on the box (above). It lists the records: an A (and AAAA) record to the server, or one CNAME for a subdomain.
- Change the records at your DNS host. The box checks again on its own and gets a Let’s Encrypt certificate a few seconds after the records point at it.
- Leave the Vercel project running until the domain shows
liveand traffic has moved, then remove the domain there.
More in domains.
What doesn't carry over
- One server, one place. There is no global edge network and no choice of regions per function. If the server has a hardware fault, your apps are down until it’s back. Tiffin is pre-1.0.
- Some vercel.json is ignored, and the build log lists it: rules with
hasormissing, rewrites to another site, and every other key (functions,regions,framework...). - Vercel products with their own APIs, such as Blob and Edge Config, have no stand-in that speaks those APIs. Move that code to buckets or KV.
- Databases from outside. Postgres and KV are reachable only from inside the box (and through
tiffin db tunnel). A frontend that stays on Vercel can’t connect to them directly: run a small API app on the box instead. - Caching and images. Next.js’s own cache works across instances; there is no edge response cache for other frameworks yet, and each app optimises its own images.
- Other frameworks’ Vercel adapters. Astro and SvelteKit with the Vercel adapter aren’t supported: switch to
@astrojs/node, or a Node or Bun adapter. - Previews. Pull requests from forks aren’t built unless the app says
previews: "forks". A preview gets its own database copy but shares the cache, buckets, secrets and real user accounts with production.
Moving makes sense when
- You run several apps, and each needs a database, sign-in or jobs
- Usage charges, seats or per-project fees keep growing
- You want your apps and data in an account you own
Vercel is the better fit when
- Your users are worldwide and you need the edge network or many regions
- An app must stay up through a hardware fault
- You lean on Vercel-only products like Blob or Edge Config
- You don't want a server at all, not even one we look after
Checklist
- The box is ready and
tiffin whoamianswers. - The app deploys on the box, and the build log shows what it took from vercel.json.
- Secrets are set, and old database, Upstash and KV values are left out.
- The database is restored, and
tiffin sqlshows your rows. - Files are in a bucket, and Blob code uses an S3 client.
- Crons show in
tiffin queue crons list. - Sign-in works on the box’s address, with redirect URIs updated.
- An email provider is connected in Settings › Email, if the app sends mail.
- The domain is added, DNS is changed, and
tiffin domains listsayslive. - The domain is removed from Vercel once traffic has moved.