next start, a PostgreSQL database in place of a Marketplace Postgres database such as Neon, an object storage bucket in place of Vercel Blob, and a cron job for each entry in vercel.json’s crons. You change a few files, recreate the setup while Vercel keeps serving, copy the data during a short maintenance window, then point the domains at Sevalla.
At a glance
Environment variables
How Vercel maps to Sevalla
Before you begin
- The Vercel CLI, signed in, and the project linked in a checkout of its repository (
vercel link). - The code in a repository on GitHub, GitLab or Bitbucket. Sevalla doesn’t deploy from a folder the way
vercel deploydoes. See Git providers. - A Sevalla company with a payment method.
- The PostgreSQL client tools, at the same major version as the source database or newer: Neon runs PostgreSQL 18 today. See Import and export PostgreSQL.
- Node.js 20 or later, for the script that copies the Blob files.
- A maintenance window: the Vercel project is paused while you copy the data.
Take stock of the Vercel project
Collect what you’ll recreate, from the root of the repository:integration listshows the Marketplace resources, such as a Neon database, andblob list-storesthe Blob stores with their file counts.crons listandvercel.jsonshow the cron jobs, redirects, rewrites and headers to recreate.vercel.envholds the production values. Secret values can’t be read back, so the file has"[SENSITIVE]"in their place, and the CLI says how many:! 2 Secret values cannot be pulled from the `production` Environment.Get those values from where you keep them, or create new ones. The file contains secrets: don’t commit it.- The
greplists the files that use a Vercel SDK or the Neon serverless driver: the next section changes them. - Vercel runs functions in
iad1(Washington, D.C., USA) unless you changed the project’s function region. Choose the Sevalla location closest to it, such as Ashburn, USA. See Locations. - Check the database version with
psql "$(sed -n 's/^DATABASE_URL_UNPOOLED=//p' vercel.env | tr -d '"')" -Atc 'show server_version'. Create the Sevalla database with the same version or a newer one.
Prepare the code for Sevalla
Make these changes on a new branch, heresevalla, so that Vercel keeps deploying your production branch until the switch. Each one replaces something only Vercel provides.
Switch the database driver
The Neon serverless driver (@neondatabase/serverless) sends queries over HTTP to Neon’s proxy, which a Sevalla PostgreSQL database doesn’t have. Postgres.js has the same tagged-template API, so only the import changes:
sql`SELECT id, name FROM products ORDER BY id` return the same rows. Code that already uses a standard PostgreSQL driver, such as pg, needs only the new DATABASE_URL.
Move uploads from Vercel Blob to the S3 API
Replaceput() from @vercel/blob with the AWS SDK’s PutObjectCommand, and build the file’s URL from the bucket’s public domain:
crypto.randomUUID() keeps names unique, as addRandomSuffix did. For the other Blob calls, list() becomes ListObjectsV2Command, del() DeleteObjectCommand, and a private file’s get() a presigned URL. See Use object storage from Node.js.
Read the country from Cloudflare headers
geolocation() and ipAddress() read headers that only Vercel sets. Every request to Sevalla passes through Cloudflare, which sets CF-IPCountry and CF-Connecting-IP:
npm uninstall @vercel/functions. Cloudflare doesn’t send a city or a region, only the country. See Request headers.
Move the vercel.json rules into next.config.ts
Sevalla doesn’t readvercel.json. Its redirects, rewrites and headers have the same fields in next.config.ts, so copy each list into a function and delete vercel.json. Add the bucket’s domain to images.remotePatterns in place of Vercel Blob’s:
permanent: true answers 308, as on Vercel. A redirect from one domain to another, which Vercel sets in the project’s domain settings, becomes a redirect with a has host condition.
Add a script for the cron jobs
Vercel Cron Jobs send aGET request to a path with Authorization: Bearer <CRON_SECRET>. A Sevalla cron job runs a command in a new instance of the application, so this script sends the same request to the web process:
CRON_SECRET check. Vercel also sends the x-vercel-cron-schedule header; if your route reads it, pass the schedule another way.
Commit the changes, package-lock.json included, and push the branch:
Create the services
Create the PostgreSQL database
storefront-db as the Display name, choose the Data center the application will use and a Size with room for the data, then click Create database. See Create a database.Create the bucket
storefront-files as the Name, choose the Location closest to the application, and click Create object storage.Make the bucket public
storefront-files-x7k2m.sevalla.storage. Keep files from a private Blob store in a private bucket. See Public access.Create the application
sevalla Branch. Under Location, choose the same Data center as the database, choose S1 or a larger Size if you’ll move a custom domain, and click Create application. The application opens with the status Not deployed yet: don’t deploy it before the data is copied, because next build prerenders the ISR pages from the database.Connect the database
On the application’s Networking tab, under Connected services, click Add internal connection. Choosestorefront-db as the Service and turn on Add environment variables to the application. Keep Available during runtime and Available during buildtime checked, change the key DB_URL to DATABASE_URL, and click Add internal connection. See Connect an application.
The URL starts with postgres:// and has no sslmode: the connection stays inside the data center. Code that forces TLS for Neon, such as ssl: "require" in the driver options, must drop it.
Move the environment variables
Clean up the exported file
vercel env pull adds, which the dashboard would keep as part of the value:sevalla.env, replace each [SENSITIVE] with the real value, and any value that contains the Vercel domain with the application’s sevalla.app domain. Code that reads VERCEL_URL or VERCEL_ENV needs a variable of your own instead. Then add the bucket’s values and the two that the cron script reads:APP_URL is the web process’s internal hostname, shown on the application’s Networking tab under Internal hostnames, with the web process port: the cron job calls the web process without leaving the data center.Import the file
sevalla.env. Keep Available during runtime and Available during buildtime selected, and click Add environment variables.PORT: Sevalla sets it. Variables of the Preview environment belong to the preview apps, in Set up preview deployments. Development values stay in your local .env.local.
Recreate the cron jobs
For each entry invercel.json’s crons, click Create process on the application’s Processes tab. Choose Cron job, enter a Name, enter node scripts/cron.mjs followed by the entry’s path as the Custom start command, and copy its schedule into Schedule. Both platforms read the schedule in UTC. Choose the smallest Resource, H1, and click Create process.
Copy the PostgreSQL data
Stop writes on Vercel
503 and DEPLOYMENT_PAUSED, and stops the cron jobs from writing:curl -X POST "https://api.vercel.com/v1/projects/<project-id>/pause" -H "Authorization: Bearer $VERCEL_TOKEN".Dump the database
DATABASE_URL_UNPOOLED, which Neon recommends for pg_dump, and leave out schemas that belong to Neon, such as neon_auth, which Neon Auth creates:Open the Sevalla database to your computer
storefront-db. Click the Networking tab, click Enable under External connection, then click Enable in the dialog. Copy the External hostname, External port and password. See Enable the external connection.Restore the dump
Compare the tables
sevalla.app domain. For the real cutover, add --clean --if-exists to pg_restore, so it replaces the rehearsal’s tables.
Copy the Vercel Blob files
Copy the files
vercel.env and the bucket’s values. It copies every blob to the same path in the bucket, with its content type:Copied 6 files, which matches the Files column of vercel blob list-stores. The bucket’s Objects tab lists them.Point the stored URLs at the bucket
put() returned, on the store’s public.blob.vercel-storage.com domain. Replace that domain with the bucket’s in every column that stores one:Deploy and verify
Deploy
Check the deployment
next build prerendered, such as └ ○ /products 1m 1y for an ISR page, then 🚀 Web has been deployed successfully. and 🚀 Cron /api/cron has been deployed successfully.Compare the responses
x-visitor-country header that proxy.ts sets shows your country.Run a cron job
200 {"ok":true,"run":{"id":2,"ran_at":"2026-10-30T11:14:50.535Z"}}. A 401 means CRON_SECRET differs from the value the route checks.Turn on auto-deploy
How Next.js features run on Sevalla
next start supports every Next.js feature, so the application runs as on Vercel. What changes is where some of the code runs.
Proxy and middleware
proxy.ts (middleware.ts before Next.js 16) runs in the next start server of each instance, in the application’s location, before the request reaches the page. On Vercel it runs in the region closest to the visitor. In a test from Central Europe, a redirect from proxy.ts took 97 ms on Vercel and 191 ms from an application in Ashburn, USA, after the TLS handshake. A route handler that queries the database took about 205 ms on both, because Vercel ran it in iad1 too. For visitors far from your location, cache public pages with edge caching.
Edge runtime routes
Sevalla has no Edge Functions. A route withexport const runtime = "edge" still works: next start runs it in the same server, in an Edge runtime sandbox, and next build warns The Edge Runtime is deprecated. You can use the "nodejs" runtime instead. Remove the line to run the route on Node.js.
Image optimization
next/image works without configuration: next start resizes images and caches the results on the instance. Each remote host needs an entry in images.remotePatterns; without one, the image request answers 400 with "url" parameter is not allowed.
ISR with more than one instance
With one instance, ISR works as on Vercel. Each instance keeps its own Next.js cache, though, so with two or more instances, a page that one instance regenerates stays old on the others.revalidatePath() and revalidateTag() only reach the instance that handles the call.
In a test with two instances and export const revalidate = 60, a price changed in the database, and a call to a route that runs revalidatePath("/products") reached one instance. Of the next 20 requests, 11 showed the new price and 9 the old one, from the other instance. That instance showed the new price only after its own revalidate period ran out, 38 seconds later. A page that relies only on on-demand revalidation would stay old there until the next deployment.
Keep the web process at one instance, or share the cache as in Share the Next.js cache between instances.
Share the Next.js cache between instances
A cache handler stores the Next.js cache in Redis, where every instance reads and revalidates the same entries. These steps use the Redis cache handler from the Next.js examples.Create the Redis database
storefront-cache as the Display name, and choose the application’s Data center.Connect it
storefront-cache with Add environment variables to the application on. Its URL is already called REDIS_URL.Add the cache handler
Add instances
2 or more, click Update process, then deploy. See Set the number of instances.revalidatePath() call showed the new price. The handler connects only at runtime, so next build doesn’t need Redis. If Redis is down, pages render without the cache and revalidation calls fail with Redis is unavailable, so the tag revalidation was not recorded. The handler covers ISR pages and route handlers; for 'use cache' with Cache Components, the same example has a cacheHandlers entry.
Set up preview deployments
Vercel builds a preview deployment for every branch and pull request. On Sevalla, put the application in a pipeline and turn on preview apps: each pull request gets its own application, deployed on every push and deleted when the pull request closes.- In the sidebar, click Applications, click Create, then click Pipeline. Keep Trunk-based development and click Create pipeline. In the Production stage, click Add app, pick Existing app and
storefront, and click Add app to stage. See Create a pipeline. - Turn on preview apps as in Enable preview apps, with the same repository, location and Ingot build type, and a Cron process if previews need one.
- Add the Preview environment’s variables in the settings’ Environment variables section. Get them with
vercel env pull vercel-preview.env --environment=previewand clean them up as in Move the environment variables.
DATABASE_URL in the preview settings, or add an internal connection on each preview app’s Networking tab. Preview apps are public on their sevalla.app domain, where Vercel’s Deployment Protection asked for a sign-in.
Move the custom domains
Add each domain
shop.example.com, and click Add custom domain.Verify it
TXT records that Sevalla shows (_cf-custom-hostname and _acme-challenge) at your DNS provider. Vercel keeps serving the domain meanwhile. If the domain uses Vercel’s nameservers, add them with the Vercel CLI, for example:CAA record for Let’s Encrypt to the domain, and Sevalla’s certificates come from Google Trust Services, so the second record lets them be issued. When the domain’s status changes to Pointing, its certificate is ready. See Verify the domain.Point it at Sevalla
A record to 76.76.21.21 or a CNAME to a vercel-dns target) with the A record that Sevalla shows. On Vercel DNS, add it with vercel dns add example.com shop A <address shown>: a record you add overrides the one Vercel creates for the project. The status changes to Active when Sevalla sees it. See Point the domain.Shut down the Vercel project
When the domains are Active and traffic reaches Sevalla:- Disable the external connection: on the database’s Networking tab, click Disable under External connection, then click Disable in the dialog.
- Merge the
sevallabranch into your production branch, and choose that branch in the application’s Auto-deploy settings. - Remove the domain from the Vercel project’s Domains settings. Keep it in your Vercel account while its DNS is on Vercel’s nameservers: removing it from the account with
vercel domains rmremoves its DNS records too. - Keep
vercel.dumpuntil you no longer need it. Then delete the Vercel project withvercel project delete storefront, empty the Blob store withvercel blob empty-store <store-id>and delete it withvercel blob delete-store <store-id>(a store that still has files can’t be deleted:Blob store not empty), delete the Neon database withvercel integration resource delete <name>, and remove the integration withvercel integration remove neon. Deleting the store or the database deletes their data.
Troubleshoot
Deploy with the API
This sequence does what Create the services to Deploy and verify do in the dashboard, with the Sevalla API and Ingot: it creates the database, the bucket and the application, adds the variables and the cron jobs, copies the data and the files, and deploys. A script or an AI agent runs it as is, in one shell, from the root of the repository, with thesevalla branch pushed and main the production branch that still has vercel.json. Each step names the CLI command and the Sevalla MCP server tool that do the same. You need:
- An API key with the Company developer role, in
SEVALLA_API_KEY. - The repository on GitHub, and a GitHub account with access to it connected to Sevalla. Without access, the step that creates the application fails with
400andCannot access repository https://github.com/acme/storefront: Repository not found or you don't have access. - The Vercel CLI, signed in and linked,
copy-blob.mjsfrom Copy the Vercel Blob files in a folder of its own, here../copy-blob, with its packages installed, andcurl,jq,openssl,pg_dump,pg_restoreandpsql.
[SENSITIVE] in sevalla.env with the real value:
Choose the location
listClustersV3) returns every location with its region ID in location. Save Ashburn, USA, the one next to Vercel’s default iad1:sevalla resources clusters. MCP: get_platform_options.Look up the cron job size
listProcessResourceTypesV3). Save H1, the smallest, for the cron jobs:sevalla resources process-types. MCP: get_platform_options.Look up the database size
listDatabaseResourceTypesV3). Pick a size with room for the data; here DB1, the smallest:sevalla resources db-types. MCP: get_platform_options.Look up the PostgreSQL version
listDatabaseVersionsV3) returns the versions the location offers, oldest first. Save the newest, which is at least as new as Neon’s:sevalla resources db-versions --cluster "$CLUSTER_ID". MCP: not needed, create_database takes the newest version by default.Create the PostgreSQL database
createDatabaseV3). You choose the password, and the API never returns it, so keep it for the connection URL:sevalla databases create --name storefront-db --type postgresql --db-version "$PG_VERSION" --cluster "$CLUSTER_ID" --resource-type "$DB_SIZE_ID" --db-name storefront --db-user storefront --db-password "$PG_PASSWORD". MCP: create_database.Create the bucket
createObjectStorageV3) with public access, in enam (Eastern North America). The response has the bucket’s name, public domain, endpoint and keys:storefront-files-x7k2m.sevalla.storage. CLI: sevalla object-storage create --name storefront-files --location enam, then sevalla object-storage cdn-domain enable <bucket-id>. MCP: sevalla_api_create with createObjectStorageV3 and the same body.Wait for the database
getDatabaseV3) shows status ready when the database is up, with its internal hostname and port:ready. CLI: sevalla databases get "$PG_ID". MCP: get_database.Create the application
createApplicationV3) creates the application with a web process at S1, which can have custom domains, and doesn’t deploy it. Its name makes up the web process’s internal hostname:sevalla apps create --name storefront --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository "$REPO" --branch "$BRANCH". MCP: create_application with deploy: false.Connect the database
createApplicationInternalConnectionV3):DATABASE_URL. CLI: sevalla apps internal-connections create "$APP_ID" --target-id "$PG_ID" --target-type database. MCP: sevalla_api_create with createApplicationInternalConnectionV3 and the same body.Add the environment variables
createEnvironmentVariableV3), once per variable: DATABASE_URL in the format the internal connection uses, the bucket’s values, CRON_SECRET and APP_URL for the cron script, then every line of sevalla.env. CRON_SECRET gets a new value unless sevalla.env has one:sevalla apps env-vars create "$APP_ID" --key <key> --value <value>, once per variable. MCP: set_environment_variables with all the variables in one call.Create the cron jobs
createProcessV3) with type cron, once per entry in the crons of vercel.json, read from main:sevalla apps processes create "$APP_ID" --name "Cron /api/cron" --type cron --resource-type "$H1_ID" --command "node scripts/cron.mjs /api/cron" --schedule "0 5 * * *". MCP: sevalla_api_create with createProcessV3 and the same body.Open the database to your computer
toggleDatabaseExternalConnectionV3) turns it on. Then Get database returns external_hostname and external_port once it’s ready:External connection toggle started. The hostname appears before the database accepts connections on it, so the last loop waits for pg_isready. Keep the address in a URL rather than in PGHOST and PGPORT: psql and pg_dump apply those to every connection without its own port, including Neon’s. CLI: sevalla databases connections external-toggle "$PG_ID". MCP: sevalla_api_create with toggleDatabaseExternalConnectionV3.Copy the data
next build can’t prerender the ISR pages.Copy the files
copy-blob.mjs with the Blob token and the bucket’s values:Copied 6 files.Point the stored URLs at the bucket
products.image_url and uploads.url:Deploy
triggerDeploymentV3) deploys the latest commit of the branch:sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.Wait for the deployment
getDeploymentV3) reports the status, which ends as success, failed, cancelled or skipped. The loop polls for up to 30 minutes:sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.Read the build log
getApplicationDeploymentLogsV3) returns the build log, oldest line first. When the status is failed, the last lines say why; limit=5000, the most it allows, returns the whole log:sevalla apps deployments logs "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment_logs.Check the application
listApplicationDomainsV3) returns the application’s domain. Request the products API from it:sevalla apps domains list --app-id "$APP_ID". MCP: get_application.Run a cron job
triggerProcessCronV3) starts one run now, and List cron runs (listProcessCronRunsV3) reports it:sevalla apps processes trigger "$CRON_ID" --app-id "$APP_ID", then sevalla apps processes runs --app-id "$APP_ID" --process-id "$CRON_ID". MCP: sevalla_api_create with triggerProcessCronV3.Turn on auto-deploy
updateApplicationV3):true. CLI: sevalla apps update "$APP_ID" --data '{"auto_deploy": true}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.type redis, connect it, add REDIS_URL as redis://:<password>@<internal hostname>:<internal port>/0, push the cache handler, and set the web process’s instances with Update process (updateProcessV3) and {"scaling_strategy": {"type": "manual", "config": {"instanceCount": 2}}} before you deploy. Add application domain (addApplicationDomainV3) adds each custom domain and returns the DNS records in dns_records, about 5 seconds later in List application domains.