Guide

Moving your apps off Vercel

A Tiffin box reads what your app already has, so most Next.js apps that deploy on Vercel deploy here unchanged. This guide covers what carries over, what you move by hand, and what stays behind. For prices side by side, see ShipTiffin vs Vercel.

Updated

In short

Most Next.js apps move from Vercel to a Tiffin box with no code changes. The box reads vercel.json at every deploy (build commands, crons, headers, redirects), installs monorepos the way Vercel does, and gives the app the variables Next.js expects.

You copy your environment variables, move your data, test on the box’s own address, then point DNS at the server. Vercel’s global edge network, regions and products with their own APIs, such as Blob, don’t come along.

What maps to what

Vercel features and what replaces them on a Tiffin box
On VercelOn the box
Deploy on git pushA GitHub App per box: every push to the production branch deploys (GitHub)
Preview deploymentsOne per pull request at its own address, with its own copy of the database; removed when the pull request closes
Deployment URLsEvery production deploy keeps an address, d-<id>--<name>, read-only and behind your dashboard sign-in by default
Instant Rollbacktiffin rollback <app>, to any of the last 20 production deploys
Environment variablesPlain 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 durationtimeoutSeconds: 15 minutes by default, up to 24 hours
ISR and revalidateTagNext.js's cache, shared by every instance through the project's KV
Vercel Postgres, NeonPostgres 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 BlobS3-compatible buckets; code that calls @vercel/blob moves to an S3 client
Workflow DevKitRuns unchanged, on the project's Postgres
Web AnalyticsCookieless page views counted at the box's edge, with no script for page views
ObservabilitySentry-compatible error tracking, logs and traces; @vercel/otel sends to the box
Spend managementA flat fee, and a hard limit per project for memory, CPU, database and KV
Team seatsPeople are free
Domains and HTTPStiffin domains add, with Let’s Encrypt certificates (domains)
FirewallRate limits, a proof-of-work bot check, CrowdSec and a firewall, on by default (protection)
Edge network and regionsOne server, in the place you picked
From Coming from Vercel and the pages linked in the table. The full list of gaps is what works and what doesn’t.

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

In the app's folder
$ 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 plan says 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 own next build, and the edge serves out/ as a static site, with no container.
vercel.json keys and what the box does with them
vercel.jsonOn the box
buildCommand, installCommandReplace the build's own: build in the app's folder, install at the top of its workspace
outputDirectoryThe folder a static site or export serves
cronsCrons that call the app with GET and its CRON_SECRET, as Vercel does
headersSet at the edge on matching responses; they win over the app's own and the edge's defaults
redirectsAnswered at the edge, query string kept; permanent: false is 307, statusCode is kept
rewritesStatic sites: another path of the site answers when no file matches
cleanUrls, trailingSlashStatic sites: /page.html redirects to /page; paths get (or lose) their trailing slash
Sources use Vercel’s patterns (/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:

Secrets
$ 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:

tiffin.config.ts
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_DSN and more. A value you set yourself wins over the box’s, so don’t copy the old ones across. If your code reads POSTGRES_URL, point it at DATABASE_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_URL to the app’s host, so opengraph-image and other metadata resolve without a metadataBase. VERCEL and VERCEL_URL stay 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 with tiffin db tunnel hello, and restore into the postgresql:// URL it prints with pg_restore or psql. The tunnel needs an API key with full access to the project.
  • KV (Vercel KV, Upstash): apps that use @vercel/kv, @upstash/redis or @upstash/ratelimit run 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.s3 or an AWS SDK. See files.
  • Workflows built on the Workflow DevKit deploy unchanged and run on the project’s Postgres. See jobs.
Check the data arrived
$ 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

Move 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
  1. A day ahead, lower the TTL on the records you’ll change, so the switch spreads quickly.
  2. 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.
  3. 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.
  4. Leave the Vercel project running until the domain shows live and 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 has or missing, 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 whoami answers.
  • 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 sql shows 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 list says live.
  • The domain is removed from Vercel once traffic has moved.

All your Vercel projects, on one box

$19 a month per box, plus your server at Hetzner’s price. No usage bills or seats.