Skip to main content
A Netlify site moves to Sevalla as a static site, which builds from the same repository and reads the same _redirects and _headers files, plus an application that runs its Netlify Functions. You set up Sevalla while Netlify keeps serving the site, move the rules that netlify.toml holds into those two files, then point the domains at Sevalla. Netlify Forms and Netlify Identity have no equivalent on Sevalla.

At a glance

Environment variables

How Netlify maps to Sevalla

Netlify’s dashboard calls a site a project; this page says site, as Netlify’s documentation does.

Before you begin

  • The Netlify CLI, signed in, and the repository linked to the site with netlify link.
  • The site’s code in a repository on GitHub, GitLab or Bitbucket. See Git providers.
  • A Sevalla company with a payment method. Static sites are free up to 100 GB of bandwidth and 600 build minutes a month; the functions application is billed by its size. See Pricing.
  • jq, for the commands that read the Netlify CLI’s JSON.

Take stock of the Netlify site

Run these in the linked repository:
  • netlify.toml holds the build settings ([build]), the build variables of each deploy context, and [[redirects]] and [[headers]] rules. Keep it open: Sevalla doesn’t read it.
  • netlify.env and netlify-preview.env hold every variable as KEY=value for production and for Deploy Previews, netlify.toml values included. A secret variable’s value shows masked, such as ****************_123: take it from your own records. Both files contain secrets: don’t commit them.
  • functions:list lists the functions in the functions folder, netlify/functions unless netlify.toml sets another. A function with path in its config answers on that path; the others on /.netlify/functions/<name>.
  • listSiteForms prints each Netlify Form with its number of submissions, such as contact 4. Export them before you delete the site.
  • getSite lists the custom domains to move.
  • A site whose package.json has a Netlify adapter, such as @astrojs/netlify, renders pages on the server. See Move a site with server-side rendering.

Move the netlify.toml rules

Netlify reads _redirects and _headers as well as netlify.toml, so you can move the rules now and push: Netlify serves the site the same way while you set up Sevalla.
1

Move the redirects

Write each [[redirects]] table as a line at the end of _redirects, in the publish directory or the folder your generator copies to it (public/ for Astro and Vite). Netlify applies _redirects rules before netlify.toml rules, so appending them keeps the order. Then add the 404 rule last:
netlify.toml
_redirects
force = true becomes ! after the status, such as 301!. query pairs go between from and to. Drop conditions, headers and signed: see Rule differences.
2

Move the headers

Write each [[headers]] table as a path line in _headers, with its values indented below it:
netlify.toml
_headers
3

Remove the tables from netlify.toml

Delete the [[redirects]] and [[headers]] tables, so each rule exists once. Keep [build] and the variables until you shut down the Netlify site.
4

Fix what works differently

Go through Rule differences and change the rules it lists, then commit and push.

Rule differences

Sevalla reads Netlify’s _redirects and _headers formats, and most rules work unchanged: status codes 301, 302, 307 and 200, splats, placeholders, !, rewrites to a file, and custom 404 pages for one folder. These differ:
A Basic-Auth line copied from Netlify sends the user name and password to every visitor in a response header. Remove it before the first deployment on Sevalla.

Trailing slashes

Netlify’s Pretty URLs are on by default: /about redirects to /about/ when about/index.html exists. Sevalla’s pretty URLs are off for new sites, and /about serves about/index.html directly, with status 200. To get Netlify’s redirects, turn pretty URLs on on the static site’s Settings tab, then purge the cache. With them on, Sevalla adds the trailing slash to every URL without a file extension before it applies your rules, so /old-page first redirects to /old-page/, then follows your rule. Pull request previews don’t use pretty URLs.

Move the functions to an application

A static site serves files only, so Netlify Functions move to a small Node.js application that runs them unchanged. A function’s default export takes a web Request and returns a Response, which is what a fetch-style server such as @hono/node-server calls.
1

Move the functions into their own folder

So that Netlify keeps deploying them from there until you shut it down, add to netlify.toml:
netlify.toml
2

Add the server

