Skip to main content
A Next.js project on Vercel moves to Sevalla as an application whose web process runs 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 deploy does. 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 list shows the Marketplace resources, such as a Neon database, and blob list-stores the Blob stores with their file counts.
  • crons list and vercel.json show the cron jobs, redirects, rewrites and headers to recreate.
  • vercel.env holds 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 grep lists 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, here sevalla, 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:
lib/db.ts
Queries such as 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

Replace put() from @vercel/blob with the AWS SDK’s PutObjectCommand, and build the file’s URL from the bucket’s public domain:
app/api/upload/route.ts
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:
proxy.ts
Then 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 read vercel.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:
next.config.ts
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 a GET 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:
scripts/cron.mjs
The route keeps its 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

1

Create the PostgreSQL database

In the sidebar, click Databases, then click Create database. Under Database, choose PostgreSQL and a version at least as new as the source’s. Enter 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.
2

Create the bucket

If the project uses Vercel Blob: in the sidebar, click Object storage, then click Create object storage. Enter storefront-files as the Name, choose the Location closest to the application, and click Create object storage.
3

Make the bucket public

For a public Blob store: on the bucket’s Settings tab, in Public access, click Enable public access, then click Enable public access in the dialog. Public domain shows the domain, such as storefront-files-x7k2m.sevalla.storage. Keep files from a private Blob store in a private bucket. See Public access.
4

Create the application

In the sidebar, click Applications, then click Create > Application. Under Source, click Private repository, then choose the Git provider, the Repository and the 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. Choose storefront-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

1

Clean up the exported file

Drop the variables that Vercel and its integrations set, and the quotes that vercel env pull adds, which the dashboard would keep as part of the value:
In 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:
sevalla.env
The bucket’s values are on its Settings tab, under Details and Access keys. 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.
2

Import the file

Click the Environment variables tab, then click Add environment variables. Click Import .env and choose sevalla.env. Keep Available during runtime and Available during buildtime selected, and click Add environment variables.
Don’t add 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 in vercel.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. Vercel’s Hobby plan runs a cron job once a day at most, at any minute within the hour. A Sevalla cron job runs at the minute you set, as often as every minute. See Cron jobs.

Copy the PostgreSQL data

1

Stop writes on Vercel

Pause the project, which answers every production request with 503 and DEPLOYMENT_PAUSED, and stops the cron jobs from writing:
The CLI asks you to type the project name. Without a terminal, use the REST API: curl -X POST "https://api.vercel.com/v1/projects/<project-id>/pause" -H "Authorization: Bearer $VERCEL_TOKEN".
2

Dump the database

Use the unpooled connection, 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:
3

Open the Sevalla database to your computer

In the sidebar, click Databases, then click 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.
4

Restore the dump

It prints nothing when every object restores.
5

Compare the tables

Save a query that counts each table’s rows and hashes them, then run it on both databases. Name your own tables in it:
compare.sql
To rehearse, run these steps without pausing the project, deploy, and test on the 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

1

Copy the files

Save this script in an empty folder, install its two packages, and run it with the Blob token from vercel.env and the bucket’s values. It copies every blob to the same path in the bucket, with its content type:
copy-blob.mjs
It prints the number of files, such as Copied 6 files, which matches the Files column of vercel blob list-stores. The bucket’s Objects tab lists them.
2

Point the stored URLs at the bucket

The database holds the full URLs that 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:
The path after the domain stays the same, because the script kept each blob’s path.

Deploy and verify

1

Deploy

On the application’s Overview tab, click Deploy now under Recent deployments, check the Branch, and click Deploy.
2

Check the deployment

On the Deployments tab, wait for the status Deployed. The build log shows the routes that 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.
3

Compare the responses

Request the same paths from Vercel, during a rehearsal, and from the application’s domain. They answer with the same status codes:
API responses match too, except for file URLs, which now point at the bucket. The x-visitor-country header that proxy.ts sets shows your country.
4

Run a cron job

On the Processes tab, click ⋯ on the cron job, click Run now, then click Run now in the dialog. The run shows Succeeded, and the Logs tab has the line the script prints, such as 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.
5

Turn on auto-deploy

To deploy every push to the branch, click the Settings tab, then click Update auto-deploy in the Auto-deploy section. Turn on Auto-deploy, choose the Branch, and click Update auto-deploy. See Deployments.

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 with export 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.
1

Create the Redis database

Create a database as in Create the services: choose Redis, enter storefront-cache as the Display name, and choose the application’s Data center.
2

Connect it

On the application’s Networking tab, add an internal connection to storefront-cache with Add environment variables to the application on. Its URL is already called REDIS_URL.
3

Add the cache handler

From the root of the repository:
Then point Next.js at it and turn off the in-memory cache, so that Redis is the only copy:
next.config.ts
Commit and push the changes.
4

Add instances

On the Processes tab, click ⋯ on the web process, then click Update process. In Scaling, drag Instance count to 2 or more, click Update process, then deploy. See Set the number of instances.
In the same test with the handler and two instances, all 20 requests after the 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.
  1. 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.
  2. 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.
  3. Add the Preview environment’s variables in the settings’ Environment variables section. Get them with vercel env pull vercel-preview.env --environment=preview and clean them up as in Move the environment variables.
