--- title: Migrate from Zero 0.8 description: Publishing a Zero 0.8 app moves it to the current Zero whenever nothing would be lost. When something would, it publishes as 0.8 and tells you exactly what to change. sidebar: order: 8 --- Your Zero 0.8 app keeps working. Publish it with a current CLI and it moves to the current Zero, the `zero.config.ts` compiler described in [Dynamic sites with Zero](/zero), whenever nothing would be lost. When something would, it publishes as 0.8, keeps serving, and the receipt lists each blocker. New Spaces start on the current Zero. A Zero 0.8 app is one built on `@spacefast/zero`: a `capsule()` in `server/index.ts`, declared in `sf.jsonc` as `"runtime": { "kind": "zero" }`. ## Keep working on a 0.8 app A 0.8 app keeps its own local dev server and database commands. The [`sf zero`](/cli/zero) development commands are for `zero.config.ts` apps, not for this one. The exception is `sf zero migrate app`, which [previews the conversion](#preview-the-conversion). ### Run it locally From the 0.8 project root, `sf dev` starts the 0.8 dev server, because `sf.jsonc` declares `"runtime": { "kind": "zero" }`. ```bash sf dev sf dev --state-backend sqlite sf dev --no-watch ``` | Flag | Default | What it does | | --- | --- | --- | | `--port=` | `4173` | Port for the dev server | | `--state-backend=memory\|sqlite` | `memory` | Where local state lives. `memory` resets on every start | | `--watch` / `--no-watch` | `--watch` | Poll source files and reload after changes | | `--watch-interval=` | `1000` | Polling interval in milliseconds | | `--dry-run` | off | Print the dev server plan without starting it | `sf dev` binds `127.0.0.1` unless you pass `--host` with `--allow-network`. ### Read and migrate its database These commands read the tables the 0.8 app declares. A [Functions](/functions) worker declares none, so they answer it with `db_not_available` or `runtime_kind_mismatch`. `sf db console` works on any Space. See [Database](/database#the-sql-console). ```bash sf db sf db list sf db dump --table todos --limit 100 sf db export --output ./backup.json sf db migrate ``` | Command | What it does | | --- | --- | | `sf db [target]` | Tables, columns, and the pending migration plan | | `sf db list [target]` | Tables and columns. Aliased as `sf db ls` | | `sf db dump [target]` | Rows from every declared table. `--table` picks one, `--limit` caps rows per table (default 25, at most 100), and `--limit 0` prints row counts only | | `sf db export [target]` | Every declared table to one versioned JSON backup. `--output` names the file. An existing file is replaced only after the whole export succeeds | | `sf db migrate [source]` | Re-apply the live version's schema migrations, and preview what the source tree would change on top. `--drop` and `--rename` preview those operations without applying them | Pass `--port ` or `--local-url ` to `sf db`, `sf db list`, `sf db dump`, or `sf db export` to read a running `sf dev` server instead of the live version. ## Migrate by publishing From the 0.8 project root, publish as usual: ```bash sf publish --space spc_123 --json ``` The CLI converts the app on your machine and checks the result. Then one of two things happens: | Result | What you see | | --- | --- | | Nothing would be lost | The Space moves to the current Zero, on its existing tables. `version.zero.migration.status` is `migrated` | | Something would be lost | The app publishes as 0.8 and keeps serving. `status` is `refused`, and `diagnostics` lists each blocker with its `code`, `file`, `line`, `feature` and `fix` | Fix what `diagnostics` names and publish again. Nothing about your app changes until a publish migrates it. `waiting_on_runtime` means the app can move but the Space's runtime can't run the current Zero yet. It publishes as 0.8, and the next publish after the runtime update migrates it. ## Preview the conversion To read the converted source before you publish, run the converter yourself. It writes a new project and never touches the Space. ```bash sf zero migrate app --from zero-0.8 ./old-app --out ./candidate --space spc_123 --dry-run sf zero migrate app --from zero-0.8 ./old-app --out ./candidate --space spc_123 sf zero check ./candidate ``` `./old-app` is the 0.8 project root, the directory with `sf.jsonc` and `server/index.ts`. `--out` must be a new directory, and a refusal writes nothing. Pass the Space's ID with `--space` so the converter binds each table to the exact MySQL table the Space already stores it in, or `--fresh` for an app that has no Space yet. Add `--users` when the Space has Space Users turned on but the capsule has no `auth` declaration. The command prints a JSON report: | Field | What it holds | | --- | --- | | `entries` | Every declaration, `mapped` or `review` | | `errors` | Everything the converter refused, each with an RFC 6901 pointer into your source | | `adoption_sql` | The SQL that checks the existing tables before first use | | `connector_roles` | Connector role mappings, also written to `connector-roles.json` | The exit code is `0` for a candidate or a clean dry run, `1` for a refusal, and `2` for bad arguments. Fix every `errors` pointer, then read every `review` entry. A candidate with review entries still compiles, but it may not behave the way your 0.8 app did. ## What converts | Zero 0.8 | Current Zero | | --- | --- | | `capsule()` in `server/index.ts` | `defineConfig()` in `zero.config.ts` | | `schema` tables | `table()` entries under `tables`, bound to the existing MySQL tables | | `query()` | An action that reads | | `mutation()` | An action that writes, in one transaction | | `action()` | An action | | `ctx.db` `get`, `insert`, `update`, `delete`, and static `withIndex` chains ending in `collect`, `count`, `first`, or `take` | `db.query` and `db.insert`/`update`/`delete` | | `auth` | A `members` user projection with a unique email. `auth.requireSignIn` becomes a signed-in rule on each action | | Native `posts`, or a `content/posts` directory | A `posts` collection with title and rich-text body | | Connectors | `connector-roles.json`: `owner` becomes `personal`, `team` and `visitor` stay | Ported tables keep their storage. IDs stay text in action results, numbers stay doubles, and booleans match both the old `true`/`false` text and the new `1`/`0`. The converter copies no rows and renames no columns. Every write starts denied. Open `zero.config.ts` and set `access` on each collection, table, and action before you rely on it. ## What you port by hand The converter refuses these, with a pointer to each: - **Endpoints.** Rewrite each one as a named action at `POST /wp-json/zero/v1/actions/`, then update its callers. - **Pages, layouts, and islands.** Rebuild them as [TSX pages](/zero#pages). - **Connector calls inside handlers.** - **Realtime.** Declarations are dropped with a review hint. The current Zero has no live queries, so a client reads again after a write. - **Pagination, dynamic query builders, and range comparisons on optional fields.** - **npm imports and unknown `ctx` services.** - **Row IDs above JavaScript's safe integer range.** The converter reads only the server. Your `client/` code uses `@spacefast/zero/client`, which has no counterpart: call the generated client in `zero/client/` from your frontend instead, or move the UI to TSX pages. ## Publishing a hand-ported app Publish the 0.8 project, not a hand-edited candidate. A Space's data moves with the app: publishing a `zero.config.ts` app to a Space that still runs Zero 0.8 is refused with `zero_migration_required`, because it would leave the posts, users, and sessions behind. Once a publish has migrated the Space, edit and publish the current Zero app as usual. A project with both `"runtime": { "kind": "zero" }` in `sf.jsonc` and a `zero.config.ts` fails to publish with `zero_mixed_generation`. Delete that block if it has one.