# Moving from Netlify

Move a Netlify site to Deplane with one command, or from the dashboard ([From the dashboard](#from-the-dashboard)). `deplane import netlify` reads the site on Netlify and the `netlify.toml` in your code, and shows what moves and what stays. It then creates the Deplane app with its production variables and deploys your code. Netlify keeps serving your users until you switch.

The command runs on your own computer with your Netlify token, which never leaves it. It only reads Netlify: nothing there is changed. The command and its flags are in the [CLI reference](cli.md#deplane-import-netlify), and every error has a fix in [Troubleshooting and error codes](errors.md#netlify_token_missing). It works like [Moving from Heroku](heroku.md).

## From the dashboard

**New app → Import from another platform** (`/import`, or `/import/netlify` with Netlify already picked) does the same import without the CLI or a folder, in five steps:

1. **Connect.** Create a personal access token in Netlify (**User settings → Applications → Personal access tokens → New access token**) with a short expiration, paste it into the masked field and press **Read**. Netlify has no read-only tokens: the token can change everything your account can. Deplane only reads with it (GET requests), inside the request that carries it, and never stores it. Delete the token in the same place when the import is done.
2. **Pick.** The page lists the sites the token can see (at most 100, with a filter box). Pick the one to import, or type its name or ID. A link `/import/netlify?ref=NAME` fills it in, and a token that sees exactly one site checks it at once. Picking runs the check: the report, nothing created.
3. **Review.** The same ✓ / ⚠ / ✗ report as `--check`, with the **Plan** section. There is no `netlify.toml` here: the build settings come from the site. Then **Code and name**, filled in: the repository and branch Netlify builds, preselected when the repository is among the ones you gave the Deplane GitHub App access to (the match is made only inside your own grants; otherwise the page shows the repository's name and a **Connect GitHub** link that brings you back), with the commit of the published deploy when the repository has it. The Deplane app's name is the site's, made a Deplane name, with a suffix when a team already holds it. **Import** asks for your passkey.
4. **Import.** The page follows each step (the app with the production variables Netlify returns, as secrets and as build secrets), lists what exists so far, then stops at **Set secrets** when the site has secret variables.
5. **Set secrets.** Netlify gives the values of **secret** variables only to code running on Netlify (see [Secret variables](#secret-variables)), so the import waits here, as long as it takes, with one masked field per name the report listed. Take the values from where you keep them, fill them in, and press **Set secrets**: they go straight to the app's secrets, never through the import. Then **Deploy** connects the repository and builds, and ends with the live address, the switch-over checklist and the reminder to delete the token. A waiting import does not block your team's next import, and **Cancel import** ends it.

A failed or cancelled import has **Delete what it created**: the app this import made, nothing else, after your passkey. Then import again. The rest is as for Heroku's [From the dashboard](heroku.md#from-the-dashboard): the code comes from GitHub (previews off), the token is used inside the requests that list, check and start the import and dropped, only you signed in to the dashboard can do it, and a team runs one import at a time.

## Before you start

1. [Install the Deplane CLI](install.md) and run `deplane login`.
2. Have the site's code in a folder: a checkout of the repository Netlify builds, at the commit it runs. If Netlify builds a subfolder (its **Base directory**), pass that folder with `--source`.
3. Create a personal access token in Netlify: **User settings → Applications → Personal access tokens → New access token**, with a short expiration.

**The Netlify token.** A personal access token can do everything your account can; there are no read-only tokens. The CLI only reads with it (GET requests to `api.netlify.com`). Delete it when the import is done.

Give the token to Deplane in one of three ways, never as a flag:

- run `deplane import netlify my-site --check` in a terminal and paste it at the hidden prompt;
- set it for one command: `NETLIFY_AUTH_TOKEN=nfp_... deplane import netlify my-site --check` (it lands in your shell history; the prompt does not);
- or pipe it in from a password manager: `op read op://Private/netlify/token | deplane import netlify my-site --check`.

A token piped on stdin wins over `NETLIFY_AUTH_TOKEN`. The CLI says which one it used, never the token itself. It is never written to disk, never printed, left out of `--debug`, and never sent to Deplane.

## Secret variables

Netlify gives the values of **secret** variables only to code running on Netlify: the dashboard, API and CLI show none of them, except a local development value. The import lists them by name, copies the others, and creates the app without deploying it, since it would start without them. The next steps show what to set before you deploy:

```sh
deplane secrets set API_KEY=... --app my-site
deplane secrets set --build API_KEY=... --app my-site   # if the build reads it
cd my-site && deplane launch --app my-site   # deploys the app and writes deplane.toml
```

Variables shared by your Netlify team (**Team settings → Environment variables**) may not be listed for the site: check them there, and set the ones the site needs.

## Check first

```sh
$ deplane import netlify shop --check
Netlify site shop (framework astro, team acme, repo github.com/acme/shop@main, commit 1a2b3c4) → Deplane app shop

Code
  ✓ Deploys /home/me/shop with Railpack: Detected Astro 5, Node 22, port 8080.
  ✓ Your folder is at commit 1a2b3c4, the one Netlify runs.
  ⚠ Netlify builds with `npm run build` and publishes dist. Railpack detects the build itself ...
  ⚠ Netlify applies 2 redirect rule(s) (_redirects, netlify.toml) and 0 header rule(s) itself; Deplane does not. ...
  ⚠ Netlify serves the site from its global CDN; Deplane serves it from one region, in Europe. ...

Processes
  ✓ web: Netlify serves the site from its CDN → 1 small instance. Set more with `deplane scale`.

Config vars
  ✓ 2 copied as secrets: PUBLIC_API_URL, SITE_ENV.
  ✓ 2 also set as build secrets, since Netlify gives them to the build ...

Domains
  ⚠ www.shop.example points at Netlify (shop.netlify.app). It moves when you switch ...

Plan
  ✓ Fits your Free plan: after the import the team uses 1 of 2 instances and 512 MB of 1 GB memory.

Nothing was changed (--check). Run the command again without --check to import.
```

✓ is what moves as it is, ⚠ is what you should know or do yourself, and ✗ is what stops the import. `--check --json` prints the same report as one JSON document. Then run the command again without `--check` (in a terminal it asks first; from a script, add `--yes`).

## What moves

| On Netlify | On Deplane |
|---|---|
| A static site (Astro, Vite, Hugo, Eleventy, Gatsby ...) | An app that Railpack builds and serves as files. |
| A Next.js site | An app that Railpack builds and runs with `next start`. |
| Production variables (the production value, else the one for all contexts) | Secrets. Those Netlify gives to builds are build secrets too, since a static site's variables are built into its files. `netlify.toml`'s `[build.environment]` and `[context.production.environment]` apply to builds only, so they become build secrets only (over the site's value for the build). Not copied: Netlify's own variables, and variables without a production value. |
| Secret variables | Listed by name, not copied: see [Secret variables](#secret-variables). |
| The custom domain and domain aliases | Listed, and moved at the switch. `*.netlify.app` stays with Netlify. |
| Databases you connected (Netlify DB, Neon, Supabase ...) | Their URLs are copied as they are, and the site keeps using them. To move a Postgres database later, use `deplane db import`. |
| The code | Your folder, built by Railpack, or by your Dockerfile if it has one. |

Instances: Netlify has none to map. The app starts with one small instance (0.5 CPU, 512 MB).

## What does not move

- **Netlify Functions, scheduled functions and Edge Functions** do not run on Deplane: the report lists them. Serve those routes from your app's own server, and run scheduled work as a [cron job](workers.md).
- **Redirects and headers** (`_redirects`, `netlify.toml`) are applied by Netlify's CDN, not by Deplane: the report counts them. Move them into your framework's config or your server.
- **Netlify Forms** keep working: Deplane reads the same `netlify` and `data-netlify="true"` attributes (see [Forms](forms.md)). Past submissions are not moved; export the ones you keep from Netlify first.
- **Netlify Identity, Blobs, Image CDN, split testing, prerendering and build plugins** are Netlify services.
- **Netlify DNS**: if Netlify hosts your domain's DNS, make the changes there, or move the zone first.
- Netlify serves from a global CDN; Deplane serves from one region, in Europe. Put a CDN in front of the app if your users are far away.

## Switch over

1. **Set the secret variables and deploy**, if the import listed any. Test the app at its Deplane address.
2. **Before the day:** run `deplane domains add HOSTNAME --app APP` for each custom domain and add the TXT record it shows.
3. **Switch DNS** to the target `deplane domains add` showed, and wait for the certificate (`deplane domains check HOSTNAME --app APP`).
4. **Remove the domains from the Netlify site**, and delete it once the app has run well on Deplane.
5. **Delete the Netlify token** in User settings → Applications.

## Limits

The [Heroku import's limits](heroku.md#limits) apply. Secret variables, functions and forms are not moved. The import is built on Netlify's public API; tell support@deplane.io if Netlify answers something the CLI does not expect.