Add api/package.json and api/server.mjs, with one routes entry per function: its config.path, or /.netlify/functions/<name>:
api/package.json
api/server.mjs
Install the dependency, which writes api/package-lock.json, then commit and push:
The server answers OPTIONS requests and adds CORS headers, because the site and the functions now have different origins. Functions that read the second argument, context (context.params, context.geo, context.ip, context.cookies), or call Netlify.env.get(), need a change: read process.env and the request’s headers instead. A function in the older format, exports.handler = async (event) => ..., works once you wrap it: export default withLambda(handler), with withLambda from @netlify/aws-lambda-compat.
3

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 Branch. Under Location, choose a Data center close to your visitors, keep the Size at H1, and click Create application. See Create an application.
4

Build from the api folder

On the application’s Settings tab, in the Build strategy section, click Update build strategy. Keep Ingot, set the Build path to api, and click Update build strategy.
5

Add the functions' variables

Click the Environment variables tab, then click Add environment variables. Add the variables your functions read, such as GREETING, and click Add environment variables. To let only your site call the functions from a browser, add ALLOWED_ORIGIN with its origin, such as https://shop.example.com; pull request previews have other origins, so leave it out while you use them. See Environment variables.
6

Deploy

On the Overview tab, click Deploy now under Recent deployments, check the Branch, and click Deploy. When the status on the Deployments tab is Deployed, copy the domain under the application’s name, such as storefront-api-k3x9p.sevalla.app.
The build log shows Ingot’s build plan for the api folder:
Build log
The function answers as it did on Netlify:
Then make the site call the application. Prefix every function URL in the site’s code with a build variable, PUBLIC_API_URL here, which stays unset on Netlify, so relative URLs keep working there:
src/pages/index.astro
Use your framework’s prefix for variables that reach the browser: PUBLIC_ for Astro, VITE_ for Vite, NEXT_PUBLIC_ for Next.js. A _redirects proxy rule such as /api/* https://storefront-api-k3x9p.sevalla.app/api/:splat 200 isn’t a replacement: Sevalla drops the request’s body and headers and caches the responses.

Create the static site

1

Clean up the exported variables

In netlify.env and netlify-preview.env, delete NODE_VERSION (it becomes the Node version setting) and the variables only your functions read, such as GREETING. Replace masked secret values with the real ones. Add PUBLIC_API_URL with the application’s address to both, and, if your build reads URL (Astro’s site, sitemaps, canonical links), URL with the site’s address.
2

Open the create form

In the sidebar, click Static sites, then click Create static site. Under Source, click Private repository, then choose the Git provider, the Repository and the Branch, the one Netlify deploys to production.
3

Choose how it deploys

Leave Auto-deploy on, and turn on Pull request previews if the site uses Deploy Previews.
4

Copy the build settings

Under Details, enter a Name. Under Build settings, fill in the values from netlify.toml:
5

Import the variables

Under Environment variables, click Import .env and choose netlify.env. With previews on, select Production only.
6

Create it

Click Create static site. The site opens on its Overview tab and its first deployment starts. See Create a static site.
Sevalla doesn’t set Netlify’s read-only variables, such as URL, CONTEXT, DEPLOY_PRIME_URL or COMMIT_REF. A build that reads one gets nothing unless you add it: with site: process.env.URL in astro.config.mjs, Astro leaves out canonical links until the URL variable exists.

Set up pull request previews

Netlify builds a Deploy Preview for each pull request; Sevalla builds a pull request preview for every push to a branch other than the site’s default branch, and comments on the pull request with links to it.
1

Add the preview variables

On the static site’s Environment variables tab, click Add environment variables, click Import .env and choose netlify-preview.env. Select Preview only, and click Add environment variables. Previews build with the variables marked Preview only, not with the production ones, so this file holds every variable a preview needs. A key can exist once for production and once for previews, with different values.
2

Open a pull request

Push a branch and open a pull request. Sevalla’s comment, Preview deployments for <display name>, links to the branch and commit previews. See Pull request previews.

Export the form submissions

Netlify Forms has no equivalent: a static site on Sevalla doesn’t process form posts, and discards what a form sends to it. Export the submissions while the Netlify site exists, and give each form another destination before you move the domain.
1

Export the submissions

In Netlify, open the site’s Forms, choose a form, and click Download as CSV. Or, with the CLI, save every submission of the site as one JSON line, 100 per request:
The count matches the sum of the forms’ submissions from Take stock of the Netlify site, such as 4. Each line has the form’s fields in data. The file contains personal data: keep it out of the repository.
2

Send the form somewhere else

Point each form’s action at an endpoint that stores or forwards the submission: a route in the functions application that writes to a Sevalla database or sends an email, or a form-handling service. Remove data-netlify="true", netlify-honeypot and the hidden form-name field, which only Netlify reads.

Replace Netlify Identity

Netlify Identity, and the Role= redirect conditions that rely on it, have no equivalent: every file a static site publishes is public. Before you move the domain, move sign-in to an authentication provider, or to an application that checks the session and serves the protected pages itself.

Deploy and verify

1

Check the deployment

On the static site’s Deployments tab, wait for the status Deployed. The deployment’s log shows 🔍 `_redirects` file found!, then Redirects processed successfully!, and the same for _headers. If a line can’t be parsed, the log shows There were errors while parsing your redirect rules: and the deployment is published without any of the file’s rules.
2

Compare the pages

On the Overview tab, click Visit site and compare it with the Netlify site. From a terminal, with your site’s domain:
3

Check the rules

Request the URLs that your rules handle and compare them with Netlify. For the rules moved from netlify.toml:
With the API, Get static site deployment (getStaticSiteDeploymentV3) returns the status, success for Deployed, and Get static site deployment logs (getStaticSiteDeploymentLogsV3) returns the log. See Deploy with the API.

Move a site with server-side rendering

A site that renders pages on the server builds with a Netlify adapter, such as @astrojs/netlify or the Next.js runtime that Netlify adds, into Netlify Functions. On Sevalla it runs as an application with the framework’s Node.js server, which serves its static files too, so it needs no static site. For Astro, replace the adapter, then remove the leftover import netlify from '@astrojs/netlify'; line from astro.config.mjs:
astro add node sets adapter: node({ mode: 'standalone' }), which Ingot starts with node dist/server/entry.mjs. Run it before you uninstall the Netlify adapter: the other way round, astro add fails on the import of the missing package. With the Netlify adapter still in package.json, the build fails with Astro builds for Netlify with @astrojs/netlify, which no server in the image can run. Then deploy the repository as in Render pages on demand. For other frameworks, follow their guide: Next.js, Nuxt, SvelteKit or Remix. The application’s variables replace the site’s runtime variables, and _redirects and _headers rules move into the framework’s own redirects and headers, because an application doesn’t read those files.

Move the custom domains

Netlify keeps serving each domain until you change its pointing record, and Sevalla issues the certificate before that, so visitors never see an error.
1

Add each domain

On the static site’s Domains tab, click Add custom domain, enter the domain in Domain name, for example shop.example.com, and click Add custom domain. Its DNS records sheet opens.
2

Verify it

Where the domain’s DNS is managed, add the two TXT records the sheet shows, _cf-custom-hostname.shop.example.com and _acme-challenge.shop.example.com. Don’t change the record that points at Netlify yet. When the status changes to Pointing, the certificate is ready. See Verify the domain.
3

Point it at Sevalla

Replace the record that points at Netlify, a CNAME to <site>.netlify.app for a subdomain, or an ALIAS to apex-loadbalancer.netlify.com or an A record with 75.2.60.5 for a root domain, with the A record that the sheet shows. The status changes to Active when Sevalla sees it. See Point the domain.
4

Remove it from Netlify

When the domain is Active and its old record has expired (its TTL), remove the domain from the Netlify site’s domain management.
If the domain uses Netlify DNS, make these changes in its Netlify DNS zone (DNS in the team’s sidebar, then the domain, then Add new record), and move the zone to another DNS provider, with every record, before you delete the site: Netlify DNS hosts records only while a Netlify site uses the domain. Update URL and ALLOWED_ORIGIN to the custom domain, then deploy the static site and the application.

Shut down the Netlify site

When the domains are Active and traffic reaches Sevalla:
  1. Keep the exported form submissions and netlify.env until you no longer need them, outside the repository.
  2. Delete the site with netlify sites:delete "$SITE_ID". Its form submissions go with it, and its netlify.app address stops working.
  3. Remove what only Netlify reads from the repository: netlify.toml, and Netlify packages such as @netlify/functions.

Troubleshoot

Deploy with the API

This sequence does what Move the functions to an application, Create the static site and Set up pull request previews do in the dashboard, with the Sevalla API. It starts from the repository as it is after Move the netlify.toml rules and the first two steps of Move the functions to an application, pushed to main. A script or an AI agent runs it as is, in one shell, from the root of the linked repository. 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 CLI reads it from SEVALLA_API_TOKEN.
  • The repository on GitHub, and a GitHub account with access to it connected to Sevalla. Without access, the steps that create the services fail with 400 and Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to grant it.
  • The Netlify CLI, signed in and linked to the site, plus curl and jq.
Export the variables as JSON, drop NODE_VERSION and the functions’ variables from the site’s files, and keep the functions’ variables in their own file. Replace masked secret values in the files before you go on:
The files contain secrets: don’t commit them.
1

Choose the location

List clusters (listClustersV3) returns every location with its region ID in location. Save the one for the functions, here Frankfurt, Germany:
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Look up the H1 size

List process resource types (listProcessResourceTypesV3):
CLI: sevalla resources process-types. MCP: get_platform_options.
3

Create the functions application

Create application (createApplicationV3) creates the application with a web process at S1 and doesn’t deploy it:
CLI: sevalla apps create --name storefront-api --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository "$REPO" --branch main. MCP: create_application with deploy: false.
4

Build from the api folder

Update application (updateApplicationV3) sets the build path:
CLI: sevalla apps update "$APP_ID" --data '{"build_path": "api"}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
5

Set the size

List processes (listProcessesV3) finds the web process, and Update process (updateProcessV3) moves it to H1:
It prints h1. CLI: sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --resource-type "$H1_ID". MCP: scale_process.
6

Add the functions' variables

Create environment variable (createEnvironmentVariableV3), once per entry of functions.json, available at runtime:
It prints each key, here GREETING. CLI: sevalla apps env-vars create "$APP_ID" --key GREETING --value <value>, which also makes it available during the build. MCP: set_environment_variables with runtime: true and buildtime: false.
7

Deploy the application

Trigger deployment (triggerDeploymentV3), then Get deployment (getDeploymentV3) until the status is final, for up to 30 minutes:
CLI: sevalla apps deployments trigger "$APP_ID", then sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: trigger_deployment, then get_deployment.
8

Check the functions

List application domains (listApplicationDomainsV3) returns the application’s domain:
CLI: sevalla apps domains list --app-id "$APP_ID". MCP: get_application.
9

Create the static site

Create static site (createStaticSiteV3) with the settings from netlify.toml, auto-deploy and pull request previews on. "node_version": "22" is Node 22. It doesn’t start a deployment:
CLI: sevalla static-sites create --name storefront --source privateGit --git-type github --repository "$REPO" --branch main --build-command "npm run build" --publish-directory dist --node-version 22, which leaves auto-deploy and previews off. MCP: create_static_site with the same fields and deploy: false.
10

Add the build variables

Create static site environment variable (createStaticSiteEnvVarV3), once per variable: production.json for production, preview.json for previews, and URL and PUBLIC_API_URL for both:
It prints each variable with where it applies, such as PUBLIC_BANNER production=false preview=true. Unlike Netlify, Sevalla doesn’t give previews the production variables: a variable that both need is added for both. CLI: sevalla static-sites env-vars create "$SITE_ID" --key <key> --value <value>, which makes the variable available to production and previews. MCP: set_environment_variables with scope: "static_site", production and preview.
11

Deploy the static site

Deploy static site (deployStaticSiteV3), then Get static site deployment (getStaticSiteDeploymentV3) until the status is final:
CLI: sevalla static-sites deployments trigger "$SITE_ID", then sevalla static-sites deployments get "$SITE_DEPLOYMENT_ID" --site-id "$SITE_ID". MCP: trigger_deployment, then get_deployment, with service_type set to static_site.
12

Check the rules files

Get static site deployment logs (getStaticSiteDeploymentLogsV3) returns the build log. limit=5000 reads all of it:
CLI: sevalla static-sites deployments logs "$SITE_DEPLOYMENT_ID" --site-id "$SITE_ID" --search processed. MCP: get_deployment_logs with service_type set to static_site.
13

Check the site

Get static site (getStaticSiteV3) returns the site’s domain in hostname:
CLI: sevalla static-sites get "$SITE_ID". MCP: get_static_site.
Then run the checks in Deploy and verify. Toggle pretty URLs (toggleStaticSitePrettyUrlV3) turns on Netlify’s trailing-slash redirects (CLI: sevalla static-sites pretty-url-toggle "$SITE_ID"). Add static site domain (addStaticSiteDomainV3) adds each custom domain with {"domain_name": "shop.example.com"}, and List static site domains (listStaticSiteDomainsV3) returns the records to add in dns_records: verification and ssl (the two TXT records) and origin (the A record).