From 1a5110bf66950e749d323ed5d14fa84990aeac4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 7 Nov 2026 02:01:56 +0000 Subject: [PATCH 01/12] Teach Zero v2 and move Zero 0.8 to one migration page Zero v2, a zero.config.ts compiled to WordPress, is now what the docs teach. The Dynamic pages, the CLI pages, and the config reference no longer show capsules, @spacefast/zero, or sf.jsonc runtime blocks. A new "Migrate from Zero 0.8" page is the only place 0.8 appears: existing apps keep working, automatic migration is coming, and sf zero migrate app ports the source by hand today. Co-Authored-By: Claude Opus 5.5 --- content/(dynamic)/crons.mdx | 2 +- content/(dynamic)/database.mdx | 188 ++++------ content/(dynamic)/environment-variables.mdx | 28 +- content/(dynamic)/functions.mdx | 4 +- content/(dynamic)/logs.mdx | 6 +- content/(dynamic)/meta.ts | 2 +- content/(dynamic)/object-storage.mdx | 25 +- content/(dynamic)/wordpress.mdx | 26 +- content/(dynamic)/zero-migrate.mdx | 79 ++++ content/(dynamic)/zero-runtime.mdx | 386 ++++++++++---------- content/(reference)/config-file.mdx | 15 +- content/(reference)/glossary.mdx | 6 +- content/(serve)/site-pages.mdx | 2 +- content/cli/builds.mdx | 2 +- content/cli/db.mdx | 159 +------- content/cli/project.mdx | 9 +- content/cli/publish.mdx | 5 +- content/cli/storage.mdx | 4 +- content/cli/zero.mdx | 155 ++++---- content/index.mdx | 4 +- content/quickstart.mdx | 2 +- evals.yaml | 2 +- scripts/docs-experience.test.mjs | 7 +- 23 files changed, 449 insertions(+), 669 deletions(-) create mode 100644 content/(dynamic)/zero-migrate.mdx diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index ee25e156..46ef387a 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -9,7 +9,7 @@ There's no dashboard editor for crons. Declare schedules in `sf.jsonc`, then pub ## How a run happens -A cron is a scheduled `GET` against a path on your own site. It goes through the same front door a visitor gets, so whatever serves that path answers it: a [Zero](/zero-runtime) endpoint, a [Functions](/functions) route, or a static file. +A cron is a scheduled `GET` against a path on your own site. It goes through the same front door a visitor gets, so whatever serves that path answers it: a [Zero](/zero-runtime) page, a [Functions](/functions) route, or a static file. A response under 400 is a success and is silent. A response of 400 or worse, or a fatal error, is a failure and is reported. diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index f49b7949..c5ec0e3b 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -1,69 +1,75 @@ --- title: Database -description: Every Zero Space gets its own MySQL. Declare tables in your capsule, migrate on publish, read rows with sf db, and open a single-use SQL console when you need one. +description: Every Space gets its own MySQL. A Zero app's collections and tables live there, Spacefast migrates it on publish, and a single-use SQL console opens it when you need raw SQL. sidebar: order: 2 --- -There's no connection string for this database — your code reaches it through `ctx.db` or `env.DB`, or through a single-use SQL console when you need raw SQL. +There's no connection string for this database. Your code reaches it through Zero's `db` or a worker's `env.DB`, and you reach it through a single-use SQL console. :::note[Don't have a database yet?] -For [Zero](/zero-runtime), start with `sf init --runtime zero`, or add the full [runtime block](/zero-runtime#declare-it) to `sf.jsonc`, including `kind: "zero"`, `server`, and `client` entry paths. The server file is required; Zero can generate a client shell. Publish the app before querying its live database. A [Functions](/functions#what-is-on-env) worker also gets a database through `env.DB`, but the Zero table commands do not apply to it. +Start a [Zero](/zero-runtime) app with `sf init --runtime zero`. Its `zero.config.ts` declares the collections and tables, and Spacefast creates them when you publish. A [Functions](/functions#what-is-on-env) worker also gets the database, through `env.DB`. ::: ## What it is -Every Space that runs [Zero](/zero-runtime) gets one database, the MySQL that lives on the machine serving the site. A [Functions](/functions) worker gets one too, as `env.DB`. +Every Space has one database, the MySQL that lives on the machine serving the site. A [Zero](/zero-runtime) app keeps its collections and tables there. A [Functions](/functions) worker gets it as `env.DB`. -You do not create it, size it, or connect to it. Your capsule declares the tables. The platform applies the migration when a version finalizes. +You do not create it, size it, or connect to it. Your config declares the schema, and Spacefast applies it when a version initializes. :::warning[There is no external connection string] -No route, flag, or dashboard field returns a DSN, and none is coming. Your code reaches the database through `ctx.db` (Zero) or `env.DB` (Functions). For everything else there is the SQL console below. +No route, flag, or dashboard field returns a DSN. Your code reaches the database through `db` (Zero) or `env.DB` (Functions). For everything else there is the [SQL console](#the-sql-console). ::: ## Query it from code -In a Zero capsule the schema you declared is the API. +In a Zero app, hooks and actions read and write through `db`. Your schema is the API, and its column and relation names are checked at build time. -```ts server/index.ts -import { capsule, mutation, query, string, table } from "@spacefast/zero/server"; +```ts zero.config.ts +import { action, db, defineConfig, integer, table, text } from "zero"; -export default capsule({ - name: "notes", - schema: { - notes: table({ body: string(), ownerId: string() }).index("by_owner", ["ownerId"]), - }, - queries: { - recent: query(async (ctx) => - ctx.db.notes - .withIndex("by_owner", (range) => range.eq("ownerId", ctx.auth.userId)) - .order("desc") - .take(20), - ), - }, - mutations: { - add: mutation(async (ctx, body: string) => { - await ctx.db.notes.insert({ body, ownerId: ctx.auth.userId }); - ctx.invalidate("recent"); +const notes = table({ + body: text().notNull(), + ownerId: integer().notNull(), +}); + +export default defineConfig({ + tables: { notes }, + actions: { + recent: action({ + input: { ownerId: integer().notNull() }, + access: "authenticated", + async handler({ ownerId }) { + return await db.query.notes.findMany({ + where: { ownerId }, + orderBy: { id: "desc" }, + limit: 20, + }); + }, + }), + add: action({ + input: { body: text().notNull(), ownerId: integer().notNull() }, + access: "authenticated", + async handler(input) { + const [row] = await db.insert(notes).values(input).returning(); + return row; + }, }), }, }); ``` -| Call | Available in | What it does | -| --- | --- | --- | -| `get(id)` | queries and mutations | One row by id, or `null` | -| `withIndex(index, range?)` | queries and mutations | A query builder over a declared index | -| `.order("asc" \| "desc")` | builder | Direction | -| `.collect()` | builder | Every matching row | -| `.take(n)` | builder | The first `n` | -| `.first()` | builder | One row or `null` | -| `.paginate({ cursor, numItems })` | builder | `{ page, continueCursor, isDone }` | -| `insert(value)` | mutations and write endpoints | Returns the new row | -| `update(id, patch)` | mutations and write endpoints | Returns the row or `null` | -| `delete(id)` | mutations and write endpoints | Returns a boolean | - -Index ranges take `eq`, `gt`, `gte`, `lt`, and `lte`. Every call is async, so await all of them. +| Call | What it does | +| --- | --- | +| `db.query..findMany({ where, orderBy, limit, offset, columns, with })` | Matching rows | +| `db.query..findFirst({ where, ... })` | One row, or `null` | +| `db.insert(table).values(row \| rows).returning()` | Inserts, and returns the new rows | +| `db.update(table).set(patch).where(filter).returning()` | Updates, and returns the changed rows | +| `db.delete(table).where(filter).returning()` | Deletes, and returns the removed rows | + +`where` is an object filter: `{ col: value }`, or `{ col: { eq, ne, gt, gte, lt, lte, in, notIn, like, isNull, isNotNull } }`, with top-level `OR`, `AND`, and `NOT`. A relation key filters through the relation: `{ author: { name: "Alice" } }`. `orderBy` is `{ col: "asc" | "desc" }`. Await every call. + +An action's writes run in one transaction. A throw anywhere rolls all of them back. A Functions worker gets a D1-shaped binding instead. @@ -76,124 +82,54 @@ export default { }; ``` -## Inspect the schema - -```bash -sf db -``` - -`sf db` reads the live version's declared tables, its migration plan, and whether that plan has been applied. It takes an optional target, so `sf db docs` reads the space `docs` without `--space`. A space id, slug, live URL, or custom domain all work. - -```text -Backend mysql -Schema hash sha256:1f9c… -Tables notes, sessions -Migrations 0 pending -``` - -`plan.applied` can come back `null`. That means the control plane has no application record and refuses to guess, not that something failed. - -To read a locally running capsule instead of the live version, point at `sf dev`. - -```bash -sf db --port 4173 -sf db --local-url http://127.0.0.1:4173 -``` - -`--local-url` accepts `localhost`, `127.0.0.1`, or `::1` only. A refused port exits `40`. - -## Read rows - -```bash -sf db dump -sf db dump --table notes --limit 100 -``` +## Migrations -`--limit` is 1 to 100 and defaults to 25. `--table` must be a declared table name matching `/^[A-Za-z_][A-Za-z0-9_]*$/`. Omit it to dump every declared table. The dump reads through the space's live runtime, so it needs the app to be reachable. +Your `zero.config.ts` is the schema. When you publish a version that changes it, Spacefast diffs it against the schema the Space already runs and applies the difference while the new version initializes, before it can go live. -## Back up everything +To see the SQL a change produces, build locally and read the newest file in `zero/migrations/`: ```bash -sf db export -sf db export docs --out ./docs-backup.json +sf zero build ``` -`sf db export` walks every declared table in stable keyset pages of 500 rows and writes one JSON file. Without `--out` the file is `spacefast-backup--.json`. - -The write is atomic and mode `0600`. If the export fails part-way, an existing destination is left exactly as it was. - -## Migrations +:::warning[Rollback does not roll back the schema] +Every version of a Space shares one database, migrated forward to the newest schema. Rolling back to older code keeps the newer tables and columns, so make schema changes the older code can live with. +::: -Your capsule's tables are the schema. Publishing a version that changes them plans a migration, and the platform applies it at finalize. `sf db migrate` applies the live plan again, or previews what your source tree would change on top of it. +To rename a collection or table without moving its data, keep the old physical name with `storageName`: -```bash -sf db migrate -sf db migrate --drop -sf db migrate --rename +```ts +const stockItems = table(fields, { storageName: "stock_items" }); ``` - - - Edit `table(...)` and `.index(...)` in your server entry. - - - Run `sf db migrate` from the project directory. It computes the plan locally, prints the operations, and asks before applying. - - - Drops and renames are refused unless you pass `--drop` or `--rename`. The two are mutually exclusive. - - - -| Operation | Needs a flag | -| --- | --- | -| `create_table`, `add_column`, `add_index` | no | -| `drop_table`, `drop_column`, `drop_index` | `--drop` | -| `rename_table`, `rename_column` | `--rename` | - -`sf db migrate` decides Zero-ness from your local `sf.jsonc` before it contacts the API. A static or Functions project fails with `runtime_kind_mismatch` and exit code `30`, having sent no request. `--previous-artifact` diffs against a named artifact instead of the live schema. - -`sf db`, `sf db dump`, and `sf db migrate` are Zero-only. A worker declares no table set, so they answer `runtime_kind_mismatch` for one. +A build that drops one collection or table while adding another is refused as an ambiguous replacement. Rename with `storageName` instead, and remove storage in its own change. ## The SQL console -When you need raw SQL, the dashboard mints a single-use phpMyAdmin sign-in URL. - -Open **Database** for the space and click **Open db console**. The page shows **Backend**, **Migrations**, a **Database tables** list, and per-table **Columns** and **Indexes**. - -From the CLI: +When you need raw SQL, the CLI mints a single-use phpMyAdmin sign-in URL. ```bash sf db console sf db console --show-secret ``` -`sf db console` opens a browser by default, because the URL grants full SQL authority over the space's database. `--show-secret` prints it instead, and is the deliberate override. The URL is masked in `--json` receipts unless you pass it. - -The console is also how you reach a Functions worker's tables, since the schema routes do not apply to one. +`sf db console` opens a browser by default, because the URL grants full SQL authority over the Space's database. `--show-secret` prints it instead, and is the deliberate override. The URL is masked in `--json` receipts unless you pass it. Opening the console needs write access to the Space. -Until the space runs a Zero app or a worker, the Database page shows **Database isn't enabled**. +A Zero app's tables share a prefix that starts with `sfz_`. ## Limits | Limit | Value | | --- | --- | | Databases per Space | 1 | -| `sf db dump --limit` | 1 to 100, default 25 | -| Export page size | 500 rows, fixed | -| Field types you can declare | `string()`, `boolean()`, `id("table")` | -| Reserved column names | `id`, `createdAt`, `updatedAt` | -| Reserved index name | `by_creation` | +| Console URL | Single-use | ## API | Operation | Route | | --- | --- | -| [`getSpaceDatabase`](/api/reference/database/getspacedatabase) | `GET /v1/spaces/{spaceId}/db` | -| [`getSpaceDatabaseDump`](/api/reference/database/getspacedatabasedump) | `GET /v1/spaces/{spaceId}/db/dump` | -| [`getSpaceDatabaseExport`](/api/reference/database/getspacedatabaseexport) | `GET /v1/spaces/{spaceId}/db/export` | -| [`migrateSpaceDatabase`](/api/reference/database/migratespacedatabase) | `POST /v1/spaces/{spaceId}/db/migrate` | | `openSpaceDatabaseConsole` | `POST /v1/spaces/{spaceId}/db/console` | ## Next steps -Use [CLI database commands](/cli/db) to inspect tables or export data. +See [CLI database commands](/cli/db) for `sf db console` and crons. diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index 35b238a0..295093a1 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -1,6 +1,6 @@ --- title: Environment variables -description: Set variables on a Space or a team, keep secrets write-only, substitute them into files at build, and read them from a handler as ctx.env +description: Set variables on a Space or a team, keep secrets write-only, substitute them into files at build, and read them from server code at runtime sidebar: order: 3 --- @@ -81,7 +81,7 @@ sf env set API_URL https://api.example.com --branch-value beta=https://beta.exam `--branch-value` takes `branch=value` and repeats. An empty branch or an empty value is a validation error. -### Runtime, `ctx.env` +### Runtime, `env()` Server-only values belong in `.env.server` next to your project. `sf publish` reads that file, syncs every key into the Space's variables **as secrets**, and never uploads the file itself. The shared publish denylist covers `.env.*`, so the bytes stay on your machine. @@ -92,24 +92,26 @@ EOF sf publish ``` -A Zero handler reads it from `ctx.env`. +A [Zero](/zero-runtime) action or hook reads it with `env()`, which returns `null` for a name that isn't set. -```ts server/index.ts -import { capsule, endpoint, json } from "@spacefast/zero/server"; +```ts zero.config.ts +import { action, defineConfig, env } from "zero"; -export default capsule({ - name: "billing", - endpoints: { - charge: endpoint({ mode: "write", method: "POST", path: "/api/charge" }, (ctx) => - json({ configured: Boolean(ctx.env.STRIPE_SECRET_KEY) }), - ), +export default defineConfig({ + actions: { + billingStatus: action({ + access: "editor", + handler() { + return { configured: env("STRIPE_SECRET_KEY") !== null }; + }, + }), }, }); ``` -A [Functions](/functions) worker reads the same values from `env`, its second handler argument. +A Zero app sees **Space** variables only. Team variables do not reach it, so set what the app needs on the Space. -`.env.lakebed.server` is accepted as an alias for capsule portability. +A [Functions](/functions) worker reads the same values from `env`, its second handler argument. An anonymous first publish, before you have an account, syncs nothing and reports `skipped: "anonymous"`. diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index 8f96dcb0..915445d5 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -5,9 +5,9 @@ sidebar: order: 1 --- -Functions can detect a supported worker layout when you publish, without an explicit `runtime` block. Custom layouts need a declaration, and [Zero](/zero-runtime#declare-it) is always declared explicitly. +Functions can detect a supported worker layout when you publish, without an explicit `runtime` block. Custom layouts need a declaration. A [Zero](/zero-runtime) app is a project with a `zero.config.ts`. -Functions runs your code as a worker. Use it when you need npm packages or framework output like OpenNext Next.js. If you want live queries in the browser and a schema the platform migrates for you, use [Zero](/zero-runtime) instead. Both can call `fetch()`. One version declares one runtime. +Functions runs your code as a worker. Use it when you need npm packages or framework output like OpenNext Next.js. If you want collections, tables, and actions compiled to WordPress, with a schema Spacefast migrates for you, use [Zero](/zero-runtime) instead. Both can call `fetch()`. One version declares one runtime. Functions is on for every account. diff --git a/content/(dynamic)/logs.mdx b/content/(dynamic)/logs.mdx index 8740ee54..2d187ae6 100644 --- a/content/(dynamic)/logs.mdx +++ b/content/(dynamic)/logs.mdx @@ -35,7 +35,7 @@ The first positional is a target, which can be a Space id, slug, live URL, or cu | `--follow`, `-f` | off | Polls every 2 seconds until Ctrl-C | | `--cursor` | none | Continues from the cursor the previous page printed | | `--request-id` | none | Runtime only. Everything logged while serving one request | -| `--handler` | none | Runtime only. Lines from one Zero mutation or Functions handler | +| `--handler` | none | Runtime only. Lines from one Functions handler | `--request-id` and `--handler` on the access log are refused rather than ignored, with `Flags --request-id and --handler only apply to \`sf logs runtime\`.` @@ -58,12 +58,12 @@ Runtime entries carry more. | `timestamp` | When the line was written | | `level` | `debug`, `info`, `warn`, or `error` | | `requestId` | Nullable. Pass it to `--request-id` | -| `handlerName` | Nullable. Zero names the mutation, Functions names the handler | +| `handlerName` | Nullable. The Functions handler that wrote the line | | `versionId` | Nullable | | `message` | What you logged | | `metadata` | The data bag you passed | -In a Zero capsule, `ctx.log.info(message, data)` writes here, as do `warn` and `error`. A static Space returns an empty runtime stream, because it runs no code. +A [Functions](/functions) worker's console output lands here. A static Space returns an empty runtime stream, because it runs no code. ## Two things that surprise people diff --git a/content/(dynamic)/meta.ts b/content/(dynamic)/meta.ts index d0c68f8e..4aa612cd 100644 --- a/content/(dynamic)/meta.ts +++ b/content/(dynamic)/meta.ts @@ -4,5 +4,5 @@ export default defineMeta({ title: "Dynamic", collapsed: false, order: 5, - pages: ["zero-runtime", "functions", "database", "environment-variables", "crons", "object-storage", "logs", "wordpress"], + pages: ["zero-runtime", "functions", "database", "environment-variables", "crons", "object-storage", "logs", "wordpress", "zero-migrate"], }); diff --git a/content/(dynamic)/object-storage.mdx b/content/(dynamic)/object-storage.mdx index 58681a11..13fa4e4a 100644 --- a/content/(dynamic)/object-storage.mdx +++ b/content/(dynamic)/object-storage.mdx @@ -28,28 +28,9 @@ An object is addressed by a bare 32-character hexadecimal id. There is no key, n `filename` is presentation only. Path separators, control characters, `.`, and `..` are refused rather than stripped, so the value you send is the value that comes back. -## Upload from a capsule +## Uploads in a Zero app -The client SDK is the upload path for a [Zero](/zero-runtime) app. - -```tsx client/upload.tsx -import { storage } from "@spacefast/zero/client"; - -async function onFile(file: File) { - const object = await storage.upload(file); - console.log(object.id, object.url); -} -``` - -`storage` has three methods. - -| Method | Returns | -| --- | --- | -| `upload(file: Blob)` | `{ id, contentType, size, url }` | -| `get(id: string)` | A `Response`, or `null` | -| `delete(id: string)` | Nothing | - -In local `sf dev`, uploads live in memory. +A [Zero](/zero-runtime) app handles its own uploads. Declare a `media()` column for images your content shows, or a `file()` column for private files that download through a signed link valid for five minutes. ## Upload from the API @@ -78,7 +59,7 @@ sf storage rm 0f3a2c19b74e4d0f8c6e5a2b1d9f4c73 `sf storage rm` confirms first. The id must be the 32-character id the list prints. A prefixed value like `private/` is refused. Delete is idempotent, so removing an object that is already gone prints ` was already gone.` rather than failing. -There is no `sf storage upload`. Uploads go through the client SDK, the API, or the dashboard. +Upload from the CLI with `sf storage put `, or through the API or the dashboard. ## Dashboard diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index f1f28c51..bd01ad67 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -48,7 +48,7 @@ The host runs WP-CLI as a job and does not return its output; only success or fa Run with --local against a local WordPress to see what the command prints. ``` -When you need the bytes, run the same command locally, or reach for `sf db` and the database console. +When you need the bytes, run the same command locally, or open the database console with `sf db console`. ```bash sf wp --local --path ./wordpress core version @@ -76,12 +76,6 @@ The API route is `runSpaceWpCliCommand`. It takes an already-split argument vect An Ability is a named operation with a schema that the Space's WordPress publishes for agents to call. Some come from the runtime itself and exist on every Space. -```bash -sf zero abilities -``` - -The table prints `ABILITY | CATEGORY | MODE | REQUIRES`. The `REQUIRES` column is what answers "why did that come back 403". - ### Storage Abilities Five Abilities in the `zero-storage` category are registered by the runtime, so every Space publishes them whether or not it has ever published an app. They are the media library, the same objects [Storage](/object-storage) manages. @@ -100,19 +94,7 @@ WordPress decides which file types `zero/storage-upload` accepts. ### Call one -```bash -sf zero call zero/storage-list -sf zero call zero/storage-list --input '{"folder":"press","perPage":20}' -sf zero call zero/storage-upload --input @upload.json -``` - -`sf zero call` mints a short-lived token for the Space, runs the Ability against the Space's own WordPress REST API, and prints the HTTP status and the JSON result. The token is never printed and never appears in the `--json` receipt. - -`--input` takes inline JSON or `@path` to read it from a file. `--credential mch_...` picks a machine credential when the Space has more than one. A non-2xx answer is `zero_ability_failed` and exits 1, with the whole receipt in `details`. A 403 means the credential's Grant does not permit that Ability. - -### Mint a token yourself - -An agent working from the API does the same thing in two calls. +Mint a short-lived token, then call the Ability on the Space's own host. ```bash curl -X POST https://api.spacefast.com/v1/spaces/spc_xxxxxxxx/zero/token \ @@ -142,11 +124,9 @@ The plain `Authorization` header belongs to your application and passes through The token is bound to the host the Space serves on, and the runtime re-derives what it may do from the credential's Grant on every request. The `capabilities` in the response are what to expect, not a promise: a credential scoped away from a path answers there with less, never with more. -`listSpaceZeroAbilities` is the API side of `sf zero abilities`. - ## The database -The same WordPress owns a MySQL database. `sf db` reads its schema and rows, and the dashboard mints a single-use SQL console URL for raw queries. See [Database](/database). +The same WordPress owns a MySQL database. `sf db console` mints a single-use SQL console URL for raw queries. See [Database](/database). ## Next steps diff --git a/content/(dynamic)/zero-migrate.mdx b/content/(dynamic)/zero-migrate.mdx new file mode 100644 index 00000000..e0ac74f7 --- /dev/null +++ b/content/(dynamic)/zero-migrate.mdx @@ -0,0 +1,79 @@ +--- +title: Migrate from Zero 0.8 +description: Zero 0.8 apps keep working and publishing. Automatic migration to the current Zero is coming; until then, port the source by hand with sf zero migrate app. +sidebar: + order: 8 +--- + +Your Zero 0.8 app keeps working. Its Space keeps serving, and you can keep publishing updates to it. New Spaces start on the current Zero, the `zero.config.ts` compiler described in [Dynamic sites with Zero](/zero-runtime). + +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" }`. + +## What's coming + +Automatic migration is coming. This page will show how to run it when it ships. Until then, nothing changes for your app, and you do not need to do anything. + +## Port the source now + +To start early, convert the source on your machine. The converter 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` whenever the capsule declares tables: the converter binds each table to the exact MySQL table the Space already stores it in. 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. + +Keep the 0.8 source, the candidate, its `zero/migrations/`, and `connector-roles.json` together. + +## 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-runtime#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 the port + +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 a plain publish would leave the posts, users, and sessions behind. The automatic migration is the way across. It copies the data and switches the Space in one operation. + +The candidate belongs to this Space. `--space` binds its tables by name to the tables this Space already stores, so it is not a starting point for a different Space. + +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 from the candidate's `sf.jsonc` if it has one. diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 572cf1c6..0121988f 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -1,287 +1,271 @@ --- title: Dynamic sites with Zero -description: Zero is the runtime a Space gets when its project declares it. A database, HTTP endpoints, live queries, crons, and storage, from one TypeScript app. +description: Zero compiles one zero.config.ts (collections, tables, actions, and TSX pages) into WordPress, and Spacefast hosts it on your Space. sidebar: order: 0 --- -Zero is available on every account, with [usage allowances and metered rates](/usage). To use it, declare the full [runtime block](#declare-it) in `sf.jsonc` and provide the server entry. +Zero turns one `zero.config.ts` into a WordPress app: collections, tables, actions, access rules, and pages. `sf publish` sends the source, and Spacefast compiles it and runs it on the Space's own WordPress. Zero is available on every account, with [usage allowances and metered rates](/usage). ## What Zero is -Zero is the runtime a Space gets when its project declares it. Your server code runs on the machine that already serves the site, next to that space's own MySQL database, inside a QuickJS runner. You write one TypeScript app. It declares tables, queries, mutations, actions, and HTTP endpoints. `sf publish` compiles it into a **capsule** the platform installs on the space. +Zero is a compiler. You describe your data and your server logic in TypeScript, and Zero compiles it to WordPress plugins: a post type for each collection, a SQL table for each table, REST routes for both, and plain PHP for your hooks and actions. Nothing runs JavaScript on the server. -There is no account-level switch to enable. +A project is a Zero app when it has a `zero.config.ts` at its root. There is no runtime block to declare and no switch to turn on. -The other runtime is [Functions](/functions), a worker for npm-heavy code and framework output like OpenNext. One version declares one runtime, not both. +The other runtime is [Functions](/functions), a worker for npm-heavy code and framework output like OpenNext. -## Declare it - -Zero is never inferred. A runtime that owns a database does not spring into existence from a directory shape, so both entry paths are required. - -```jsonc sf.jsonc -{ - "$schema": "https://spacefast.com/schemas/sf.json", - "name": "my-app", - "runtime": { - "kind": "zero", - "server": "server/index.ts", - "client": "client/index.tsx" - } -} -``` - -The fastest way to get that file plus a working app is the scaffold. +## Start an app ```bash -sf init my-app --runtime zero --template todo +sf init my-app --runtime zero cd my-app -sf dev +sf zero dev sf publish ``` -`--template` accepts `todo`, `guestbook`, or `contact`. The client entry is optional on disk. If no file backs it, the build generates a shell and the capsule is server-only. The server entry is not optional. +`sf init --runtime zero` writes a starter `zero.config.ts` named after the directory. `sf zero init` writes the same file. `sf zero dev` runs the app on a local WordPress and rebuilds when you save. `sf publish` publishes it to a new Space. -## What you get +The starter holds one collection, one table, and one action: -| Piece | What it is | Page | -| --- | --- | --- | -| Database | The space's own MySQL. Your capsule declares the tables; the platform migrates them when a version finalizes. | [Database](/database) | -| Endpoints | Plain HTTP routes in your capsule, for webhooks and callers outside the app. | This page | -| Queries, mutations, and actions | The read, transactional write, and outbound-work paths your client calls. Queries are live. | This page | -| Environment variables | `ctx.env`, from `.env.server` or `sf env`. | [Environment variables](/environment-variables) | -| Crons | Scheduled `GET` requests to your own paths, declared in `sf.jsonc`. | [Crons](/crons) | -| Storage | Runtime object storage for visitor uploads. | [Storage](/object-storage) | -| Logs | What your handlers wrote, plus every request the edge served. | [Logs](/logs) | +```ts zero.config.ts +import { action, collection, db, defineConfig, table, text, timestamp } from "zero"; -## The `@spacefast/zero` package +// Collections are WordPress posts: they show up in wp-admin and the REST API. +export const posts = collection({ + title: text().notNull(), + body: text(), +}, { title: "title" }); -The scaffold's `package.json` pins the package to the compiler's own version. Nothing you install reaches the page. The build resolves the SDK, preact, and the rest from the platform, so these dependencies exist for types and editor completion. +// Tables are plain MySQL tables for app data. +export const signups = table({ + email: text().notNull().unique().max(191), + createdAt: timestamp().notNull().defaultNow(), +}); -| Import | Use it from | What it holds | -| --- | --- | --- | -| `@spacefast/zero/server` | `server/` | `capsule`, `query`, `mutation`, `action`, `endpoint`, `table`, field constructors, response helpers | -| `@spacefast/zero/client` | `client/` | `useQuery`, `usePaginatedQuery`, `useMutation`, `useAction`, `useAuth`, `storage`, `Router` and friends | -| `@spacefast/zero/kit` | `client/` | The component set: `Button`, `Card`, `Input`, `Dialog`, `Table`, and about 30 more | -| `@spacefast/zero/charts` | `client/` | `LineChart`, `BarChart`, `Sparkline`, `StatTile` | +export default defineConfig({ + name: "my-app", + collections: { posts }, + tables: { signups }, + actions: { + signup: action({ + input: { email: text().notNull() }, + access: "anyone", + async handler({ email }) { + const [row] = await db.insert(signups).values({ email: email.trim().toLowerCase() }).returning(); + return { id: row?.id ?? null }; + }, + }), + }, +}); +``` -`action`, `useAction`, and the `actions` capsule field require compiler 0.5.0 or later. +Everything comes from the `"zero"` module. -## The minimal app +## What you get -Server code declares the schema and the handlers. +| Piece | What it is | +| --- | --- | +| Collections | Editorial content. Each is a WordPress post type with the block editor, drafts, and revisions | +| Tables | App data in its own SQL table | +| REST routes | `/wp-json/zero/v1/` for every collection and table, with relations loaded through `with` | +| Actions | Named server functions at `POST /wp-json/zero/v1/actions/` | +| Hooks | Code that runs before or after a collection write, inside the same transaction | +| Access rules | Who may read and write each collection, table, field, and action | +| Sign-in | Accounts, sessions, guests, passkeys, and MFA through the `zero-identity` plugin | +| Jobs and webhooks | Background work that runs after a request commits | +| Pages | TSX routes and layouts that WordPress renders per request | +| Typed client | A `fetch` client generated from your schema | + +Your data lives in the Space's MySQL database. See [Database](/database). + +## Collections and tables + +Pick the storage by what the data is. A `collection()` is content people edit: it gets a WordPress post type, an editor in wp-admin, drafts, and revisions. A `table()` is app data: it gets its own SQL table and nothing else. + +```ts zero.config.ts +import { collection, defineConfig, defineRelations, integer, richText, table, text } from "zero"; + +const posts = collection({ title: text().notNull(), body: richText() }, { content: "body" }); +const stock = table({ + postId: integer().references(() => posts.id, { onDelete: "cascade" }), + quantity: integer().notNull().default(0), +}); -```ts server/index.ts -import { boolean, capsule, endpoint, mutation, query, string, table, text } from "@spacefast/zero/server"; +const collections = { posts }; +const tables = { stock }; +const relations = defineRelations({ ...collections, ...tables }, (r) => ({ + stock: { post: r.one.posts({ from: r.stock.postId, to: r.posts.id }) }, + posts: { stock: r.many.stock() }, +})); -export default capsule({ - name: "notes", - schema: { - notes: table({ - body: string(), - done: boolean().default(false), - ownerId: string(), - }).index("by_owner", ["ownerId"]), - }, - queries: { - notes: query(async (ctx) => - ctx.db.notes - .withIndex("by_owner", (range) => range.eq("ownerId", ctx.auth.userId)) - .order("desc") - .collect(), - ), - }, - mutations: { - addNote: mutation(async (ctx, body: string) => { - await ctx.db.notes.insert({ body, done: false, ownerId: ctx.auth.userId }); - ctx.invalidate("notes"); - }), - }, - endpoints: { - status: endpoint({ mode: "read", method: "GET", path: "/api/status" }, () => text("ok")), - }, -}); +export default defineConfig({ collections, tables, relations }); ``` -Client code reads through queries and writes through mutations. - -```tsx client/index.tsx -import { useMutation, useQuery } from "@spacefast/zero/client"; -import { Button, Card } from "@spacefast/zero/kit"; - -export function App() { - const notes = useQuery<{ id: string; body: string }[]>("notes"); - const addNote = useMutation<[body: string], void>("addNote"); - return ( - - -
    {notes.map((note) =>
  • {note.body}
  • )}
-
- ); -} -``` +Every row gets an `id`. The registry key is the name everywhere: `shoppingList` is `zero.shoppingList` in the client and `/wp-json/zero/v1/shopping_list` over REST. -`capsule()` takes `name`, `favicon`, `schema`, `queries`, `mutations`, `actions`, and `endpoints`, and throws on a key it does not know. +Column builders include `text()`, `integer()`, `real()`, `boolean()`, `timestamp()`, `date()`, `json()`, `email()`, `url()`, `slug()`, `richText()`, `media()`, `select()`, `group()`, `repeater()`, and `blocks()`. Chain `.notNull()`, `.default(value)`, `.unique()`, `.min(n)`, `.max(n)`, `.pattern(/re/)`, or `.references(...)` to constrain one. A modifier that makes no sense for its column, like `.max()` on a boolean, fails the build at that line. -### Tables +## Access rules -`table(fields)` takes five field constructors: `string()`, `boolean()`, `number()`, `id("otherTable")`, and `userId()`. Store dates as strings. The wire schema also accepts `json`, but the SDK has no `json()` field constructor. `.default(value)` makes a field optional on insert. Every row gets `id`, `createdAt`, and `updatedAt`, so those three names are reserved, as is the built-in `by_creation` index. Field and index names must match `/^[A-Za-z_][A-Za-z0-9_]*$/`. A leading underscore is allowed. Declare an index with `.index(name, fields)` and read it through `withIndex`. +Every collection and table takes `access: { read, create, update, delete }`. Each rule is `true`, `false`, `"anyone"`, `"authenticated"`, a role, or a function of `ctx`. Roles rank `"guest" < "subscriber" < "author" < "editor" < "admin"`, and a role admits itself and everything above it. -### Handler context +```ts +{ access: { read: "anyone", create: "author", update: "author", delete: "editor" } } +``` -Every query, mutation, action, and endpoint handler takes `ctx` first. +The default is read by `"anyone"`, write by `"editor"`. Actions default to `"editor"` too, so an action a visitor calls needs `access: "anyone"`. The rules hold on every path into the data: Zero's REST routes, WordPress's own `/wp/v2` routes, wp-admin, and Abilities. -| On `ctx` | Queries and read endpoints | Mutations and write endpoints | Actions | What it is | -| --- | --- | --- | --- | --- | -| `db` | read only | read and write | read only | `get`, `withIndex`, and on the write side `insert`, `update`, `delete` | -| `auth` | yes | yes | yes | `userId`, `displayName`, `isGuest`, `isSignedIn`, `isAuthenticated`, `provider` | -| `env` | yes | yes | yes | Server-only variables. See [Environment variables](/environment-variables) | -| `log` | yes | yes | yes | `info`, `warn`, `error`, each with an optional data bag | -| `invalidate` | no | yes | no | Names the live queries this mutation changed | +## Actions -A signed-out visitor is a guest with a real `userId` (`guest:…`), so `!ctx.auth.userId` never rejects anyone. Gate sign-in on `ctx.auth.isAuthenticated`. +An action takes typed input and returns JSON. `input` uses the same column builders as your schema, so a bad request is refused before your handler runs. -A query can run again when it satisfies a subscription, so any `fetch()` it makes can run again too. Queries have no `invalidate`; only write handlers tell subscriptions to refresh. +```bash +curl -X POST https://my-site.view.fast/wp-json/zero/v1/actions/signup \ + -H 'Content-Type: application/json' \ + --data '{"email":"sam@example.com"}' +``` -An action runs without a transaction. It reads the database but cannot write to it, and it has no `invalidate`. Use it when a call causes an external effect, not to gain more runtime: the whole action has a 5-second deadline. To keep what it fetched, hand the result to a mutation. The client calls one with `useAction(name)`. +A successful call answers `{ "data": ... }` with whatever the handler returned. Throw `new HttpError(status, code, message)` to choose the error response. Any throw rolls back every write the request made. -`invalidate()` takes **query names, not table names**. `ctx.invalidate("notes")` refreshes every `notes` subscription whatever arguments it carries. Calling it with nothing is the safe default and refreshes every live query on the page. +## Hooks -### Endpoints +Hooks run around collection writes: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, and `afterDelete`. A `before` hook can change the data or throw to refuse the write. -`endpoint(route, handler)` takes `{ method, path, mode }`, where `mode` is `"read"` or `"write"`. Both `method` and `path` must be literal strings, because the compiler reads them from source without running it. The handler gets `(ctx, req)`, where `req` carries `method`, `path`, `url`, `headers`, `query`, and `text()` / `json()` / `bytes()`. +```ts +hooks: { + posts: { + beforeCreate(data) { + data.title = data.title.trim(); + if (data.title === "") throw new HttpError(422, "invalid_input", "A post needs a title"); + }, + }, +}, +``` -Return one of the response helpers: `json(value)`, `text(value)`, `empty()` for a 204, `redirect(url)` for a 302, or `new ImageResponse(element)` for a rendered 1200x630 image. +## What compiles -## The request lifecycle +Hooks, actions, and access functions are a checked subset of TypeScript compiled to PHP. Functions, closures, spread, classes, `async`/`await` on Zero calls, and the usual control flow all work. `eval`, generators, `any`, and dynamic `import()` do not. Anything outside the subset fails the build at `file:line:col` with a fix: -1. Static bytes win. Anything committed in the version serves from disk without waking your code. -2. Platform paths are reserved. First segments `__spacefast`, `__stattic`, and `__span` belong to the platform, as do `/auth/` and `/_spacefast/` inside the app. -3. Your endpoint table matches. A hit runs the handler in the QuickJS runner on the space's own machine, in one transaction with the database. -4. Everything else falls to the app shell, which serves your client bundle. +```bash +sf zero check +sf zero check --json +``` -Queries, mutations, and actions do not get their own URLs. The client sends them over `/__zero/run`. When a mutation commits, the runtime publishes an invalidation event. Every browser holding a matching subscription re-runs its query. That is the whole realtime story. Do not poll. +Imports can be `"zero"`, your own relative modules, and installed source packages. `sf publish` captures the installed packages it needs. Spacefast never runs `npm install`. -An endpoint cannot claim `/`, `/index.html`, `/client.js`, `/auth/callback`, anything under `/auth/`, or anything under `/_spacefast/`. The shell, the client bundle, and the sign-in flow already answer those. +## Call it from a frontend -## What a capsule is, and what compiles +`sf zero build` and `sf zero dev` write a typed client to `zero/client/`. It is plain `fetch`, so it works from any framework. -A capsule is the value `capsule({...})` returns. `sf publish` runs esbuild over your tree. It produces one ESM server bundle for the runner, one browser client bundle, and a migration plan for the tables you declared. Rolling a version back rolls all three back together, because they travel with the version. +```ts +import { createClient } from "./zero/client/index.ts"; -Only three source roots are read: `client/`, `server/`, and `shared/`. Beyond them the compile admits `package.json`, `.env.server`, and `.env.lakebed.server` and nothing else. `shared/` imports its own relative files only, no packages at all. Client code cannot import the server SDK, and server code cannot import client files. +const zero = createClient({ url: "https://my-site.view.fast/wp-json/zero/v1" }); -| Limit | Value | -| --- | --- | -| Server bundle | 768 KiB | -| Client bundle, your code and your npm packages | 8 MiB | -| Server imports | `@spacefast/zero/server`, `preact`, `preact/jsx-runtime`, `preact/jsx-dev-runtime` | -| Client imports | Anything except the server SDK and `node:*` built-ins | -| Endpoint request body | 2 MiB | -| Rejected everywhere | `eval`, `Function`, `require()`, dynamic `import()`, `process`, `__dirname`, `__filename`, `Bun`, `Deno` | -| Rejected in `server/` and `shared/` | `window`, `document`, `localStorage`, `WebSocket`, `Worker`, `XMLHttpRequest`, `globalThis` | +const posts = await zero.posts.list({ orderBy: { title: "asc" } }); +await zero.actions.signup({ email: "sam@example.com" }); +``` -Platform modules do not count against your client cap. The SDK, kit, charts, preact, recharts, and lucide serve from immutable same-origin URLs the browser keeps across publishes. +Every argument and result is typed from your schema. A published Space also serves its client at `/__spacefast/client.js`, with types at `/__spacefast/client.d.ts`. -### Outbound HTTP +To serve a static frontend from the same Space, put its files in `public/`. `sf publish` uploads them with the app. -`fetch()` works in every Zero handler, HTTPS only. A mutation or write endpoint holds its database transaction open while it waits, so each fetch has a 5-second timeout. Queries and read endpoints get 10 seconds per fetch. An action gets 5 seconds for the whole invocation, shared across every fetch it makes. +## Pages -How far `fetch()` reaches depends on who owns the space: +Pages are TSX files under `pages/`. They compile at build time, and WordPress renders them on every request with the visitor's own data, through the same access rules as the REST routes. -| Space | Reaches | +| File | Route | | --- | --- | -| Claimed | Any public HTTPS host | -| Unclaimed | Only the trusted list: `api.anthropic.com`, `api.openai.com`, `api.github.com`, `api.stripe.com`, `api.groq.com`, `api.mistral.ai`, `api.together.xyz`, `generativelanguage.googleapis.com`, `openrouter.ai` | - -From an unclaimed space, `fetch()` to any other host throws a `SpacefastFetchError` with code `zero_fetch_host_untrusted` and the message "This host isn't reachable from an unclaimed space. Claim the space to reach any public host." Claiming opens it up without a republish. Private addresses and cloud metadata stay blocked either way. +| `pages/page.tsx` | `/` | +| `pages/about/page.tsx` | `/about` | +| `pages/notes/[slug]/page.tsx` | `/notes/`, one per post in a collection, with `collectionPage("notes")` | +| `pages/layout.tsx` | Wraps every page below it | +| `pages/not-found.tsx` | The body of a 404 | -Outbound access has nothing to do with who can open your space. Calling an API does not make the space public or touch its Grants. +A static route beats an item route at the same path, so `notes/featured` wins over `notes/[slug]`. WordPress's own paths, like `/wp-admin` and `/wp-json`, cannot be claimed. Every page response is `Cache-Control: private, no-store`, because its HTML depends on the visitor. -### Example: invite People from your app +A page reads data with a `load()` function and renders request values through helpers like `` and `orElse()`. Plain `&&` or `?:` on request data is refused at build time, with the line and the helper to use instead. -There is no `ctx.people`. To invite someone to a private space, or resend their invite, call the People API from an action with a [machine credential](/access#machine-credentials). +## Environment variables -```bash -sf share token create --name invites --path '/**' --role manager --show-secret --json \ - | jq -r '.data.token' \ - | sf env set INVITE_TOKEN --value-from-stdin -sf env set INVITE_SPACE_ID spc_123 -sf env set INVITE_ADMINS user-id-1,user-id-2 -``` +Read a Space variable with `env()`. Set it with `sf env set`, or put it in `.env.server` and publish. -The `manager` role carries `access.manage`, which the People routes require. Narrow `--path` to limit where the token can invite people. An invite with `scope: "/docs"` requires `--path '/docs/**'`; the bare `/docs` path does not cover that subtree. `INVITE_ADMINS` lists the `ctx.auth.userId` values allowed to send invites. The People API only works on a claimed space. Each `sf env set` queues a settings-only finalize; the action sees the new value after that operation finishes. - -An action is callable by anyone who can load your app, so gate it before it spends that token. - -```ts server/index.ts -import { action, capsule } from "@spacefast/zero/server"; - -type Ctx = { - auth: { isAuthenticated: boolean; userId: string | null }; - env: Record; -}; - -async function callPeople(ctx: Ctx, path: string, body?: unknown) { - const admins = (ctx.env.INVITE_ADMINS ?? "").split(",").map((id) => id.trim()); - if (!ctx.auth.isAuthenticated || !admins.includes(ctx.auth.userId ?? "")) { - throw new Error("You can't send invites."); - } - const response = await fetch( - `https://api.spacefast.com/v1/spaces/${ctx.env.INVITE_SPACE_ID}/people${path}`, - { - method: "POST", - headers: { - authorization: `Bearer ${ctx.env.INVITE_TOKEN}`, - "content-type": "application/json", - }, - body: body === undefined ? undefined : JSON.stringify(body), - }, - ); - if (!response.ok) throw new Error(`People API answered ${response.status}.`); - return (await response.json()).data; -} +```ts +import { action, env } from "zero"; -export default capsule({ - actions: { - invite: action((ctx, email: string) => - callPeople(ctx, "", { email, grants: [{ scope: "/", role: "viewer" }] }), - ), - resendInvite: action((ctx, personId: string) => - callPeople(ctx, `/${encodeURIComponent(personId)}/resend`), - ), +const checkout = action({ + access: "anyone", + handler() { + return { configured: env("STRIPE_SECRET_KEY") !== null }; }, }); ``` -Call them from the client with `useAction("invite")`. `scope: "/"` covers the whole space; `/docs` covers one subtree. Both routes return the Person in `{ data }`. The invite link itself only ever travels by email. +See [Environment variables](/environment-variables). -## Styling +## Jobs and webhooks -Write Tailwind classes in your JSX. Zero compiles them at publish, so do not add a CSS, PostCSS, or Tailwind pipeline. `@plugin` and `@config` directives are rejected, and `theme.json` plus utility classes are the whole styling story. +A job is background work you enqueue from a hook or an action. It runs after the request commits, never inside it, and a job that throws retries with backoff up to its `retries`. + +```ts +jobs: { + notifyEditors: job({ + input: { postId: integer().notNull() }, + retries: 3, + async handler(input, ctx) { + // send the email + }, + }), +}, +``` -Class names must be static strings. The compiler finds them by scanning source text, so `bg-${tone}-500` produces no CSS for that class. Branch to whole literals instead. +Call `jobs.notifyEditors.enqueue({ postId })` to queue one. A `webhooks` entry POSTs each matching write to a URL, signed in the `zero-signature` header, and retries the same way. On Spacefast a due job can wait up to five minutes before it starts. -Prefer the semantic tokens, which re-skin from `theme.json` and handle light and dark on their own: `canvas`, `surface`, `ink`, `ink-muted`, `line`, `accent`, `success`, `warning`, `danger`. The shadcn names (`bg-background`, `text-muted-foreground`, `bg-primary`) alias onto the same tokens. +## No realtime + +Zero has no live subscriptions. A client sees a change when it reads again. + +## Before you publish a schema change + +Every version of a Space shares one database, migrated forward to the newest schema. Rolling back the code does not roll back the schema. See [Database](/database#migrations). ## Run it locally ```bash -sf dev +sf zero dev +sf zero dev stop +sf zero build ``` -`sf dev` starts the capsule dev server on `http://127.0.0.1:4173`. State lives in memory and resets on restart. Pass `--state-backend sqlite` to keep it between runs. `--watch` is on by default and reloads the runtime when a source file changes. A non-loopback `--host` needs `--allow-network`. +`sf zero dev` boots WordPress through WordPress Studio, with no Docker. It needs Node 22 or newer. The first start downloads WordPress and the plugins the app needs. Keep it running and it rebuilds on every save. It prints the local URL and an admin login for local use only. + +`sf zero build` writes the plugins to `.zero/wp-content/`, new migrations to `zero/migrations/`, and the client to `zero/client/`. Commit `zero/migrations/`, `zero/client/`, and `zero.gen.ts`. `.zero/` is local state, and publish skips it. + +`sf zero check` and `sf publish` use the compiler bundled with the CLI. `sf zero dev` and `sf zero build` need the CLI's native optional package. If it is missing, reinstall the CLI with optional dependencies enabled. + +## Bring a schema from another CMS + +```bash +sf zero import payload ../my-payload-app +sf zero import emdash ../site --out app/zero.config.ts +``` + +`sf zero import` reads a Payload, EmDash, or Sanity schema and writes a new `zero.config.ts`. It never runs code from the other CMS and never overwrites a file. Anything Zero cannot hold refuses the import, and nothing is written. Every write rule starts as denied, so set `access` before you publish. + +Have a Zero app on Spacefast with no `zero.config.ts`? See [Migrate an older Zero app](/zero-migrate). - Migrations, dumps, exports, and the SQL console. + Where the data lives, migrations, and the SQL console. + + + Secrets your actions read with `env()`. Workers, the `functions/` file router, and OpenNext. - - Every key the config schema accepts. - - - Follow what your handlers log. + + Every `sf zero` command and its flags. diff --git a/content/(reference)/config-file.mdx b/content/(reference)/config-file.mdx index cbc05dc4..107265d6 100644 --- a/content/(reference)/config-file.mdx +++ b/content/(reference)/config-file.mdx @@ -52,7 +52,7 @@ sidebar: }, // Run code alongside the files. - "runtime": { "kind": "zero", "server": "server/index.ts", "client": "client/index.tsx" }, + "runtime": { "kind": "functions" }, // Scheduled GETs against this space's own paths. "crons": [{ "path": "/api/digest", "schedule": "daily" }], @@ -121,7 +121,7 @@ While `name` is set in the file, the API refuses to rename the Space. The file w Colors accept hex, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and CSS color names. `theme` is a closed object, so an unknown key is an error. -A `theme.json` file next to `sf.jsonc` is an inference input rather than configuration. Only `settings.color.palette` and `settings.typography.fontFamilies` are read from it. A [Zero](/zero-runtime) capsule's Tailwind compile reads the same file. +A `theme.json` file next to `sf.jsonc` is an inference input rather than configuration. Only `settings.color.palette` and `settings.typography.fontFamilies` are read from it. What these tokens paint, and how to replace those pages outright, is on [Layout, theme, and site pages](/site-pages). @@ -178,16 +178,9 @@ Data location is chosen once, when the Space is created, and never changes. ## Runtime -One runtime per version, discriminated on `kind`. Omit the block for a static site. +Omit the block for a static site. -### `kind: "zero"` - -| Key | Type | Default | What it does | -| --- | --- | --- | --- | -| `runtime.server` | string, required | none | Server entry, relative to the published directory | -| `runtime.client` | string, required | none | Client entry, relative to the published directory | - -Both are required. Zero is never inferred. See [Dynamic sites with Zero](/zero-runtime). +A [Zero](/zero-runtime) app needs no `runtime` block. A `zero.config.ts` at the project root is the declaration, and a project that has one must not also set `runtime.kind` to `"zero"`: publish refuses it with `zero_mixed_generation`. ### `kind: "functions"` diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx index c7682bcd..4c6ee6e0 100644 --- a/content/(reference)/glossary.mdx +++ b/content/(reference)/glossary.mdx @@ -15,10 +15,6 @@ A named operation with a schema that a Space's WordPress publishes for agents to A build turns source code into publishable output. You can build locally or let Spacefast build remotely and publish the result. See [Frameworks and builds](/frameworks). -## Capsule - -The compiled form of a Zero app. `sf publish` turns your TypeScript server code into a capsule, which the platform installs on the Space. See [Dynamic sites with Zero](/zero-runtime). - ## Claim / claiming Attaching an anonymous Space (one published with no account) to a real account and team, so it stops counting down to its expiry deadline. See [Publish without an account](/anonymous-and-claim). @@ -53,4 +49,4 @@ An immutable snapshot created when a publish produces a new version. A ready ver ## Zero -The runtime for an app with live queries and a declared database schema. One TypeScript app compiles into a [capsule](#capsule) with a MySQL database next to your code. [Functions](#functions) also provides a database, and [crons](/crons) can call either runtime or static files. See [Dynamic sites with Zero](/zero-runtime). +The compiler that turns one `zero.config.ts` (collections, tables, actions, and pages) into a WordPress app, hosted on the Space with its own MySQL database. [Functions](#functions) also provides a database, and [crons](/crons) can call either runtime or static files. See [Dynamic sites with Zero](/zero-runtime). diff --git a/content/(serve)/site-pages.mdx b/content/(serve)/site-pages.mdx index e7b409a9..c90bf42e 100644 --- a/content/(serve)/site-pages.mdx +++ b/content/(serve)/site-pages.mdx @@ -59,7 +59,7 @@ A WordPress-compatible `theme.json` next to your config is read as an inference Every other WordPress key rides through and is never read. Slugs are lowercase letters, digits, and dashes, up to 64 characters. The palette takes at most 64 entries and `fontFamilies` at most 32. The file is read up to 64 KiB at publish; a larger one is ignored rather than failing the publish. -`theme.json` is the weakest of the three theme sources. What `sf.jsonc` sets under `theme` overrides it, and what the Space's [Customization](/customization) tab sets overrides both. A [Zero](/zero-runtime) capsule's Tailwind compile reads the same file, so one file styles both. +`theme.json` is the weakest of the three theme sources. What `sf.jsonc` sets under `theme` overrides it, and what the Space's [Customization](/customization) tab sets overrides both. ## `_layout.html` diff --git a/content/cli/builds.mdx b/content/cli/builds.mdx index 3447e50f..bcfcc81b 100644 --- a/content/cli/builds.mdx +++ b/content/cli/builds.mdx @@ -17,7 +17,7 @@ Build and pack project output. sf build [DIR] ``` -Detects build settings, runs the build, and packs the output into an archive. A `zero` runtime compiles the capsule instead and leaves the app shell that `sf publish` would upload. This command talks to your filesystem, not to a Space. +Detects build settings, runs the build, and packs the output into an archive. This command talks to your filesystem, not to a Space. To compile a [Zero](/zero-runtime) app locally, use `sf zero build`. ### Arguments diff --git a/content/cli/db.mdx b/content/cli/db.mdx index b82a52e8..b3549641 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -1,170 +1,19 @@ --- title: db and crons -description: Inspect a Space's database, dump or export rows, apply schema migrations, open a SQL console, and exercise scheduled jobs +description: Open a single-use SQL console on a Space's database, and list and run its scheduled jobs sidebar: order: 22 --- -Inspect, migrate, and dump a Zero Space's database without a connection string. Functions workers use `env.DB` or the SQL console instead of the Zero table commands. You can also trigger a declared cron job on demand. +Open a single-use SQL console on a Space's database, and exercise its scheduled jobs on demand. A [Zero](/zero-runtime) app's schema lives in its `zero.config.ts` and migrates on publish. See [Database](/database). Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`) and the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). -`sf db`, `sf db dump`, and `sf db migrate` are Zero capsules only. A Functions worker declares no table set, so those routes answer `db_not_available` or `runtime_kind_mismatch`. There is no connection string anywhere in the CLI or the API. `sf db console` is the escape hatch. - -## sf db - -Inspect a Space's database. Unlike other topic commands, the bare `sf db` runs the inspect read instead of printing help. - -```text -sf db [target] [--space ] [--local-url ] [--port ] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[target]` | Space ID, slug, live URL, or domain. Defaults to the linked Space | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--local-url=` | none | Read from a local `sf dev` server instead of the live version. Must be `http:` or `https:` with hostname `localhost`, `127.0.0.1`, `::1`, or `[::1]` | -| `--port=` | none | Local `sf dev` server port. Shorthand for `--local-url http://127.0.0.1:`. Integer 1 to 65535 | - -```bash -sf db -sf db docs -sf db --port 4173 -``` - -### Output - -Tables, columns, and the pending migration plan. With `--json`, `data` carries `versionId`, `artifactId`, `backend` (always `mysql`), `migrationMode` (`safe`, `drop`, or `rename`), `schemaHash`, `tables[]`, `migrations[]`, and `plan`. `plan.applied` is `null` when the control plane cannot prove the plan ran. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `network_error` | 40 | A local `sf dev` port refused the connection | - -## sf db dump - -Print rows from a Space's database, for debugging and export. - -```text -sf db dump [target] [--space ] [--table ] [--limit ] - [--local-url ] [--port ] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[target]` | Space ID, slug, live URL, or domain. Defaults to the linked Space | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--table=` | none | Restrict the dump to one table. Must match `^[A-Za-z_][A-Za-z0-9_]*$`, 1 to 128 characters | -| `--limit=` | `25` | Maximum rows per table. Maximum 100. Validated before the request | -| `--local-url=` | none | Read from a local `sf dev` server instead of the live version | -| `--port=` | none | Local `sf dev` server port | - -```bash -sf db dump -sf db dump --table todos --limit 100 -``` - -### Output - -With `--json`, `data` is `{ versionId, artifactId, schemaHash, tables: [{ name, rows }] }`. - -## sf db export - -Export every declared table to one versioned JSON backup. Rows are read in stable keyset pages and the destination is replaced only after the complete export succeeds. - -```text -sf db export [target] [--space ] [--out ] [--local-url ] [--port ] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[target]` | Space ID, slug, live URL, or domain. Defaults to the linked Space | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--out=` | `spacefast-backup--.json` | Backup file path | -| `--local-url=` | none | Read from a local `sf dev` server instead of the live version | -| `--port=` | none | Local `sf dev` server port | - -The file is written to a temp path with mode `0600`, fsynced, then renamed. A failed export leaves an existing destination untouched and unlinks the temp file. - -```bash -sf db export -sf db export docs --out ./docs-backup.json -sf db export --port 4173 -``` - -### Output - -With `--json`, `data` is `{ spacefastBackup: 1, path, spaceId, versionId, exportedAt, schemaHash, tableCount, rowCount }`. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `zero_db_export_cursor_repeated` | 1 | A page returned the same cursor twice | - -## sf db migrate - -Apply the live version's schema migrations, and report what this source tree would change on top of them. - -```text -sf db migrate [source] [--space ] [--previous-artifact ] [--drop] [--rename] -``` - -### Arguments - -| Argument | What it is | -| --- | --- | -| `[source]` | Capsule source directory. Defaults to `.` | - -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--previous-artifact=` | none | Diff against this artifact or finalize payload instead of the live schema | -| `--drop` | `false` | Allow the planned migration to include explicit drop operations. Exclusive with `--rename` | -| `--rename` | `false` | Allow the planned migration to include explicit rename operations. Exclusive with `--drop` | - -`create_table`, `add_column`, and `add_index` are always allowed. `drop_table`, `drop_column`, and `drop_index` need `--drop`. `rename_table` and `rename_column` need `--rename`. - -The command checks the runtime kind locally before it sends anything, and confirms before applying. - -```bash -sf db migrate -sf db migrate --drop -``` - -### Output - -With `--json`, `data` is `{ plan, applied, operation }`. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `runtime_kind_mismatch` | 30 | `sf.jsonc` declares a runtime that is not a Zero capsule. Raised before any request | +There is no connection string anywhere in the CLI or the API. `sf db console` is how you reach the database directly. ## sf db console -Open a phpMyAdmin console on the Space's database, for anything `sf db` and `sf db dump` cannot answer. +Open a phpMyAdmin console on the Space's database. ```text sf db console [target] [--space ] [--show-secret] diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 2b1df5b8..7b9db236 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -30,11 +30,8 @@ sf init [NAME] [--space ] [-o ] [-f] [--runtime static|zero|functio | Flag | Default | What it does | | --- | --- | --- | | `-f, --force` | `false` | Overwrite existing generated files. | -| `--runtime=` | `static` | Project runtime to scaffold. | -| `--template=` | none | Zero starter template to scaffold. | -| `--title=` | none | Zero starter app title. | -| `--no-git` | off | Skip initializing git for a Zero starter app. | -| `--space=` | none | Space ID, live URL, domain, or slug to link the directory to. Env `SPACEFAST_SPACE`. | +| `--runtime=` | `static` | Project to scaffold. `zero` writes a starter [Zero](/zero-runtime) `zero.config.ts`, the same one `sf zero init` writes. | +| `--space=` | none | Space ID, live URL, domain, or slug to link the directory to. Only with `--runtime static`. Env `SPACEFAST_SPACE`. | | `-o, --team=` | default team | Team slug, ID, or name. Env `SPACEFAST_TEAM`. | | `--claim-token=` | none | Space key (`sfc_…`) for an unclaimed anonymous Space. Env `SPACEFAST_CLAIM_TOKEN`. | @@ -44,7 +41,7 @@ sf init [NAME] [--space ] [-o ] [-f] [--runtime static|zero|functio sf init sf init my-site sf init --space docs -sf init my-app --runtime zero --template guestbook +sf init my-app --runtime zero ``` ### What it writes diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index 78bc0420..b16c3ded 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -293,7 +293,7 @@ Start the local dev server. sf dev ``` -`sf dev` runs the project in this directory. It is not a local mirror of what publish serves: for a `zero` runtime it starts the capsule dev server, and for anything else it serves a Pages preview with sample data and the publish-time expander at `/_spacefast/pages/`. There is no static file server for your site on that path. For a framework project, use that framework's own dev server (`next dev`, `vite`), then run `sf publish` when you're ready. +`sf dev` serves a Pages preview with sample data and the publish-time expander at `/_spacefast/pages/`. It is not a local mirror of what publish serves, and there is no static file server for your site on that path. For a [Zero](/zero-runtime) app, run `sf zero dev`. For a framework project, use that framework's own dev server (`next dev`, `vite`), then run `sf publish` when you're ready. | Flag | Default | What it does | | --- | --- | --- | @@ -301,9 +301,6 @@ sf dev | `-p, --port=` | `4173` | Preview port. 1 to 65535 | | `--host=` | `127.0.0.1` | Host interface for the local dev server | | `--allow-network` | off | Allow an explicit non-loopback binding | -| `--state-backend=memory\|sqlite` | `memory` | Local state adapter for a runtime dev server | -| `--watch` / `--no-watch` | `--watch` | Poll source files and reload the runtime after changes | -| `--watch-interval=` | `1000` | Source polling interval in milliseconds | | `--dry-run` | off | Print the dev server plan without starting it | A non-loopback `--host` without `--allow-network` fails with `validation_error`. A project whose `sf.jsonc` declares the `functions` runtime fails with `runtime_dev_unsupported` (exit 2), which points you at your framework's dev server. diff --git a/content/cli/storage.mdx b/content/cli/storage.mdx index fd201d29..249b094c 100644 --- a/content/cli/storage.mdx +++ b/content/cli/storage.mdx @@ -46,7 +46,7 @@ A table of `Id | Size | Type | Uploaded by | Created`, or "No stored objects." A With `--json`, `data` is `{ objects, cursor }`. Each object carries `id`, `contentType`, `createdAt`, `filename`, `size`, `uploaderId`, `sha256`, and `url`. -There is no `sf storage upload`. Uploading is an API call, or `storage.upload()` from the client SDK inside your app. +Upload with `sf storage put` or the API. ### Errors @@ -110,7 +110,7 @@ A bare kind in the first slot works: `sf logs runtime` is the same as `sf logs - | `-f, --follow` | `false` | Poll for new entries every 2 seconds until interrupted with Ctrl-C | | `--cursor=` | none | Continue from the cursor printed by the previous page | | `--request-id=` | none | Runtime logs only. Everything logged while serving one request | -| `--handler=` | none | Runtime logs only. Lines from one Zero mutation or Functions handler | +| `--handler=` | none | Runtime logs only. Lines from one Functions handler | `--follow` polls, it does not stream over a socket. It dedupes by the whole entry and remembers up to 5,000 keys. diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index 5281ee4e..cc46c97e 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -1,138 +1,127 @@ --- title: zero, wp, runtime, pages, design, analytics -description: Inspect and call a Space's Zero Abilities, generate types, run WP-CLI, see what a Space is serving, and read its traffic +description: Start, check, run, and build Zero apps, import a CMS schema, run WP-CLI, see what a Space is serving, and read its traffic sidebar: order: 25 --- -List a live capsule's published Abilities and call the ones your credential permits. Remote `sf wp` needs a provisioned Space and write access, and reports status without the command's printed output. +`sf zero` builds and runs [Zero](/zero-runtime) apps: a `zero.config.ts` compiled to WordPress. The rest of this page covers WP-CLI, runtime status, site pages, design tokens, and analytics. -Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). All except `sf zero import`, `sf pages pull`, `sf pages validate`, and `sf design generate` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). +Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). `sf wp`, `sf runtime status`, and `sf analytics` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). -## sf zero abilities +To publish a Zero app, run `sf publish` from its root. See [Dynamic sites with Zero](/zero-runtime). -List the Abilities the Space's live capsule publishes: what an agent can call, what category it belongs to, and whether it reads or writes. Aliased as `sf zero ls`. +## sf zero init + +Start a Zero app: a starter `zero.config.ts` with one collection, one table, and one action, named after the directory. ```text -sf zero abilities [--space ] +sf zero init [name] [-f] ``` -No arguments, no command-specific flags. +| Argument or flag | What it does | +| --- | --- | +| `[name]` | Directory to create. Omit to initialize the current directory | +| `-f, --force` | Overwrite an existing `zero.config.ts` | + +It also adds `.zero/` and `node_modules/` to `.gitignore`. `sf init --runtime zero` writes the same starter. ```bash -sf zero abilities +sf zero init my-app +sf zero init ``` -### Output - -A table of `ABILITY | CATEGORY | MODE | REQUIRES`, or "This space publishes no Zero Abilities." The `REQUIRES` column is the capability or Grant capabilities the Ability needs, which is what answers "why did that come back 403". - -With `--json`, `data` is the array of Ability rows. +## sf zero check -## sf zero call - -Mint a short-lived Ability token for the Space and run the Ability against the Space's own WordPress REST API. The token is never printed and never appears in the `--json` receipt. +Compile `zero.config.ts` and report diagnostics without writing anything. ```text -sf zero call [--space ] [--credential ] [--input ] +sf zero check [project] [--json] ``` -### Arguments - -| Argument | What it is | -| --- | --- | -| `` | Ability name, as shown by `sf zero abilities`. Required | +Each diagnostic names a `file:line:col` and a fix. `--json` prints the diagnostics envelope instead of text. `sf zero check` uses the compiler bundled with the CLI, so it needs no native download. -### Flags +```bash +sf zero check +sf zero check ./app --json +``` -| Flag | Default | What it does | -| --- | --- | --- | -| `--credential=` | the only one, when there is exactly one | Machine credential id to mint the Ability token for | -| `--input=` | none | Ability input as inline JSON, or `@path` to read it from a file | +## sf zero dev -The minted token travels in `X-SF-Authorization`. The plain `Authorization` header belongs to your application and passes through untouched. +Run the app on a local WordPress and rebuild when its source changes. -```bash -sf zero call content.posts.list -sf zero call content.posts.get --input '{"slug":"hello"}' -sf zero call content.posts.save --input @post.json +```text +sf zero dev [project] [--port ] [--no-watch] [--allow-destructive] +sf zero dev stop [project] +sf zero dev status [project] +sf zero dev wp [project] -- ``` -### Output +`sf zero dev` boots WordPress through WordPress Studio, with no Docker. It needs Node 22 or newer, and the first start downloads WordPress and the plugins the app needs. It watches TypeScript, config, and `public/` files. Ctrl-C stops WordPress and keeps the local database for the next start. -The HTTP status, then the Ability's JSON result. With `--json`, `data` is `{ ability, spaceId, endpoint, credentialId, role, status, ok, result }`. +| Argument or flag | What it does | +| --- | --- | +| `--port=` | Port for a new local site. An existing site keeps its port | +| `--no-watch` | Start the site and return, without watching | +| `--allow-destructive` | Apply a schema change that drops or retypes data, once. Saves after that are protected again | +| `stop` | Stop the local site | +| `status` | Print whether the local site is running, and its URL | +| `wp -- ` | Run WP-CLI against the local site | -### Errors +The login it prints is for local development only. -| Code | Exit | When | -| --- | --- | --- | -| `zero_ability_failed` | 1 | The Ability answered a non-2xx status. The whole receipt rides along in `details`. A 403 means the credential's Grant does not permit this Ability | +```bash +sf zero dev +sf zero dev --no-watch +sf zero dev wp -- user application-password create admin local --porcelain +sf zero dev stop +``` -## sf zero types +## sf zero build -Generate TypeScript for everything the platform owns rather than your source: the live content model's types, the Ability catalog with the capability each one needs, and a row type per database table. Commit the result and use `--check` in CI. +Compile `zero.config.ts` into WordPress plugins. ```text -sf zero types [--space ] [--out ] [--check] +sf zero build [project] [--out ] ``` -### Flags - -| Flag | Default | What it does | -| --- | --- | --- | -| `--out=` | `zero-types.ts` | File to write, relative to the current directory | -| `--check` | `false` | Compare instead of writing, and exit non-zero when the file is stale or missing | +It writes the plugins to `.zero/wp-content/`, new migrations to `zero/migrations/`, the typed client to `zero/client/`, and editor types to `zero.gen.ts`. Commit everything except `.zero/`. ```bash -sf zero types -sf zero types --check +sf zero build +sf zero build ./app --out ./dist ``` -### Output - -With `--json`, `data` is `{ out, spaceId, versionId, contentModelRevision, abilities, tables, checked, drifted }`. - -### Errors - -| Code | Exit | When | -| --- | --- | --- | -| `zero_types_stale` | 2 | `--check` and the file is missing or no longer matches what the Space publishes | +`sf zero dev` and `sf zero build` run the native compiler from the CLI's optional platform package. If it's missing, reinstall the CLI with optional dependencies enabled. Set `SPACEFAST_ZERO_BIN` to a `zero` binary to use your own build instead. ## sf zero import -Translate a Payload CMS or EmDash project into an authored Zero capsule and its content files, and print the translation report. This is a local codegen step with no space flags. - -A Payload config carrying anything Zero cannot hold is refused outright, and nothing is written. A content model missing the fields that refused would publish a site that silently lost content. +Translate a Payload, EmDash, or Sanity schema into a new `zero.config.ts`, check that it compiles, and print what mapped and what did not. Fully local, no space flags. ```text -sf zero import [--out ] [--capsule ] [--force] +sf zero import [--out ] ``` -### Arguments - -| Argument | What it is | +| Argument or flag | What it does | | --- | --- | -| `` | Project format to translate: `payloadcms` or `emdash`. Required | -| `` | Directory holding the project to translate. Required | - -### Flags +| `` | `payload`, `emdash`, or `sanity`. `payloadcms` is accepted for `payload` | +| `` | The schema file, or the project directory to find it in (`payload.config.ts`, `.emdash/seed.json`, `sanity.config.ts`) | +| `--out=` | Config file to write. Defaults to `zero.config.ts`, and it must not exist yet | -| Flag | Default | What it does | -| --- | --- | --- | -| `--out=` | current directory | Directory to write the capsule and content into | -| `--capsule=` | `server/index.ts` | Capsule module to write, relative to `--out` | -| `--force` | `false` | Overwrite files the import would otherwise refuse to replace | +It never runs code from the other CMS and never overwrites a file. Anything Zero cannot hold refuses the whole import, and nothing is written. Every write rule in the result starts as denied, so set `access` before you publish. ```bash -sf zero import payloadcms ../my-payload-app -sf zero import emdash ../my-emdash-site --out . +sf zero import payload ../my-payload-app +sf zero import emdash ../site --out app/zero.config.ts ``` -### Errors +### Output -| Code | Exit | When | -| --- | --- | --- | -| `zero_import_refused` | 1 | The source config carries something Zero cannot hold. Nothing is written | +One line per schema entry: its outcome, its JSON pointer, and the reason. With `--json`, `data` carries `source`, `from`, `out`, and `report.entries[]`. + +## sf zero migrate + +Convert an older Zero app's source into a `zero.config.ts` project. See [Migrate an older Zero app](/zero-migrate). ## sf wp @@ -169,7 +158,7 @@ sf wp -- --version Show what a Space is serving: its live version, the URL it answers on, and what the live version deployed when it ships code. -A capsule lists tables, queries, mutations, endpoints, migration count, and schema digest. A worker lists entry, bundle digest, routes, endpoints, and capabilities. All of it comes from version metadata, so it answers while the runtime is asleep or moving. +A worker lists entry, bundle digest, routes, endpoints, and capabilities. All of it comes from version metadata, so it answers while the runtime is asleep or moving. ```text sf runtime status [--space ] @@ -193,7 +182,7 @@ Live URL: https://docs.view.fast Then one table per runtime section. A static Space has no app and prints nothing further. -With `--json`, `data` carries `liveVersionId`, `pendingVersionId`, `liveUrl`, `runtimeState`, `liveVersionStatus`, `pendingVersionStatus`, `app` (with a `capsule` or a `worker`), and `operation`. +With `--json`, `data` carries `liveVersionId`, `pendingVersionId`, `liveUrl`, `runtimeState`, `liveVersionStatus`, `pendingVersionStatus`, `app`, and `operation`. ## sf pages pull @@ -245,7 +234,7 @@ The command sets exit code 2 when any diagnostic has severity `error`. Warnings Read `DESIGN.md`'s ` ```theme.json ` and ` ```layout.html ` fenced blocks and write `theme.json` plus `_layout.html`. Fully local, no API call. -`theme.json` can seed Pages tokens and feeds a Zero capsule's Tailwind compile. `_layout.html` provides site chrome. `DESIGN.md` itself is never read at finalize or serve time; this is an authoring-time codegen step. +`theme.json` can seed Pages tokens. `_layout.html` provides site chrome. `DESIGN.md` itself is never read at finalize or serve time; this is an authoring-time codegen step. ```text sf design generate [-f] diff --git a/content/index.mdx b/content/index.mdx index 5cceb0ec..2ecdd631 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -41,7 +41,7 @@ Publish this folder to Spacefast and give me the live URL. A **Space** holds a site and its **versions**, immutable snapshots with their own URLs. A publish with no changes can reuse the current version. The `live` channel points at the version visitors see, or is empty before the first publish. Promoting or rolling back to a retained, ready version moves that pointer without rebuilding. Preview publishes and manual promotion policies leave `live` alone. (New terms piling up? See the [glossary](/glossary).) -Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs declare a [Zero runtime](/zero-runtime#declare-it) and server entry. +Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs add a [Zero](/zero-runtime) `zero.config.ts` or a [Functions](/functions) worker. In a self-serve account, your team owns the Spaces you claim, even when you are its only member. On [Plus](/billing#seats), you can add members who publish and roll back without sharing your login. Free and Go allow only the owner. Team roles control who can manage domains, credentials, and billing. See [Teams and members](/teams) to invite people and choose how new Spaces are shared. @@ -53,7 +53,7 @@ In a self-serve account, your team owns the Spaces you claim, even when you are Drag a folder onto drop.fast. No install, no account required. - Space, version, live, capsule, Ability, grant — core terms in one place. + Space, version, live, Ability, grant — core terms in one place. What gets uploaded, how versions work, and how to roll back. diff --git a/content/quickstart.mdx b/content/quickstart.mdx index 9f9819bb..bd67cd9d 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -108,7 +108,7 @@ Working with other people? If your Space is still anonymous, [claim it first](/a Push to a remote or connect GitHub and let Spacefast build. - Add a database, functions, and crons with the Zero runtime. + Add collections, tables, and actions with Zero. `CI=1` switches the CLI to JSON output and no prompts. diff --git a/evals.yaml b/evals.yaml index 96005bbc..78d53b9b 100644 --- a/evals.yaml +++ b/evals.yaml @@ -70,7 +70,7 @@ questions: routes: /config-file - id: zero-declare question: How do I turn on the Zero runtime for a project? - expected: ["runtime.kind set to zero in sf.jsonc", "a server entry"] + expected: ["a zero.config.ts at the project root", "no runtime block in sf.jsonc"] routes: /zero-runtime - id: routing-precedence question: If both a _redirects file and sf.jsonc rules match a path, which wins? diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index 7ef9a371..d804e0a4 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -60,7 +60,7 @@ test("unfamiliar homepage terms have a linked glossary with definitions", async const home = await builtPage("/"); const glossary = await builtPage("/glossary"); assert.match(home, /\[glossary\]\(\/docs\/glossary\/\)/iu); - for (const term of ["Ability", "Capsule", "Grant", "Live", "Space", "Version", "Zero"]) { + for (const term of ["Ability", "Grant", "Live", "Space", "Version", "Zero"]) { assert.match(glossary, new RegExp(`^## ${term}(?:\\b| \\()`, "mu"), `${term} needs a definition`); } }); @@ -71,11 +71,8 @@ test("Database gives the Zero prerequisite before query instructions", async () const query = database.indexOf("## Query it from code"); assert.ok(prerequisite >= 0 && query > prerequisite); assert.match(database.slice(prerequisite, query), /\[Zero\]\(\/docs\/zero-runtime\/\)/u); - assert.match(database.slice(prerequisite, query), /kind: "zero"/u); - assert.match(database.slice(prerequisite, query), /\[runtime block\]\(\/docs\/zero-runtime\/#declare-it\)/u); - assert.match(database.slice(prerequisite, query), /`server`/u); - assert.match(database.slice(prerequisite, query), /server file is required/iu); assert.match(database.slice(prerequisite, query), /sf init --runtime zero/u); + assert.match(database.slice(prerequisite, query), /`zero\.config\.ts`/u); }); test("Troubleshooting gives a first diagnostic step before the error catalog", async () => { From fb2641bb61bdf43496994b2551c16fddcf1f5461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 7 Nov 2026 03:53:59 +0100 Subject: [PATCH 02/12] Serve the Zero page at /docs/zero The page moved to /zero-runtime when the old /zero URL was taken by a redirect. Nothing in the runtime or the docs mount reserves /docs/zero any more, so the page goes back to the short URL. /zero-runtime, /runtimes, /runtimes/zero, and /apps/zero all 301 to it, including their Markdown twins. --- content/(dynamic)/crons.mdx | 2 +- content/(dynamic)/database.mdx | 4 ++-- content/(dynamic)/environment-variables.mdx | 2 +- content/(dynamic)/functions.mdx | 4 ++-- content/(dynamic)/meta.ts | 2 +- content/(dynamic)/object-storage.mdx | 2 +- content/(dynamic)/wordpress.mdx | 2 +- content/(dynamic)/zero-migrate.mdx | 4 ++-- content/(dynamic)/{zero-runtime.mdx => zero.mdx} | 0 content/(reference)/config-file.mdx | 2 +- content/(reference)/glossary.mdx | 2 +- content/cli/builds.mdx | 2 +- content/cli/db.mdx | 2 +- content/cli/project.mdx | 2 +- content/cli/publish.mdx | 2 +- content/cli/zero.mdx | 4 ++-- content/index.mdx | 2 +- content/quickstart.mdx | 2 +- evals.yaml | 2 +- redirects.ts | 4 ++-- scripts/docs-experience.test.mjs | 2 +- scripts/redirect-rules.mjs | 6 +++--- scripts/verify-routes.mjs | 2 +- 23 files changed, 29 insertions(+), 29 deletions(-) rename content/(dynamic)/{zero-runtime.mdx => zero.mdx} (100%) diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index 46ef387a..d7eede8d 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -9,7 +9,7 @@ There's no dashboard editor for crons. Declare schedules in `sf.jsonc`, then pub ## How a run happens -A cron is a scheduled `GET` against a path on your own site. It goes through the same front door a visitor gets, so whatever serves that path answers it: a [Zero](/zero-runtime) page, a [Functions](/functions) route, or a static file. +A cron is a scheduled `GET` against a path on your own site. It goes through the same front door a visitor gets, so whatever serves that path answers it: a [Zero](/zero) page, a [Functions](/functions) route, or a static file. A response under 400 is a success and is silent. A response of 400 or worse, or a fatal error, is a failure and is reported. diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index c5ec0e3b..f05f9114 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -8,12 +8,12 @@ sidebar: There's no connection string for this database. Your code reaches it through Zero's `db` or a worker's `env.DB`, and you reach it through a single-use SQL console. :::note[Don't have a database yet?] -Start a [Zero](/zero-runtime) app with `sf init --runtime zero`. Its `zero.config.ts` declares the collections and tables, and Spacefast creates them when you publish. A [Functions](/functions#what-is-on-env) worker also gets the database, through `env.DB`. +Start a [Zero](/zero) app with `sf init --runtime zero`. Its `zero.config.ts` declares the collections and tables, and Spacefast creates them when you publish. A [Functions](/functions#what-is-on-env) worker also gets the database, through `env.DB`. ::: ## What it is -Every Space has one database, the MySQL that lives on the machine serving the site. A [Zero](/zero-runtime) app keeps its collections and tables there. A [Functions](/functions) worker gets it as `env.DB`. +Every Space has one database, the MySQL that lives on the machine serving the site. A [Zero](/zero) app keeps its collections and tables there. A [Functions](/functions) worker gets it as `env.DB`. You do not create it, size it, or connect to it. Your config declares the schema, and Spacefast applies it when a version initializes. diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index 295093a1..6f813e31 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -92,7 +92,7 @@ EOF sf publish ``` -A [Zero](/zero-runtime) action or hook reads it with `env()`, which returns `null` for a name that isn't set. +A [Zero](/zero) action or hook reads it with `env()`, which returns `null` for a name that isn't set. ```ts zero.config.ts import { action, defineConfig, env } from "zero"; diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index 915445d5..c316287d 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -5,9 +5,9 @@ sidebar: order: 1 --- -Functions can detect a supported worker layout when you publish, without an explicit `runtime` block. Custom layouts need a declaration. A [Zero](/zero-runtime) app is a project with a `zero.config.ts`. +Functions can detect a supported worker layout when you publish, without an explicit `runtime` block. Custom layouts need a declaration. A [Zero](/zero) app is a project with a `zero.config.ts`. -Functions runs your code as a worker. Use it when you need npm packages or framework output like OpenNext Next.js. If you want collections, tables, and actions compiled to WordPress, with a schema Spacefast migrates for you, use [Zero](/zero-runtime) instead. Both can call `fetch()`. One version declares one runtime. +Functions runs your code as a worker. Use it when you need npm packages or framework output like OpenNext Next.js. If you want collections, tables, and actions compiled to WordPress, with a schema Spacefast migrates for you, use [Zero](/zero) instead. Both can call `fetch()`. One version declares one runtime. Functions is on for every account. diff --git a/content/(dynamic)/meta.ts b/content/(dynamic)/meta.ts index 4aa612cd..b4ca900d 100644 --- a/content/(dynamic)/meta.ts +++ b/content/(dynamic)/meta.ts @@ -4,5 +4,5 @@ export default defineMeta({ title: "Dynamic", collapsed: false, order: 5, - pages: ["zero-runtime", "functions", "database", "environment-variables", "crons", "object-storage", "logs", "wordpress", "zero-migrate"], + pages: ["zero", "functions", "database", "environment-variables", "crons", "object-storage", "logs", "wordpress", "zero-migrate"], }); diff --git a/content/(dynamic)/object-storage.mdx b/content/(dynamic)/object-storage.mdx index 13fa4e4a..b126f899 100644 --- a/content/(dynamic)/object-storage.mdx +++ b/content/(dynamic)/object-storage.mdx @@ -30,7 +30,7 @@ An object is addressed by a bare 32-character hexadecimal id. There is no key, n ## Uploads in a Zero app -A [Zero](/zero-runtime) app handles its own uploads. Declare a `media()` column for images your content shows, or a `file()` column for private files that download through a signed link valid for five minutes. +A [Zero](/zero) app handles its own uploads. Declare a `media()` column for images your content shows, or a `file()` column for private files that download through a signed link valid for five minutes. ## Upload from the API diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index bd01ad67..169675f1 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -5,7 +5,7 @@ description: Every Space runs a WordPress behind its static files. Run WP-CLI ag Every Space includes a WordPress install behind its published files. Reach it through `sf wp` or its published Abilities. -Your published files serve in front of WordPress, so a static Space never touches it and never pays for it. It is there because it is what makes a Space more than a bucket: [Zero](/zero-runtime) apps run on it, [Storage](/object-storage) is its media library, and [Database](/database) is its MySQL. This page covers the two ways to reach that WordPress directly. +Your published files serve in front of WordPress, so a static Space never touches it and never pays for it. It is there because it is what makes a Space more than a bucket: [Zero](/zero) apps run on it, [Storage](/object-storage) is its media library, and [Database](/database) is its MySQL. This page covers the two ways to reach that WordPress directly. ## `sf wp` diff --git a/content/(dynamic)/zero-migrate.mdx b/content/(dynamic)/zero-migrate.mdx index e0ac74f7..4ba97877 100644 --- a/content/(dynamic)/zero-migrate.mdx +++ b/content/(dynamic)/zero-migrate.mdx @@ -5,7 +5,7 @@ sidebar: order: 8 --- -Your Zero 0.8 app keeps working. Its Space keeps serving, and you can keep publishing updates to it. New Spaces start on the current Zero, the `zero.config.ts` compiler described in [Dynamic sites with Zero](/zero-runtime). +Your Zero 0.8 app keeps working. Its Space keeps serving, and you can keep publishing updates to it. New Spaces start on the current Zero, the `zero.config.ts` compiler described in [Dynamic sites with Zero](/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" }`. @@ -61,7 +61,7 @@ Every write starts denied. Open `zero.config.ts` and set `access` on each collec 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-runtime#pages). +- **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.** diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero.mdx similarity index 100% rename from content/(dynamic)/zero-runtime.mdx rename to content/(dynamic)/zero.mdx diff --git a/content/(reference)/config-file.mdx b/content/(reference)/config-file.mdx index 107265d6..316dfaa9 100644 --- a/content/(reference)/config-file.mdx +++ b/content/(reference)/config-file.mdx @@ -180,7 +180,7 @@ Data location is chosen once, when the Space is created, and never changes. Omit the block for a static site. -A [Zero](/zero-runtime) app needs no `runtime` block. A `zero.config.ts` at the project root is the declaration, and a project that has one must not also set `runtime.kind` to `"zero"`: publish refuses it with `zero_mixed_generation`. +A [Zero](/zero) app needs no `runtime` block. A `zero.config.ts` at the project root is the declaration, and a project that has one must not also set `runtime.kind` to `"zero"`: publish refuses it with `zero_mixed_generation`. ### `kind: "functions"` diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx index 4c6ee6e0..318be0c9 100644 --- a/content/(reference)/glossary.mdx +++ b/content/(reference)/glossary.mdx @@ -49,4 +49,4 @@ An immutable snapshot created when a publish produces a new version. A ready ver ## Zero -The compiler that turns one `zero.config.ts` (collections, tables, actions, and pages) into a WordPress app, hosted on the Space with its own MySQL database. [Functions](#functions) also provides a database, and [crons](/crons) can call either runtime or static files. See [Dynamic sites with Zero](/zero-runtime). +The compiler that turns one `zero.config.ts` (collections, tables, actions, and pages) into a WordPress app, hosted on the Space with its own MySQL database. [Functions](#functions) also provides a database, and [crons](/crons) can call either runtime or static files. See [Dynamic sites with Zero](/zero). diff --git a/content/cli/builds.mdx b/content/cli/builds.mdx index bcfcc81b..88aac8ca 100644 --- a/content/cli/builds.mdx +++ b/content/cli/builds.mdx @@ -17,7 +17,7 @@ Build and pack project output. sf build [DIR] ``` -Detects build settings, runs the build, and packs the output into an archive. This command talks to your filesystem, not to a Space. To compile a [Zero](/zero-runtime) app locally, use `sf zero build`. +Detects build settings, runs the build, and packs the output into an archive. This command talks to your filesystem, not to a Space. To compile a [Zero](/zero) app locally, use `sf zero build`. ### Arguments diff --git a/content/cli/db.mdx b/content/cli/db.mdx index b3549641..44069b7c 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -5,7 +5,7 @@ sidebar: order: 22 --- -Open a single-use SQL console on a Space's database, and exercise its scheduled jobs on demand. A [Zero](/zero-runtime) app's schema lives in its `zero.config.ts` and migrates on publish. See [Database](/database). +Open a single-use SQL console on a Space's database, and exercise its scheduled jobs on demand. A [Zero](/zero) app's schema lives in its `zero.config.ts` and migrates on publish. See [Database](/database). Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`) and the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 7b9db236..d6fb435d 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -30,7 +30,7 @@ sf init [NAME] [--space ] [-o ] [-f] [--runtime static|zero|functio | Flag | Default | What it does | | --- | --- | --- | | `-f, --force` | `false` | Overwrite existing generated files. | -| `--runtime=` | `static` | Project to scaffold. `zero` writes a starter [Zero](/zero-runtime) `zero.config.ts`, the same one `sf zero init` writes. | +| `--runtime=` | `static` | Project to scaffold. `zero` writes a starter [Zero](/zero) `zero.config.ts`, the same one `sf zero init` writes. | | `--space=` | none | Space ID, live URL, domain, or slug to link the directory to. Only with `--runtime static`. Env `SPACEFAST_SPACE`. | | `-o, --team=` | default team | Team slug, ID, or name. Env `SPACEFAST_TEAM`. | | `--claim-token=` | none | Space key (`sfc_…`) for an unclaimed anonymous Space. Env `SPACEFAST_CLAIM_TOKEN`. | diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index b16c3ded..48c4e175 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -293,7 +293,7 @@ Start the local dev server. sf dev ``` -`sf dev` serves a Pages preview with sample data and the publish-time expander at `/_spacefast/pages/`. It is not a local mirror of what publish serves, and there is no static file server for your site on that path. For a [Zero](/zero-runtime) app, run `sf zero dev`. For a framework project, use that framework's own dev server (`next dev`, `vite`), then run `sf publish` when you're ready. +`sf dev` serves a Pages preview with sample data and the publish-time expander at `/_spacefast/pages/`. It is not a local mirror of what publish serves, and there is no static file server for your site on that path. For a [Zero](/zero) app, run `sf zero dev`. For a framework project, use that framework's own dev server (`next dev`, `vite`), then run `sf publish` when you're ready. | Flag | Default | What it does | | --- | --- | --- | diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index cc46c97e..cc6f37c4 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -5,11 +5,11 @@ sidebar: order: 25 --- -`sf zero` builds and runs [Zero](/zero-runtime) apps: a `zero.config.ts` compiled to WordPress. The rest of this page covers WP-CLI, runtime status, site pages, design tokens, and analytics. +`sf zero` builds and runs [Zero](/zero) apps: a `zero.config.ts` compiled to WordPress. The rest of this page covers WP-CLI, runtime status, site pages, design tokens, and analytics. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). `sf wp`, `sf runtime status`, and `sf analytics` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). -To publish a Zero app, run `sf publish` from its root. See [Dynamic sites with Zero](/zero-runtime). +To publish a Zero app, run `sf publish` from its root. See [Dynamic sites with Zero](/zero). ## sf zero init diff --git a/content/index.mdx b/content/index.mdx index 2ecdd631..40ccab2f 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -41,7 +41,7 @@ Publish this folder to Spacefast and give me the live URL. A **Space** holds a site and its **versions**, immutable snapshots with their own URLs. A publish with no changes can reuse the current version. The `live` channel points at the version visitors see, or is empty before the first publish. Promoting or rolling back to a retained, ready version moves that pointer without rebuilding. Preview publishes and manual promotion policies leave `live` alone. (New terms piling up? See the [glossary](/glossary).) -Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs add a [Zero](/zero-runtime) `zero.config.ts` or a [Functions](/functions) worker. +Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs add a [Zero](/zero) `zero.config.ts` or a [Functions](/functions) worker. In a self-serve account, your team owns the Spaces you claim, even when you are its only member. On [Plus](/billing#seats), you can add members who publish and roll back without sharing your login. Free and Go allow only the owner. Team roles control who can manage domains, credentials, and billing. See [Teams and members](/teams) to invite people and choose how new Spaces are shared. diff --git a/content/quickstart.mdx b/content/quickstart.mdx index bd67cd9d..a5b4f63d 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -107,7 +107,7 @@ Working with other people? If your Space is still anonymous, [claim it first](/a Push to a remote or connect GitHub and let Spacefast build. - + Add collections, tables, and actions with Zero. diff --git a/evals.yaml b/evals.yaml index 78d53b9b..b3addc01 100644 --- a/evals.yaml +++ b/evals.yaml @@ -71,7 +71,7 @@ questions: - id: zero-declare question: How do I turn on the Zero runtime for a project? expected: ["a zero.config.ts at the project root", "no runtime block in sf.jsonc"] - routes: /zero-runtime + routes: /zero - id: routing-precedence question: If both a _redirects file and sf.jsonc rules match a path, which wins? expected: ["_redirects"] diff --git a/redirects.ts b/redirects.ts index 482c4763..adf1185c 100644 --- a/redirects.ts +++ b/redirects.ts @@ -60,9 +60,9 @@ export const redirects: { from: string; to: string; status: 301 }[] = [ { from: "/recipes/preview-channels", to: "/versions", status: 301 }, { from: "/recipes/redirects", to: "/routing", status: 301 }, { from: "/redirects", to: "/routing", status: 301 }, - { from: "/runtimes", to: "/zero-runtime", status: 301 }, + { from: "/runtimes", to: "/zero", status: 301 }, { from: "/runtimes/functions", to: "/functions", status: 301 }, - { from: "/runtimes/zero", to: "/zero-runtime", status: 301 }, + { from: "/runtimes/zero", to: "/zero", status: 301 }, { from: "/sdk", to: "/api/sdk", status: 301 }, { from: "/spaces/rename", to: "/spaces", status: 301 }, { from: "/versions-and-rollback", to: "/versions", status: 301 }, diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index d804e0a4..24690fef 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -70,7 +70,7 @@ test("Database gives the Zero prerequisite before query instructions", async () const prerequisite = database.indexOf("Don't have a database yet?"); const query = database.indexOf("## Query it from code"); assert.ok(prerequisite >= 0 && query > prerequisite); - assert.match(database.slice(prerequisite, query), /\[Zero\]\(\/docs\/zero-runtime\/\)/u); + assert.match(database.slice(prerequisite, query), /\[Zero\]\(\/docs\/zero\/\)/u); assert.match(database.slice(prerequisite, query), /sf init --runtime zero/u); assert.match(database.slice(prerequisite, query), /`zero\.config\.ts`/u); }); diff --git a/scripts/redirect-rules.mjs b/scripts/redirect-rules.mjs index 403aec65..ac39d7e1 100644 --- a/scripts/redirect-rules.mjs +++ b/scripts/redirect-rules.mjs @@ -23,12 +23,12 @@ const authoredRedirectRules = [ { from: `${deploymentBase}/apps/zero`, status: 301, - to: `${deploymentBase}/zero-runtime/`, + to: `${deploymentBase}/zero/`, }, { - from: `${deploymentBase}/zero`, + from: `${deploymentBase}/zero-runtime`, status: 301, - to: `${deploymentBase}/zero-runtime/`, + to: `${deploymentBase}/zero/`, }, { from: `${deploymentBase}/storage`, diff --git a/scripts/verify-routes.mjs b/scripts/verify-routes.mjs index acf634db..526130d5 100644 --- a/scripts/verify-routes.mjs +++ b/scripts/verify-routes.mjs @@ -149,7 +149,7 @@ const representativePages = [ ["errors/index.html", `${docsRoot}/errors`, "Error reference"], ["errors/rate_limited/index.html", `${docsRoot}/errors/rate_limited`, "rate_limited"], ["recipes/vite/index.html", `${docsRoot}/recipes/vite`, "Vite"], - ["zero-runtime/index.html", `${docsRoot}/zero-runtime`, "What Zero is"], + ["zero/index.html", `${docsRoot}/zero`, "What Zero is"], ]; for (const page of representativePages) { await requirePage(...page); From 29f695808cda335367f59ccedf4e192977894435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 7 Nov 2026 18:59:08 +0000 Subject: [PATCH 03/12] Regenerate the CLI reference with the Zero v2 commands The CLI snapshot now lists sf zero init, check, dev, build, and migrate, and sf init --runtime zero without the old template flags. The public-safety gate accepts the OAuth client-type term that the new sf oauth-apps reference uses. Co-Authored-By: Claude Opus 5.5 --- generated/cli/index.md | 8053 +++++++++++++++---------- generated/manifest.json | 6 +- scripts/verify-public-safety.mjs | 13 +- scripts/verify-public-safety.test.mjs | 21 + 4 files changed, 4858 insertions(+), 3235 deletions(-) diff --git a/generated/cli/index.md b/generated/cli/index.md index f29ae69a..74c625c0 100644 --- a/generated/cli/index.md +++ b/generated/cli/index.md @@ -20,7 +20,7 @@ Tell your agent to pick the smallest lane that does the job: $ npm install -g spacefast $ sf COMMAND running command... -$ sf (--version) +$ sf (--version|-v) spacefast/0.8.1 $ sf --help [COMMAND] USAGE @@ -48,15 +48,15 @@ GLOBAL FLAGS ## Commands - [`sf access`](#sf-access) -- [`sf access logout-all`](#sf-access-logout-all) +- [`sf access sessions revoke`](#sf-access-sessions-revoke) - [`sf activity`](#sf-activity) - [`sf agents init [DIRECTORY]`](#sf-agents-init-directory) - [`sf analytics`](#sf-analytics) -- [`sf api METHODORPATH [PATH]`](#sf-api-methodorpath-path) +- [`sf api METHOD [PATH]`](#sf-api-method-path) - [`sf api-keys`](#sf-api-keys) - [`sf api-keys create`](#sf-api-keys-create) - [`sf api-keys list`](#sf-api-keys-list) -- [`sf api-keys revoke ID`](#sf-api-keys-revoke-id) +- [`sf api-keys revoke KEY`](#sf-api-keys-revoke-key) - [`sf apply`](#sf-apply) - [`sf auth`](#sf-auth) - [`sf auth as NAME`](#sf-auth-as-name) @@ -64,23 +64,23 @@ GLOBAL FLAGS - [`sf autocomplete [SHELL]`](#sf-autocomplete-shell) - [`sf build [DIR]`](#sf-build-dir) - [`sf builds cancel BUILD`](#sf-builds-cancel-build) -- [`sf builds detect`](#sf-builds-detect) -- [`sf builds get BUILD`](#sf-builds-get-build) +- [`sf builds detect [DIR]`](#sf-builds-detect-dir) +- [`sf builds inspect BUILD`](#sf-builds-inspect-build) +- [`sf builds list`](#sf-builds-list) - [`sf builds logs BUILD`](#sf-builds-logs-build) -- [`sf builds ls`](#sf-builds-ls) -- [`sf builds refresh-upload BUILD`](#sf-builds-refresh-upload-build) - [`sf builds retry BUILD`](#sf-builds-retry-build) +- [`sf builds upload refresh BUILD`](#sf-builds-upload-refresh-build) - [`sf channels`](#sf-channels) - [`sf channels history [NAME]`](#sf-channels-history-name) -- [`sf channels ls`](#sf-channels-ls) +- [`sf channels list`](#sf-channels-list) - [`sf channels set NAME`](#sf-channels-set-name) - [`sf comments`](#sf-comments) - [`sf comments create`](#sf-comments-create) - [`sf comments export`](#sf-comments-export) -- [`sf comments get COMMENT`](#sf-comments-get-comment) +- [`sf comments inspect COMMENT`](#sf-comments-inspect-comment) - [`sf comments list`](#sf-comments-list) -- [`sf comments mark-read COMMENT`](#sf-comments-mark-read-comment) - [`sf comments react COMMENT EMOJI`](#sf-comments-react-comment-emoji) +- [`sf comments read COMMENT`](#sf-comments-read-comment) - [`sf comments reopen COMMENT`](#sf-comments-reopen-comment) - [`sf comments reply COMMENT`](#sf-comments-reply-comment) - [`sf comments resolve COMMENT`](#sf-comments-resolve-comment) @@ -88,35 +88,35 @@ GLOBAL FLAGS - [`sf comments settings set`](#sf-comments-settings-set) - [`sf comments unreact COMMENT EMOJI`](#sf-comments-unreact-comment-emoji) - [`sf connectors`](#sf-connectors) -- [`sf connectors add TARGET`](#sf-connectors-add-target) - [`sf connectors bind ROLE CONNECTION`](#sf-connectors-bind-role-connection) - [`sf connectors bindings`](#sf-connectors-bindings) - [`sf connectors connections`](#sf-connectors-connections) -- [`sf connectors connections add CONNECTOR`](#sf-connectors-connections-add-connector) -- [`sf connectors connections edit CONNECTION`](#sf-connectors-connections-edit-connection) +- [`sf connectors connections connect CONNECTOR`](#sf-connectors-connections-connect-connector) +- [`sf connectors connections disconnect CONNECTION`](#sf-connectors-connections-disconnect-connection) - [`sf connectors connections health CONNECTION`](#sf-connectors-connections-health-connection) -- [`sf connectors connections ls`](#sf-connectors-connections-ls) +- [`sf connectors connections list`](#sf-connectors-connections-list) - [`sf connectors connections reconnect CONNECTION`](#sf-connectors-connections-reconnect-connection) - [`sf connectors connections refresh CONNECTION`](#sf-connectors-connections-refresh-connection) -- [`sf connectors connections rm CONNECTION`](#sf-connectors-connections-rm-connection) +- [`sf connectors connections update CONNECTION`](#sf-connectors-connections-update-connection) +- [`sf connectors create TARGET`](#sf-connectors-create-target) +- [`sf connectors delete CONNECTOR`](#sf-connectors-delete-connector) - [`sf connectors detect URL`](#sf-connectors-detect-url) -- [`sf connectors ls`](#sf-connectors-ls) +- [`sf connectors list`](#sf-connectors-list) - [`sf connectors policies`](#sf-connectors-policies) - [`sf connectors policies add PATTERN`](#sf-connectors-policies-add-pattern) - [`sf connectors policies explain ADDRESS`](#sf-connectors-policies-explain-address) -- [`sf connectors policies ls`](#sf-connectors-policies-ls) -- [`sf connectors policies rm RULE`](#sf-connectors-policies-rm-rule) +- [`sf connectors policies list`](#sf-connectors-policies-list) +- [`sf connectors policies remove RULE`](#sf-connectors-policies-remove-rule) - [`sf connectors policies set RULE`](#sf-connectors-policies-set-rule) -- [`sf connectors rm CONNECTOR`](#sf-connectors-rm-connector) - [`sf connectors run [CODE]`](#sf-connectors-run-code) -- [`sf connectors run get RUN`](#sf-connectors-run-get-run) +- [`sf connectors run inspect RUN`](#sf-connectors-run-inspect-run) - [`sf connectors run list`](#sf-connectors-run-list) - [`sf connectors run resume RUN`](#sf-connectors-run-resume-run) - [`sf connectors unbind ROLE`](#sf-connectors-unbind-role) - [`sf content dashboard [TARGET]`](#sf-content-dashboard-target) - [`sf continue`](#sf-continue) - [`sf crons`](#sf-crons) -- [`sf crons ls`](#sf-crons-ls) +- [`sf crons list`](#sf-crons-list) - [`sf crons run TARGET`](#sf-crons-run-target) - [`sf db [TARGET]`](#sf-db-target) - [`sf db console [TARGET]`](#sf-db-console-target) @@ -127,7 +127,7 @@ GLOBAL FLAGS - [`sf design`](#sf-design) - [`sf design generate`](#sf-design-generate) - [`sf dev`](#sf-dev) -- [`sf dev run-many`](#sf-dev-run-many) +- [`sf dev instances`](#sf-dev-instances) - [`sf docs [QUERY]`](#sf-docs-query) - [`sf doctor`](#sf-doctor) - [`sf domains`](#sf-domains) @@ -135,36 +135,36 @@ GLOBAL FLAGS - [`sf domains check DOMAIN`](#sf-domains-check-domain) - [`sf domains contacts`](#sf-domains-contacts) - [`sf domains contacts add`](#sf-domains-contacts-add) -- [`sf domains contacts ls`](#sf-domains-contacts-ls) +- [`sf domains contacts list`](#sf-domains-contacts-list) - [`sf domains diagnostics DOMAIN`](#sf-domains-diagnostics-domain) - [`sf domains dns`](#sf-domains-dns) - [`sf domains dns add DOMAIN`](#sf-domains-dns-add-domain) - [`sf domains dns batch DOMAIN`](#sf-domains-dns-batch-domain) - [`sf domains dns capabilities DOMAIN`](#sf-domains-dns-capabilities-domain) - [`sf domains dns export DOMAIN`](#sf-domains-dns-export-domain) -- [`sf domains dns ls DOMAIN`](#sf-domains-dns-ls-domain) +- [`sf domains dns list DOMAIN`](#sf-domains-dns-list-domain) - [`sf domains dns refresh DOMAIN`](#sf-domains-dns-refresh-domain) +- [`sf domains dns remove DOMAIN RECORD`](#sf-domains-dns-remove-domain-record) - [`sf domains dns replace DOMAIN`](#sf-domains-dns-replace-domain) -- [`sf domains dns rm DOMAIN RECORD`](#sf-domains-dns-rm-domain-record) - [`sf domains dns update DOMAIN RECORD`](#sf-domains-dns-update-domain-record) -- [`sf domains ls`](#sf-domains-ls) +- [`sf domains list`](#sf-domains-list) - [`sf domains nameservers DOMAIN`](#sf-domains-nameservers-domain) - [`sf domains nameservers set DOMAIN NAMESERVERS`](#sf-domains-nameservers-set-domain-nameservers) - [`sf domains registration DOMAIN`](#sf-domains-registration-domain) - [`sf domains registration purchase DOMAIN... [flags]`](#sf-domains-registration-purchase-domain-flags) - [`sf domains registration renew DOMAIN`](#sf-domains-registration-renew-domain) -- [`sf domains rm DOMAIN`](#sf-domains-rm-domain) +- [`sf domains remove DOMAIN`](#sf-domains-remove-domain) - [`sf domains search [QUERY]`](#sf-domains-search-query) - [`sf domains update DOMAIN`](#sf-domains-update-domain) - [`sf env`](#sf-env) -- [`sf env export-template [DIR]`](#sf-env-export-template-dir) - [`sf env import FILE`](#sf-env-import-file) -- [`sf env ls`](#sf-env-ls) +- [`sf env list`](#sf-env-list) - [`sf env pull [FILE]`](#sf-env-pull-file) -- [`sf env rm NAME`](#sf-env-rm-name) +- [`sf env remove NAME`](#sf-env-remove-name) - [`sf env set NAME [VALUE]`](#sf-env-set-name-value) +- [`sf env template [DIR]`](#sf-env-template-dir) - [`sf feedback`](#sf-feedback) -- [`sf feedback get REF`](#sf-feedback-get-ref) +- [`sf feedback inspect REF`](#sf-feedback-inspect-ref) - [`sf feedback list`](#sf-feedback-list) - [`sf fetch [PATH]`](#sf-fetch-path) - [`sf functions`](#sf-functions) @@ -175,7 +175,7 @@ GLOBAL FLAGS - [`sf git github`](#sf-git-github) - [`sf git github installations`](#sf-git-github-installations) - [`sf git github repos [INSTALLATION]`](#sf-git-github-repos-installation) -- [`sf git ls`](#sf-git-ls) +- [`sf git inspect`](#sf-git-inspect) - [`sf git origin`](#sf-git-origin) - [`sf git sync`](#sf-git-sync) - [`sf git update`](#sf-git-update) @@ -191,14 +191,21 @@ GLOBAL FLAGS - [`sf mcp install`](#sf-mcp-install) - [`sf mcp proxy`](#sf-mcp-proxy) - [`sf migrate URL`](#sf-migrate-url) +- [`sf oauth-apps`](#sf-oauth-apps) +- [`sf oauth-apps create`](#sf-oauth-apps-create) +- [`sf oauth-apps delete APP`](#sf-oauth-apps-delete-app) +- [`sf oauth-apps inspect APP`](#sf-oauth-apps-inspect-app) +- [`sf oauth-apps list`](#sf-oauth-apps-list) +- [`sf oauth-apps secret rotate APP`](#sf-oauth-apps-secret-rotate-app) +- [`sf oauth-apps update APP`](#sf-oauth-apps-update-app) - [`sf open [TARGET]`](#sf-open-target) -- [`sf operations [ID]`](#sf-operations-id) +- [`sf operations [OPERATION]`](#sf-operations-operation) - [`sf pages`](#sf-pages) - [`sf pages pull [TARGET]`](#sf-pages-pull-target) - [`sf pages validate`](#sf-pages-validate) - [`sf plugins`](#sf-plugins) - [`sf profiles`](#sf-profiles) -- [`sf profiles rm NAME`](#sf-profiles-rm-name) +- [`sf profiles remove NAME`](#sf-profiles-remove-name) - [`sf profiles set NAME`](#sf-profiles-set-name) - [`sf profiles use NAME`](#sf-profiles-use-name) - [`sf promote [VERSION]`](#sf-promote-version) @@ -210,10 +217,10 @@ GLOBAL FLAGS - [`sf routing inspect`](#sf-routing-inspect) - [`sf rules`](#sf-rules) - [`sf rules add`](#sf-rules-add) -- [`sf rules ls`](#sf-rules-ls) +- [`sf rules list`](#sf-rules-list) - [`sf rules pause`](#sf-rules-pause) +- [`sf rules remove NAME`](#sf-rules-remove-name) - [`sf rules resume`](#sf-rules-resume) -- [`sf rules rm NAME`](#sf-rules-rm-name) - [`sf rules test URL`](#sf-rules-test-url) - [`sf rules validate`](#sf-rules-validate) - [`sf runtime`](#sf-runtime) @@ -221,13 +228,13 @@ GLOBAL FLAGS - [`sf sell activate`](#sf-sell-activate) - [`sf sell demo`](#sf-sell-demo) - [`sf sell onboard`](#sf-sell-onboard) -- [`sf sell orders get SESSIONID`](#sf-sell-orders-get-sessionid) -- [`sf sell orders ls`](#sf-sell-orders-ls) -- [`sf sell orders resend SESSIONID`](#sf-sell-orders-resend-sessionid) -- [`sf sell orders ship SESSIONID`](#sf-sell-orders-ship-sessionid) +- [`sf sell orders inspect ORDER`](#sf-sell-orders-inspect-order) +- [`sf sell orders list`](#sf-sell-orders-list) +- [`sf sell orders resend ORDER`](#sf-sell-orders-resend-order) +- [`sf sell orders ship ORDER`](#sf-sell-orders-ship-order) - [`sf sell products archive KEY`](#sf-sell-products-archive-key) - [`sf sell products create`](#sf-sell-products-create) -- [`sf sell products ls`](#sf-sell-products-ls) +- [`sf sell products list`](#sf-sell-products-list) - [`sf sell products update KEY`](#sf-sell-products-update-key) - [`sf sell refresh`](#sf-sell-refresh) - [`sf sell status`](#sf-sell-status) @@ -236,42 +243,43 @@ GLOBAL FLAGS - [`sf share`](#sf-share) - [`sf share check`](#sf-share-check) - [`sf share grant`](#sf-share-grant) -- [`sf share grant edit ID`](#sf-share-grant-edit-id) +- [`sf share grant update GRANT`](#sf-share-grant-update-grant) - [`sf share identity`](#sf-share-identity) - [`sf share identity create`](#sf-share-identity-create) - [`sf share identity grant`](#sf-share-identity-grant) -- [`sf share identity ls`](#sf-share-identity-ls) +- [`sf share identity list`](#sf-share-identity-list) - [`sf share identity revoke CONNECTION`](#sf-share-identity-revoke-connection) - [`sf share identity update CONNECTION`](#sf-share-identity-update-connection) - [`sf share link`](#sf-share-link) -- [`sf share link copy ID`](#sf-share-link-copy-id) +- [`sf share link copy LINK`](#sf-share-link-copy-link) - [`sf share link create`](#sf-share-link-create) -- [`sf share link edit ID`](#sf-share-link-edit-id) -- [`sf share link ls`](#sf-share-link-ls) -- [`sf share link revoke ID`](#sf-share-link-revoke-id) +- [`sf share link list`](#sf-share-link-list) +- [`sf share link revoke LINK`](#sf-share-link-revoke-link) +- [`sf share link update LINK`](#sf-share-link-update-link) - [`sf share list`](#sf-share-list) - [`sf share password`](#sf-share-password) - [`sf share password create`](#sf-share-password-create) -- [`sf share password ls`](#sf-share-password-ls) -- [`sf share password revoke ID`](#sf-share-password-revoke-id) -- [`sf share password rotate ID`](#sf-share-password-rotate-id) +- [`sf share password list`](#sf-share-password-list) +- [`sf share password revoke CREDENTIAL`](#sf-share-password-revoke-credential) +- [`sf share password rotate CREDENTIAL`](#sf-share-password-rotate-credential) - [`sf share people`](#sf-share-people) -- [`sf share people edit PERSON`](#sf-share-people-edit-person) - [`sf share people invite EMAIL`](#sf-share-people-invite-email) -- [`sf share people ls`](#sf-share-people-ls) +- [`sf share people list`](#sf-share-people-list) - [`sf share people remove PERSON`](#sf-share-people-remove-person) - [`sf share people resend PERSON`](#sf-share-people-resend-person) +- [`sf share people update PERSON`](#sf-share-people-update-person) - [`sf share request`](#sf-share-request) -- [`sf share request approve ID`](#sf-share-request-approve-id) -- [`sf share request deny ID`](#sf-share-request-deny-id) -- [`sf share request ls`](#sf-share-request-ls) -- [`sf share revoke ID`](#sf-share-revoke-id) +- [`sf share request approve REQUEST`](#sf-share-request-approve-request) +- [`sf share request deny REQUEST`](#sf-share-request-deny-request) +- [`sf share request list`](#sf-share-request-list) +- [`sf share revoke GRANT`](#sf-share-revoke-grant) - [`sf share token`](#sf-share-token) - [`sf share token create`](#sf-share-token-create) -- [`sf share token ls`](#sf-share-token-ls) -- [`sf share token revoke ID`](#sf-share-token-revoke-id) -- [`sf share token rotate ID`](#sf-share-token-rotate-id) +- [`sf share token list`](#sf-share-token-list) +- [`sf share token revoke CREDENTIAL`](#sf-share-token-revoke-credential) +- [`sf share token rotate CREDENTIAL`](#sf-share-token-rotate-credential) - [`sf skills`](#sf-skills) +- [`sf skills install`](#sf-skills-install) - [`sf skills remove`](#sf-skills-remove) - [`sf skills status`](#sf-skills-status) - [`sf source archive`](#sf-source-archive) @@ -280,41 +288,41 @@ GLOBAL FLAGS - [`sf source cat PATH`](#sf-source-cat-path) - [`sf source commits apply`](#sf-source-commits-apply) - [`sf source commits create`](#sf-source-commits-create) -- [`sf source commits get SHA`](#sf-source-commits-get-sha) -- [`sf source commits ls`](#sf-source-commits-ls) +- [`sf source commits inspect SHA`](#sf-source-commits-inspect-sha) +- [`sf source commits list`](#sf-source-commits-list) - [`sf source diff`](#sf-source-diff) - [`sf source import`](#sf-source-import) -- [`sf source ls`](#sf-source-ls) +- [`sf source list`](#sf-source-list) - [`sf source merge SOURCE TARGET`](#sf-source-merge-source-target) - [`sf source search PATTERN`](#sf-source-search-pattern) - [`sf source tags create NAME`](#sf-source-tags-create-name) -- [`sf source tags ls`](#sf-source-tags-ls) +- [`sf source tags list`](#sf-source-tags-list) - [`sf spaces`](#sf-spaces) -- [`sf spaces add`](#sf-spaces-add) - [`sf spaces archive`](#sf-spaces-archive) - [`sf spaces check NAME`](#sf-spaces-check-name) - [`sf spaces claim`](#sf-spaces-claim) +- [`sf spaces create`](#sf-spaces-create) +- [`sf spaces delete`](#sf-spaces-delete) - [`sf spaces download`](#sf-spaces-download) - [`sf spaces duplicate`](#sf-spaces-duplicate) -- [`sf spaces get`](#sf-spaces-get) -- [`sf spaces ls`](#sf-spaces-ls) +- [`sf spaces inspect`](#sf-spaces-inspect) +- [`sf spaces key rotate`](#sf-spaces-key-rotate) +- [`sf spaces list`](#sf-spaces-list) - [`sf spaces restore`](#sf-spaces-restore) -- [`sf spaces rm`](#sf-spaces-rm) -- [`sf spaces rotate-claim`](#sf-spaces-rotate-claim) - [`sf spaces transfer TEAM`](#sf-spaces-transfer-team) - [`sf spaces update`](#sf-spaces-update) - [`sf status`](#sf-status) - [`sf storage [TARGET]`](#sf-storage-target) -- [`sf storage get ID`](#sf-storage-get-id) -- [`sf storage ls [TARGET]`](#sf-storage-ls-target) +- [`sf storage delete [OBJECT]`](#sf-storage-delete-object) +- [`sf storage download OBJECT`](#sf-storage-download-object) +- [`sf storage list [TARGET]`](#sf-storage-list-target) - [`sf storage put FILE`](#sf-storage-put-file) -- [`sf storage rm [ID]`](#sf-storage-rm-id) - [`sf switch [TEAM]`](#sf-switch-team) - [`sf tags`](#sf-tags) - [`sf tags apply FILE`](#sf-tags-apply-file) - [`sf tags debug`](#sf-tags-debug) - [`sf tags releases`](#sf-tags-releases) -- [`sf tags releases get ENVIRONMENT`](#sf-tags-releases-get-environment) +- [`sf tags releases inspect ENVIRONMENT`](#sf-tags-releases-inspect-environment) - [`sf tags releases rollback ENVIRONMENT REVISION`](#sf-tags-releases-rollback-environment-revision) - [`sf tags releases set ENVIRONMENT REVISION`](#sf-tags-releases-set-environment-revision) - [`sf tags revisions`](#sf-tags-revisions) @@ -328,56 +336,61 @@ GLOBAL FLAGS - [`sf teams`](#sf-teams) - [`sf teams accept INVITATION`](#sf-teams-accept-invitation) - [`sf teams create NAME`](#sf-teams-create-name) -- [`sf teams defaults [ROOTACCESS]`](#sf-teams-defaults-rootaccess) +- [`sf teams defaults [ACCESS]`](#sf-teams-defaults-access) - [`sf teams domains`](#sf-teams-domains) - [`sf teams domains add DOMAIN`](#sf-teams-domains-add-domain) -- [`sf teams domains ls`](#sf-teams-domains-ls) -- [`sf teams domains rm DOMAIN`](#sf-teams-domains-rm-domain) +- [`sf teams domains list`](#sf-teams-domains-list) +- [`sf teams domains remove DOMAIN`](#sf-teams-domains-remove-domain) - [`sf teams invitations`](#sf-teams-invitations) -- [`sf teams invitations add EMAILS`](#sf-teams-invitations-add-emails) - [`sf teams invitations cancel INVITATION`](#sf-teams-invitations-cancel-invitation) -- [`sf teams invitations ls`](#sf-teams-invitations-ls) +- [`sf teams invitations list`](#sf-teams-invitations-list) - [`sf teams invitations resend INVITATION`](#sf-teams-invitations-resend-invitation) - [`sf teams join [TEAM]`](#sf-teams-join-team) - [`sf teams join-link`](#sf-teams-join-link) -- [`sf teams ls`](#sf-teams-ls) +- [`sf teams list`](#sf-teams-list) - [`sf teams members`](#sf-teams-members) -- [`sf teams members ls`](#sf-teams-members-ls) -- [`sf teams members rm MEMBER`](#sf-teams-members-rm-member) +- [`sf teams members invite EMAILS`](#sf-teams-members-invite-emails) +- [`sf teams members list`](#sf-teams-members-list) +- [`sf teams members remove MEMBER`](#sf-teams-members-remove-member) - [`sf teams subteams`](#sf-teams-subteams) - [`sf teams subteams create NAME`](#sf-teams-subteams-create-name) -- [`sf teams subteams ls`](#sf-teams-subteams-ls) +- [`sf teams subteams delete SUBTEAM`](#sf-teams-subteams-delete-subteam) +- [`sf teams subteams list`](#sf-teams-subteams-list) - [`sf teams subteams members`](#sf-teams-subteams-members) - [`sf teams subteams members add SUBTEAM USER`](#sf-teams-subteams-members-add-subteam-user) -- [`sf teams subteams members ls SUBTEAM`](#sf-teams-subteams-members-ls-subteam) -- [`sf teams subteams members rm SUBTEAM USER`](#sf-teams-subteams-members-rm-subteam-user) +- [`sf teams subteams members list SUBTEAM`](#sf-teams-subteams-members-list-subteam) +- [`sf teams subteams members remove SUBTEAM USER`](#sf-teams-subteams-members-remove-subteam-user) - [`sf teams subteams rename SUBTEAM NAME`](#sf-teams-subteams-rename-subteam-name) -- [`sf teams subteams rm SUBTEAM`](#sf-teams-subteams-rm-subteam) - [`sf teams switch [TEAM]`](#sf-teams-switch-team) -- [`sf transfers accept ID`](#sf-transfers-accept-id) -- [`sf transfers cancel ID`](#sf-transfers-cancel-id) -- [`sf transfers get ID`](#sf-transfers-get-id) -- [`sf transfers ls`](#sf-transfers-ls) +- [`sf transfers accept TRANSFER`](#sf-transfers-accept-transfer) +- [`sf transfers cancel TRANSFER`](#sf-transfers-cancel-transfer) +- [`sf transfers inspect TRANSFER`](#sf-transfers-inspect-transfer) +- [`sf transfers list`](#sf-transfers-list) - [`sf unlink`](#sf-unlink) - [`sf users`](#sf-users) - [`sf users delete USER`](#sf-users-delete-user) -- [`sf users get USER`](#sf-users-get-user) -- [`sf users ls`](#sf-users-ls) +- [`sf users inspect USER`](#sf-users-inspect-user) +- [`sf users list`](#sf-users-list) - [`sf users reactivate USER`](#sf-users-reactivate-user) - [`sf users revoke USER`](#sf-users-revoke-user) - [`sf users sessions USER`](#sf-users-sessions-user) - [`sf users settings`](#sf-users-settings) - [`sf users suspend USER`](#sf-users-suspend-user) - [`sf versions`](#sf-versions) -- [`sf versions get [VERSION]`](#sf-versions-get-version) -- [`sf versions ls`](#sf-versions-ls) -- [`sf versions rm [VERSION]`](#sf-versions-rm-version) +- [`sf versions delete [VERSION]`](#sf-versions-delete-version) +- [`sf versions inspect [VERSION]`](#sf-versions-inspect-version) +- [`sf versions list`](#sf-versions-list) - [`sf whoami`](#sf-whoami) - [`sf wp`](#sf-wp) - [`sf zero`](#sf-zero) - [`sf zero abilities`](#sf-zero-abilities) +- [`sf zero build`](#sf-zero-build) - [`sf zero call ABILITY`](#sf-zero-call-ability) -- [`sf zero import SOURCE DIRECTORY`](#sf-zero-import-source-directory) +- [`sf zero check`](#sf-zero-check) +- [`sf zero dev`](#sf-zero-dev) +- [`sf zero import SOURCE INPUT`](#sf-zero-import-source-input) +- [`sf zero init [NAME]`](#sf-zero-init-name) +- [`sf zero migrate`](#sf-zero-migrate) - [`sf zero queries`](#sf-zero-queries) - [`sf zero types`](#sf-zero-types) @@ -387,39 +400,43 @@ Revoke visitor sessions. ```text USAGE - $ sf access [--profile ] [-y] + $ sf access [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Revoke visitor sessions. Revoke active visitor sessions. Use `sf share` to manage who can open a space. - -EXAMPLES - Revoke visitor sessions. - - $ sf access ``` -## `sf access logout-all` +## `sf access sessions revoke` Revoke all visitor sessions. ```text USAGE - $ sf access logout-all [--profile ] [-y] [--claim-token - ] [-o ] [--space ] + $ sf access sessions revoke [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] [-T ] [--space ] GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Revoke all visitor sessions. @@ -429,7 +446,7 @@ DESCRIPTION EXAMPLES Revoke every visitor session on the space. - $ sf access logout-all --space docs + $ sf access sessions revoke --space docs ``` ## `sf activity` @@ -438,23 +455,27 @@ Show activity events. ```text USAGE - $ sf activity [--profile ] [-y] [--claim-token - ] [-o ] [--space ] [--all] [--code ] [--since ] [--until ] [--limit - ] [--cursor ] + $ sf activity [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] [-T ] [--space ] [--all] [--code ] + [--since ] [--until ] [--limit ] [--cursor ] FLAGS - --all Read account-wide activity instead of a single space. - --code= Filter by activity taxonomy code, for example version.promoted. - --cursor= Pagination cursor from a previous response. - --limit= Maximum number of events to return (default 50, max 100). - --since= Only events at or after this ISO timestamp. - --until= Only events before this ISO timestamp. + --all Read account-wide activity instead of a single space. + --code= Filter by activity taxonomy code, for example version.promoted. + --cursor= Pagination cursor from a previous response. + --limit= Maximum number of events to return (default 50, max 100). + --since= Only events at or after this ISO timestamp. + --until= Only events before this ISO timestamp. GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Show activity events. @@ -475,8 +496,8 @@ Write Spacefast agent instructions. ```text USAGE - $ sf agents init [DIRECTORY] [--profile ] [-y] [--agent - generic|claude-code] + $ sf agents init [DIRECTORY] [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] + [--profile ] [--token ] [-y] [--agent generic|claude-code] ARGUMENTS [DIRECTORY] Directory that will contain AGENTS.md and CLAUDE.md. Defaults to the current directory. @@ -486,10 +507,14 @@ FLAGS GLOBAL FLAGS + -d, --debug Print redacted diagnostic detail on stderr (SPACEFAST_DEBUG). -y, --yes [env: SPACEFAST_YES] Skip confirmation prompts. - --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. - --json Format output as json. - --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. + --api-url= [env: SPACEFAST_API_URL] Spacefast API base URL. + --cwd= Working directory for this invocation. + --[no-]json Print the result as JSON. + --no-color Disable color (NO_COLOR). + --non-interactive Never prompt; fail with the required inputs instead (SPACEFAST_NON_INTERACTIVE). + --profile= [env: SPACEFAST_PROFILE] Named provider profile from `sf profiles`. DESCRIPTION Write Spacefast agent instructions. @@ -512,18 +537,22 @@ Print runtime analytics. ```text USAGE - $ sf analytics [--profile ] [-y] [--claim-token - ] [-o ] [--space ] [--window 48h|7d|30d] + $ sf analytics [--api-url ] [--cwd ] [-d] [--no-color] [--non-interactive] [--profile + ] [--token ] [-y] [-T ] [--space ] [--window 48h|7d|30d] FLAGS --window=