_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.tomlholds 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.envandnetlify-preview.envhold every variable asKEY=valuefor production and for Deploy Previews,netlify.tomlvalues 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:listlists the functions in the functions folder,netlify/functionsunlessnetlify.tomlsets another. A function withpathin itsconfiganswers on that path; the others on/.netlify/functions/<name>.listSiteFormsprints each Netlify Form with its number of submissions, such ascontact 4. Export them before you delete the site.getSitelists the custom domains to move.- A site whose
package.jsonhas 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.
Move the redirects
[[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:force = true becomes ! after the status, such as 301!. query pairs go between from and to. Drop conditions, headers and signed: see Rule differences.Move the headers
[[headers]] table as a path line in _headers, with its values indented below it:Remove the tables from netlify.toml
[[redirects]] and [[headers]] tables, so each rule exists once. Keep [build] and the variables until you shut down the Netlify site.Fix what works differently
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:
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 webRequest and returns a Response, which is what a fetch-style server such as @hono/node-server calls.
Move the functions into their own folder
netlify.toml:Add the server
api/package.json and api/server.mjs, with one routes entry per function: its config.path, or /.netlify/functions/<name>:api/package-lock.json, then commit and push: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.Create the application
Build from the api folder
api, and click Update build strategy.Add the functions' variables
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.Deploy
storefront-api-k3x9p.sevalla.app.api folder:
PUBLIC_API_URL here, which stays unset on Netlify, so relative URLs keep working there:
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
Clean up the exported variables
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.Open the create form
Choose how it deploys
Copy the build settings
netlify.toml:Import the variables
netlify.env. With previews on, select Production only.Create it
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.Add the preview variables
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.Open a pull request
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.Export the submissions
4. Each line has the form’s fields in data. The file contains personal data: keep it out of the repository.Send the form somewhere else
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 theRole= 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
Check the deployment
🔍 `_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.Compare the pages
Check the rules
netlify.toml: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.Add each domain
shop.example.com, and click Add custom domain. Its DNS records sheet opens.Verify it
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.Point it at Sevalla
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.Remove it from Netlify
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:- Keep the exported form submissions and
netlify.envuntil you no longer need them, outside the repository. - Delete the site with
netlify sites:delete "$SITE_ID". Its form submissions go with it, and itsnetlify.appaddress stops working. - 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 tomain. 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 fromSEVALLA_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
400andCannot 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
curlandjq.
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:
Choose the location
listClustersV3) returns every location with its region ID in location. Save the one for the functions, here Frankfurt, Germany:sevalla resources clusters. MCP: get_platform_options.Look up the H1 size
listProcessResourceTypesV3):sevalla resources process-types. MCP: get_platform_options.Create the functions application
createApplicationV3) creates the application with a web process at S1 and doesn’t deploy it:sevalla apps create --name storefront-api --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository "$REPO" --branch main. MCP: create_application with deploy: false.Build from the api folder
updateApplicationV3) sets the build path:sevalla apps update "$APP_ID" --data '{"build_path": "api"}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.Set the size
listProcessesV3) finds the web process, and Update process (updateProcessV3) moves it to H1:h1. CLI: sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --resource-type "$H1_ID". MCP: scale_process.Add the functions' variables
createEnvironmentVariableV3), once per entry of functions.json, available at runtime: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.Deploy the application
triggerDeploymentV3), then Get deployment (getDeploymentV3) until the status is final, for up to 30 minutes:sevalla apps deployments trigger "$APP_ID", then sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: trigger_deployment, then get_deployment.Check the functions
listApplicationDomainsV3) returns the application’s domain:sevalla apps domains list --app-id "$APP_ID". MCP: get_application.Create the 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: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.Add the build variables
createStaticSiteEnvVarV3), once per variable: production.json for production, preview.json for previews, and URL and PUBLIC_API_URL for both: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.Deploy the static site
deployStaticSiteV3), then Get static site deployment (getStaticSiteDeploymentV3) until the status is final: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.Check the rules files
getStaticSiteDeploymentLogsV3) returns the build log. limit=5000 reads all of it: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.Check the site
getStaticSiteV3) returns the site’s domain in hostname:sevalla static-sites get "$SITE_ID". MCP: get_static_site.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).