Preview apps don’t get the production application’s internal connections, so give them a database of their own and set its connection URL as 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

1

Add each domain

On the application’s Domains tab, click Add custom domain, enter the domain in Domain name, for example shop.example.com, and click Add custom domain.
2

Verify it

Add the 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:
Vercel DNS adds a 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.
3

Point it at Sevalla

Replace the record that points at Vercel (an 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.
To keep the switch without downtime, verify the domain before the maintenance window, and point it once the data is copied and the deployment checks pass.

Shut down the Vercel project

When the domains are Active and traffic reaches Sevalla:
  1. Disable the external connection: on the database’s Networking tab, click Disable under External connection, then click Disable in the dialog.
  2. Merge the sevalla branch into your production branch, and choose that branch in the application’s Auto-deploy settings.
  3. 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 rm removes its DNS records too.
  4. Keep vercel.dump until you no longer need it. Then delete the Vercel project with vercel project delete storefront, empty the Blob store with vercel blob empty-store <store-id> and delete it with vercel blob delete-store <store-id> (a store that still has files can’t be deleted: Blob store not empty), delete the Neon database with vercel integration resource delete <name>, and remove the integration with vercel 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 the sevalla 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 400 and Cannot 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.mjs from Copy the Vercel Blob files in a folder of its own, here ../copy-blob, with its packages installed, and curl, jq, openssl, pg_dump, pg_restore and psql.
Export the production variables, drop the ones Vercel and its integrations set, and replace each [SENSITIVE] in sevalla.env with the real value:
Both files contain secrets: don’t commit them.
1

Choose the location

List clusters (listClustersV3) returns every location with its region ID in location. Save Ashburn, USA, the one next to Vercel’s default iad1:
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Look up the cron job size

List process resource types (listProcessResourceTypesV3). Save H1, the smallest, for the cron jobs:
CLI: sevalla resources process-types. MCP: get_platform_options.
3

Look up the database size

List database resource types (listDatabaseResourceTypesV3). Pick a size with room for the data; here DB1, the smallest:
CLI: sevalla resources db-types. MCP: get_platform_options.
4

Look up the PostgreSQL version

List database versions (listDatabaseVersionsV3) returns the versions the location offers, oldest first. Save the newest, which is at least as new as Neon’s:
CLI: sevalla resources db-versions --cluster "$CLUSTER_ID". MCP: not needed, create_database takes the newest version by default.
5

Create the PostgreSQL database

Create database (createDatabaseV3). You choose the password, and the API never returns it, so keep it for the connection URL:
CLI: 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.
6

Create the bucket

Create object storage (createObjectStorageV3) with public access, in enam (Eastern North America). The response has the bucket’s name, public domain, endpoint and keys:
It prints the domain, such as 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.
7

Wait for the database

Get database (getDatabaseV3) shows status ready when the database is up, with its internal hostname and port:
It prints ready. CLI: sevalla databases get "$PG_ID". MCP: get_database.
8

Create the application

Create 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:
CLI: sevalla apps create --name storefront --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository "$REPO" --branch "$BRANCH". MCP: create_application with deploy: false.
9

Connect the database

Create internal connection (createApplicationInternalConnectionV3):
The API adds no environment variables; the next step adds 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.
10

Add the environment variables

Create environment variable (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:
It prints each key as it’s added. CLI: 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.
11

Create the cron jobs

Create process (createProcessV3) with type cron, once per entry in the crons of vercel.json, read from main:
CLI: 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.
12

Open the database to your computer

Toggle external connection (toggleDatabaseExternalConnectionV3) turns it on. Then Get database returns external_hostname and external_port once it’s ready:
The first command prints 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.
13

Copy the data

Pause the Vercel project as in Copy the PostgreSQL data, then dump the Neon database and restore it into the Sevalla one:
Both print nothing when they succeed. Without the data, the deployment fails: next build can’t prerender the ISR pages.
14

Copy the files

Run copy-blob.mjs with the Blob token and the bucket’s values:
It prints the number of files, such as Copied 6 files.
15

Point the stored URLs at the bucket

Replace the Blob store’s domain with the bucket’s in each column that stores a file URL, here products.image_url and uploads.url:
16

Deploy

Trigger deployment (triggerDeploymentV3) deploys the latest commit of the branch:
CLI: sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.
17

Wait for the deployment

Get deployment (getDeploymentV3) reports the status, which ends as success, failed, cancelled or skipped. The loop polls for up to 30 minutes:
CLI: sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.
18

Read the build log

Get application deployment logs (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:
CLI: sevalla apps deployments logs "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment_logs.
19

Check the application

List application domains (listApplicationDomainsV3) returns the application’s domain. Request the products API from it:
CLI: sevalla apps domains list --app-id "$APP_ID". MCP: get_application.
20

Run a cron job

Trigger cron job (triggerProcessCronV3) starts one run now, and List cron runs (listProcessCronRunsV3) reports it:
CLI: 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.
21

Turn on auto-deploy

Update application (updateApplicationV3):
It prints true. CLI: sevalla apps update "$APP_ID" --data '{"auto_deploy": true}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
Then run the checks in Deploy and verify, and turn the external connection off with the same toggle call. To run more than one instance, create a Redis database the same way with 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.