From c4f75d3a53f716f058703c205ee35852aa1ffa3f Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 14:52:38 -0400 Subject: [PATCH 01/20] Shorten the page-opener sentence across all 53 pages Every page opened with a template sentence cramming 3-4 unrelated outcomes into one long run-on (often 30+ words, Spacefast.SentenceLength's existing suggestion-level threshold). Rewrites each opener to one short sentence naming the single most important outcome, dropping secondary ones already covered by the page's own headings. Keeps the existing 2nd-person voice; no facts changed, only trimmed and verified against each page's body. Follows WooCommerce's (Automattic-owned) developer docs style guide: be concise, lead with importance. Adds the rule to AGENTS.md so it doesn't drift back. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 6 ++++++ content/(account)/api-keys.mdx | 2 +- content/(account)/authentication.mdx | 2 +- content/(concepts)/spaces.mdx | 2 +- content/(concepts)/teams.mdx | 2 +- content/(concepts)/versions.mdx | 2 +- content/(dynamic)/crons.mdx | 2 +- content/(dynamic)/database.mdx | 2 +- content/(dynamic)/environment-variables.mdx | 2 +- content/(dynamic)/functions.mdx | 2 +- content/(dynamic)/logs.mdx | 2 +- content/(dynamic)/storage.mdx | 2 +- content/(dynamic)/wordpress.mdx | 2 +- content/(dynamic)/zero-runtime.mdx | 2 +- content/(publish)/anonymous-and-claim.mdx | 2 +- content/(publish)/ci.mdx | 2 +- content/(publish)/frameworks.mdx | 2 +- content/(publish)/git.mdx | 2 +- content/(publish)/publish.mdx | 2 +- content/(publish)/wordpress-data-sources.mdx | 2 +- content/(serve)/access.mdx | 2 +- content/(serve)/caching.mdx | 2 +- content/(serve)/customization.mdx | 2 +- content/(serve)/domains.mdx | 2 +- content/(serve)/routing.mdx | 2 +- content/(serve)/site-pages.mdx | 2 +- content/(serve)/stats.mdx | 2 +- content/(serve)/urls.mdx | 2 +- content/agents/claude-code.mdx | 2 +- content/agents/claude-desktop.mdx | 2 +- content/agents/codex.mdx | 2 +- content/agents/cursor.mdx | 2 +- content/agents/mcp-server.mdx | 2 +- content/agents/other-clients.mdx | 2 +- content/agents/permissions.mdx | 2 +- content/agents/sf-setup.mdx | 2 +- content/agents/skills.mdx | 2 +- content/cli/agents.mdx | 2 +- content/cli/builds.mdx | 2 +- content/cli/db.mdx | 2 +- content/cli/domains.mdx | 2 +- content/cli/env.mdx | 2 +- content/cli/git.mdx | 2 +- content/cli/login.mdx | 2 +- content/cli/project.mdx | 2 +- content/cli/publish.mdx | 2 +- content/cli/share.mdx | 2 +- content/cli/source.mdx | 2 +- content/cli/spaces.mdx | 2 +- content/cli/storage.mdx | 2 +- content/cli/teams.mdx | 2 +- content/cli/versions.mdx | 2 +- content/cli/zero.mdx | 2 +- content/quickstart.mdx | 2 +- 54 files changed, 59 insertions(+), 53 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 84942bae..f9bbca1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,12 @@ Assume every commit and every line of history will be public. flags, routes, defaults, or timelines. - Keep the voice direct, no-BS, and a little playful. Prefer the best path over an encyclopedia of alternatives. +- Open each page with one short sentence naming the single most important + outcome, not an inventory of every outcome the page covers — the page's own + headings already carry the rest. Be concise; lead with importance (see + WooCommerce's developer docs style guide). `styles/Spacefast/SentenceLength.yml` + flags sentences over 30 words as a suggestion; an opener that trips it is a + sign to cut, not to find a way to keep every clause. - Do not add navigation to a section until that section has a real page or generated source. - Authored navigation comes from `content/**` and its `meta.ts` files. diff --git a/content/(account)/api-keys.mdx b/content/(account)/api-keys.mdx index c91d9264..83ff3fe4 100644 --- a/content/(account)/api-keys.mdx +++ b/content/(account)/api-keys.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you can mint an API key with the right permissions, use it against the API, rotate it without downtime, and recognize every other credential Spacefast hands you by its prefix. +After this page you can mint a scoped API key and rotate it without downtime. ## Create an API key diff --git a/content/(account)/authentication.mdx b/content/(account)/authentication.mdx index 4d95890f..3ab98fb9 100644 --- a/content/(account)/authentication.mdx +++ b/content/(account)/authentication.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can create an account, sign in the way that suits you, lock the account down with two-factor, see every signed-in device, and log the `sf` CLI in from a terminal. +After this page you can create an account, sign in the way that suits you, and lock it down with two-factor. You'll also log the `sf` CLI in from a terminal. ## Create an account diff --git a/content/(concepts)/spaces.mdx b/content/(concepts)/spaces.mdx index 19dfa0dc..1f086bf5 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can pick a slug the API will accept, find any Space from the CLI or the dashboard, and predict exactly what a rename or a delete does to links you already shared. +After this page you can pick a slug the API will accept — and know exactly what a rename or delete does to links you've already shared. ## What a Space is diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index 7f209620..2ca80a9b 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can read the role table without guessing, invite and remove people, set what new Spaces start out as, and move a Space to another team. +After this page you know what each role can do and how to invite people to your team. ## What a team is diff --git a/content/(concepts)/versions.mdx b/content/(concepts)/versions.mdx index fe468dd1..0666df2f 100644 --- a/content/(concepts)/versions.mdx +++ b/content/(concepts)/versions.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you know what a version holds, how it reaches `ready`, how the `live` pointer moves, and how to roll back to any earlier version in seconds. +After this page you know how the `live` pointer works — and how to roll back to any earlier version in seconds. ## A version is a snapshot diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index c3fad78f..2139a0dc 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -After this page you can schedule recurring work, write a valid schedule the first time, trigger a job by hand, and find out why one failed. +After this page you can schedule recurring work and write a valid schedule the first time. ## How a run happens diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index 5ed87066..0ba3a7ea 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you can read your space's schema and rows from the CLI, apply a migration, take a full backup, and open a SQL console from the dashboard. +After this page you can inspect your space's database from the CLI and apply a migration safely. ## What it is diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index b7a2dab2..7badb915 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can set a variable from the CLI or the dashboard, know when it reaches a running site, and pick the right lane for a server-only secret. +After this page you can set a variable from the CLI or dashboard — and know when it actually reaches your site. ## Two scopes diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index bf9dfa09..c7ec4d54 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can add a worker to a Space, know which file layout the publish detects, write a handler with the right signature, and find its logs. +After this page you can add a worker to a Space and write a handler with the right signature. 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. diff --git a/content/(dynamic)/logs.mdx b/content/(dynamic)/logs.mdx index 83290a15..bf0babc1 100644 --- a/content/(dynamic)/logs.mdx +++ b/content/(dynamic)/logs.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -After this page you can tail what your handlers log, find every line one request wrote, and read a build's output. +After this page you can tail your logs and find every line one request wrote. ## Three streams diff --git a/content/(dynamic)/storage.mdx b/content/(dynamic)/storage.mdx index 87cdabd6..c391e800 100644 --- a/content/(dynamic)/storage.mdx +++ b/content/(dynamic)/storage.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -After this page you can accept a file upload in a running app, get its URL, list what a Space is holding, and delete an object. +After this page you can accept a file upload in a running app and get its URL. ## What it is diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index cefde388..c2246781 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -3,7 +3,7 @@ title: WordPress on every Space description: Every Space runs a WordPress behind its static files. Run WP-CLI against it, call its Abilities with a short-lived token, and open its database. --- -After this page you can run any WP-CLI command against a Space, call the Abilities its runtime publishes with a token you mint from the API, and know where the database console lives. +After this page you can run any WP-CLI command against a Space — and call its Abilities with a token you mint from the API. Every Space is backed by a WordPress install. Your published files serve in front of it, 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](/storage) is its media library, and [Database](/database) is its MySQL. This page covers the two ways to reach that WordPress directly. diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index faef910e..2c5272a0 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -5,7 +5,7 @@ sidebar: order: 0 --- -After this page you can tell whether your project needs Zero, declare it in `sf.jsonc`, write the smallest app that works, and know which page owns each piece. +After this page you can tell whether your project needs Zero and declare it in `sf.jsonc`. ## What Zero is diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index d4e77174..62b934b6 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can put a site online with no signup, know how long it stays up, and move it into a team later while keeping the same URL. +After this page you can put a site online with no signup — and claim it later without losing the URL. ## Publish with no login diff --git a/content/(publish)/ci.mdx b/content/(publish)/ci.mdx index 413afd92..92290d17 100644 --- a/content/(publish)/ci.mdx +++ b/content/(publish)/ci.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -After this page your pipeline publishes on every push to your default branch, prints the live URL, and posts a preview version for pull requests without touching live traffic. +After this page your pipeline publishes automatically on every push — and ships a preview version for pull requests without touching live traffic. ## Set it up diff --git a/content/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index bf495e76..4ab7dae9 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you know which directory your framework produces, when to publish that directory yourself versus letting Spacefast build, how detection picks commands, and where to read a failing build. +After this page you know whether to publish your own build output or let Spacefast build it — and where to look when a build fails. ## Two paths diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 7471a337..3ecc9dba 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -After this page you can publish with `git push`, connect a GitHub repository so pushes build on their own, decide which branch goes live, and find out why a push produced nothing. +After this page you can publish straight from `git push`, or connect a GitHub repository so every push builds and publishes on its own. Two lanes. The Spacefast remote needs no external host and no app install. A GitHub connection keeps your repository where it is and reacts to its webhooks. diff --git a/content/(publish)/publish.mdx b/content/(publish)/publish.mdx index 072d57da..fbac4e29 100644 --- a/content/(publish)/publish.mdx +++ b/content/(publish)/publish.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can publish a folder from the CLI, the dashboard, or the API, predict which files make it and which get dropped, and read a receipt well enough to know whether your site is live. +After this page you can publish a folder from the CLI, dashboard, or API — and read the receipt well enough to know whether your site is live. ## The mental model diff --git a/content/(publish)/wordpress-data-sources.mdx b/content/(publish)/wordpress-data-sources.mdx index 94eba00b..50f34b01 100644 --- a/content/(publish)/wordpress-data-sources.mdx +++ b/content/(publish)/wordpress-data-sources.mdx @@ -3,7 +3,7 @@ title: Build from a WordPress site description: Point a Space's repository build at a public WordPress site, read its content over the REST API during the build, and publish the static output --- -After this page you can name one or more public WordPress sites on a Space, read their content while your repository builds, and know exactly which environment variables the build receives. +After this page you can point a repository build at a public WordPress site and read its content while the build runs. A data source is a public WordPress site a build reads content from. Nothing is copied into the Space and nothing is written back. The platform hands the build a URL and a source map, your build code fetches over the WordPress REST API, and the output publishes like any other static build. diff --git a/content/(serve)/access.mdx b/content/(serve)/access.mdx index 5c696649..7a54eb53 100644 --- a/content/(serve)/access.mdx +++ b/content/(serve)/access.mdx @@ -4,7 +4,7 @@ description: Make a Space public, hand out scoped share links or a password, inv sidebar: { order: 5 } --- -After this page you can decide who opens a Space, hand out a link or a password scoped to part of it, and take any of that access back. +After this page you can decide exactly who opens a Space, down to one shared link or password, and revoke any of it instantly. ## Access is a list, not a switch diff --git a/content/(serve)/caching.mdx b/content/(serve)/caching.mdx index 14856a4f..b6c557cc 100644 --- a/content/(serve)/caching.mdx +++ b/content/(serve)/caching.mdx @@ -4,7 +4,7 @@ description: The two cache policies a published Space sends, which files get whi sidebar: { order: 4 } --- -After this page you know exactly which `Cache-Control` header each file in your Space gets, what a publish invalidates, and how to get a fresh response when you need one. +After this page you know exactly which `Cache-Control` header each file in your Space gets — and how to force a fresh response when you need one. ## Two policies diff --git a/content/(serve)/customization.mdx b/content/(serve)/customization.mdx index f2deb439..d9e9a3c3 100644 --- a/content/(serve)/customization.mdx +++ b/content/(serve)/customization.mdx @@ -3,7 +3,7 @@ title: Customization description: Theme the pages Spacefast draws, set the title and image links unfurl with, add an analytics tag, and run one site-wide script --- -After this page you can brand the pages Spacefast draws for your Space, control how a link to it unfurls, add a Google Analytics or Tag Manager id, and run one script on every published page. +After this page you can brand the pages Spacefast draws for your Space, and control how a link to it unfurls when shared. Open the Space and go to **Customization**. Four tabs, in order: **Theme**, **Search & sharing**, **Google Analytics**, and **Custom JS**. A large live preview sits beside the controls, labelled **Visitor sees**, painted from your unsaved draft. diff --git a/content/(serve)/domains.mdx b/content/(serve)/domains.mdx index f9bdb8ff..9a5090e2 100644 --- a/content/(serve)/domains.mdx +++ b/content/(serve)/domains.mdx @@ -4,7 +4,7 @@ description: Connect a domain you own, add the DNS records Spacefast asks for, g sidebar: { order: 2 } --- -After this page you can point a domain you own at a Space, read every verification and certificate state the platform reports, make the domain the Space's primary address, and move it somewhere else later. +After this page you can point a domain you own at a Space, and make it the Space's primary, live address. A domain belongs to a **team**, not to a Space. You add it once to the team's inventory, then assign it to a Space. One hostname serves exactly one Space; one Space can hold many hostnames. diff --git a/content/(serve)/routing.mdx b/content/(serve)/routing.mdx index 95c6768d..618dc551 100644 --- a/content/(serve)/routing.mdx +++ b/content/(serve)/routing.mdx @@ -4,7 +4,7 @@ description: The _redirects and _headers files, the routing rules in sf.jsonc, h sidebar: { order: 3 } --- -After this page you can redirect, rewrite, proxy, and set response headers on a published Space, and you know which rule wins when two of them match the same URL. +After this page you can redirect, rewrite, proxy, and set response headers on a published Space — and know which rule wins when two overlap. Nothing here is evaluated per request. Spacefast compiles your rules at publish time into a table the edge reads, so a rule that behaves unexpectedly is almost always a compile-time problem. `sf routing inspect` shows you the compiled result before you publish. diff --git a/content/(serve)/site-pages.mdx b/content/(serve)/site-pages.mdx index 98eff027..07cc73f2 100644 --- a/content/(serve)/site-pages.mdx +++ b/content/(serve)/site-pages.mdx @@ -3,7 +3,7 @@ title: Layout, theme, and site pages description: Theme the pages Spacefast draws with theme.json, wrap them in your own _layout.html, or take one over completely with _pages/id.html --- -After this page you can push your own design tokens into the pages Spacefast draws, wrap every one of them in your site chrome, replace any of the five outright, and check all of it before you publish. +After this page you know three ways to make Spacefast's built-in pages your own — from design tokens up to full page takeovers. Spacefast draws five pages for a Space that has not written its own. Three levels of control, in increasing order of how much you own: diff --git a/content/(serve)/stats.mdx b/content/(serve)/stats.mdx index 7cba20bc..31c8bf7b 100644 --- a/content/(serve)/stats.mdx +++ b/content/(serve)/stats.mdx @@ -3,7 +3,7 @@ title: Traffic stats description: Requests, views, unique visitors, and top paths for a Space, where the numbers come from, and how to read them from the CLI and the API --- -After this page you can read a Space's traffic in the dashboard, the CLI, or the API, and you know what each number counts and what it deliberately does not. +After this page you know what each of a Space's traffic numbers counts — and what it deliberately leaves out. ## Where to look diff --git a/content/(serve)/urls.mdx b/content/(serve)/urls.mdx index e8a571ba..cbeeafa3 100644 --- a/content/(serve)/urls.mdx +++ b/content/(serve)/urls.mdx @@ -4,7 +4,7 @@ description: Every hostname a Space answers on, which one counts as the live URL sidebar: { order: 1 } --- -After this page you know every hostname a Space answers on, which one the CLI and API report as the live URL, and what happens to the rest once a custom domain is primary. +After this page you know every hostname a Space answers on, and which one counts as the live URL. ## The default hostname diff --git a/content/agents/claude-code.mdx b/content/agents/claude-code.mdx index 0b3a7f99..3be930ed 100644 --- a/content/agents/claude-code.mdx +++ b/content/agents/claude-code.mdx @@ -3,7 +3,7 @@ title: Claude Code description: Install the Spacefast plugin in Claude Code, what it adds to your session, and the prompts that work --- -After this page Claude Code can publish your project, read its deployments, domains, and traffic, and manage the rest of your account without you leaving the terminal. +After this page Claude Code can publish your project to Spacefast and manage your account — without leaving the terminal. ## Install diff --git a/content/agents/claude-desktop.mdx b/content/agents/claude-desktop.mdx index 57555fb5..5aabf557 100644 --- a/content/agents/claude-desktop.mdx +++ b/content/agents/claude-desktop.mdx @@ -3,7 +3,7 @@ title: Claude Desktop description: Install the Spacefast desktop extension for Claude Desktop, or paste the hosted MCP server into your config --- -After this page Claude Desktop can publish files to Spacefast and read your Spaces, either through a double-click extension that runs the server on your machine or through the hosted endpoint. +After this page Claude Desktop can publish files to Spacefast and read your Spaces. ## Install the extension diff --git a/content/agents/codex.mdx b/content/agents/codex.mdx index 0ebf4196..599e61a2 100644 --- a/content/agents/codex.mdx +++ b/content/agents/codex.mdx @@ -3,7 +3,7 @@ title: Codex description: Install the Spacefast plugin in Codex, or add the MCP server to config.toml by hand --- -After this page Codex can publish your project, check builds, domains, and traffic, and fall back to bundled curl scripts when MCP is unavailable. +After this page Codex can publish your project to Spacefast — and fall back to curl scripts when MCP isn't available. ## Install diff --git a/content/agents/cursor.mdx b/content/agents/cursor.mdx index d1487d8c..9ce174d5 100644 --- a/content/agents/cursor.mdx +++ b/content/agents/cursor.mdx @@ -3,7 +3,7 @@ title: Cursor description: Install the Spacefast plugin in Cursor, or add the MCP server to mcp.json by hand --- -After this page Cursor can publish the project you have open, read its deployments, domains, and traffic, and run the rest of the Spacefast API through one sandboxed tool. +After this page Cursor can publish the project you have open and run the rest of the Spacefast API through one sandboxed tool. ## Install diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index 1e9682b1..de20ec74 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -3,7 +3,7 @@ title: MCP server description: Connect to the Spacefast MCP server over hosted HTTP or local stdio, and use its five tools, execute sandbox, and approval model --- -After this page you can connect any MCP client to Spacefast, read every tool's input schema, write `execute` programs against the whole Spacefast API, and know exactly when a call will stop and ask you for approval. +After this page you can connect any MCP client to Spacefast and know exactly when a call needs your approval. ## Two runtimes, one tool set diff --git a/content/agents/other-clients.mdx b/content/agents/other-clients.mdx index cf4e83ae..d3c4fd91 100644 --- a/content/agents/other-clients.mdx +++ b/content/agents/other-clients.mdx @@ -3,7 +3,7 @@ title: Any MCP client description: Connect any MCP client to Spacefast over hosted HTTP or local stdio, and what sf mcp proxy does --- -After this page you can wire Spacefast into an MCP client that has no first-party plugin, choose between the hosted endpoint and a local stdio server, and know what the CLI's default proxy lane actually does. +After this page you can wire Spacefast into any MCP client that has no first-party plugin, and know what the CLI's default proxy lane actually does. ## Pick a transport diff --git a/content/agents/permissions.mdx b/content/agents/permissions.mdx index 7a67d026..7d4e9bdd 100644 --- a/content/agents/permissions.mdx +++ b/content/agents/permissions.mdx @@ -3,7 +3,7 @@ title: What agents are allowed to do description: How an agent gets access to your Spacefast account, what it can never do without you, and how approvals, handoffs, and revocation work --- -After this page you can decide how much access to hand an agent, recognize the moments where it has to come back to you, and cut it off in one click when you are done. +After this page you can decide how much access to hand an agent, and cut it off in one click when you're done. ## How an agent gets access diff --git a/content/agents/sf-setup.mdx b/content/agents/sf-setup.mdx index 428c5af3..c89e9da3 100644 --- a/content/agents/sf-setup.mdx +++ b/content/agents/sf-setup.mdx @@ -3,7 +3,7 @@ title: sf setup agent description: Configure MCP and install the Spacefast skill for every agent on your machine with one CLI command --- -After this page you can point every agent client on your machine at Spacefast with one command, know exactly which files it touches, and drive it non-interactively in a script. +After this page you can point every agent client on your machine at Spacefast with one command, and drive it non-interactively in a script. ## The command diff --git a/content/agents/skills.mdx b/content/agents/skills.mdx index aa281d61..48f54324 100644 --- a/content/agents/skills.mdx +++ b/content/agents/skills.mdx @@ -3,7 +3,7 @@ title: Skills description: The Spacefast agent skill, what it tells your agent to do, and how to install or remove it with the sf CLI --- -After this page you know what the shipped skill instructs your agent to do, which clients get which variant, and how to install, check, or remove it. +After this page you know what the shipped skill instructs your agent to do, and how to install, check, or remove it. ## One skill, called `spacefast` diff --git a/content/cli/agents.mdx b/content/cli/agents.mdx index 8deedff5..bc0059f8 100644 --- a/content/cli/agents.mdx +++ b/content/cli/agents.mdx @@ -5,7 +5,7 @@ sidebar: order: 26 --- -After this page you can point Claude Code, Cursor, Codex, or any other MCP client at Spacefast in one command, understand which transport you got and why, install or remove the bundled skill, and drop a Spacefast block into your project's `AGENTS.md`. +After this page you can point Claude Code, Cursor, Codex, or any other MCP client at Spacefast in one command. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`). `sf mcp` and `sf mcp proxy` do not support `--json`, because stdout carries the MCP protocol. diff --git a/content/cli/builds.mdx b/content/cli/builds.mdx index bc303c8c..0113b945 100644 --- a/content/cli/builds.mdx +++ b/content/cli/builds.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can pack a build output archive on your own machine, and drive every remote build a Space has: list them, read their logs live, cancel one, and retry a finished one. +After this page you can pack a build output archive locally, and drive every remote build a Space runs. A build is the thing that turns source into a version. `sf publish --remote` and repository pushes create builds; `sf build` runs the same build locally and leaves you an archive. See [Frameworks and builds](/frameworks) for detection and the build settings model. diff --git a/content/cli/db.mdx b/content/cli/db.mdx index 86386611..9ff14daf 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -5,7 +5,7 @@ sidebar: order: 22 --- -After this page you can read a Space's tables and pending migration plan, pull rows out as JSON, take a full backup, apply a migration, open a real SQL console, and fire a scheduled job on demand. +After this page you can inspect a Space's database, apply schema migrations, and fire a scheduled job on demand. 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/domains.mdx b/content/cli/domains.mdx index be08cf2b..24dee8ae 100644 --- a/content/cli/domains.mdx +++ b/content/cli/domains.mdx @@ -5,7 +5,7 @@ sidebar: order: 20 --- -After this page you can attach a custom domain, make it primary or a redirect, watch DNS and TLS come up, take a hostname off a Space, and compile your `_redirects` and `_headers` before you publish them. +After this page you can attach a custom domain to a Space and watch its DNS and TLS come up. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`) and, except `sf routing inspect`, the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli) for those. diff --git a/content/cli/env.mdx b/content/cli/env.mdx index 2c47cda5..ba67f8fa 100644 --- a/content/cli/env.mdx +++ b/content/cli/env.mdx @@ -5,7 +5,7 @@ sidebar: order: 21 --- -After this page you can set a variable without putting it in your shell history, import a whole `.env` file, pull the readable ones back into a local file, and know exactly when a change takes effect. +After this page you can set a Space's variables without putting secrets in your shell history, and know exactly when changes take effect. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). All except `sf env export-template` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). diff --git a/content/cli/git.mdx b/content/cli/git.mdx index aacbc239..b5e0c14e 100644 --- a/content/cli/git.mdx +++ b/content/cli/git.mdx @@ -5,7 +5,7 @@ sidebar: order: 24 --- -After this page you can connect a GitHub repository to a Space, trigger and watch a remote build, install a signed push remote, and list the GitHub App installations and repositories available to you. +After this page you can connect a GitHub repository to a Space and trigger a remote build from it. 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/login.mdx b/content/cli/login.mdx index a7ee5f95..bbefab04 100644 --- a/content/cli/login.mdx +++ b/content/cli/login.mdx @@ -5,7 +5,7 @@ sidebar: order: 8 --- -After this page you can sign in through the browser, sign in with an API key on a machine that has no browser, confirm which account and teams a credential reaches, and revoke it. +After this page you can sign this machine in through the browser, or with an API key when there's no browser. Every command here also takes the [global flags](/cli#global-flags). diff --git a/content/cli/project.mdx b/content/cli/project.mdx index a8dd71a4..380f2d14 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -5,7 +5,7 @@ sidebar: order: 9 --- -After this page you can scaffold a project, link a directory to a Space so `sf publish` needs no flags, read that link back, and move between teams and providers. +After this page you can scaffold a project and link a directory to a Space so `sf publish` needs no flags. Two files carry the link. `.spacefast/space.json` is the committable one: which Space, which team, which API base URL. `.spacefast/state.json` is the local cache and holds credentials, so it never gets committed and the CLI adds it to `.gitignore` for you. diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index 69d6b82a..09381d1b 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you can publish a folder, build a framework project locally or remotely, publish without an account and claim the Space later, and parse the `--json` receipt in a script. `sf dev` is documented at the end because it runs a project locally, not a copy of what publish serves. +After this page you can publish any folder, project, or archive to a Space with one command. `sf dev` is documented at the end because it runs a project locally, not a copy of what publish serves. ## sf publish diff --git a/content/cli/share.mdx b/content/cli/share.mdx index e2c924d4..4bab512d 100644 --- a/content/cli/share.mdx +++ b/content/cli/share.mdx @@ -5,7 +5,7 @@ sidebar: order: 7 --- -After this page you can open a Space to the public or to your team, send someone a link that expires, put a password on a route subtree, mint a token for a machine, invite a named person to one path, and explain exactly why a given visitor can or cannot see a URL. +After this page you can grant access to a Space with links, passwords, or machine tokens — and explain exactly why a visitor can or can't see a URL. ## The model in one minute diff --git a/content/cli/source.mdx b/content/cli/source.mdx index b1e4664f..09188156 100644 --- a/content/cli/source.mdx +++ b/content/cli/source.mdx @@ -5,7 +5,7 @@ sidebar: order: 25 --- -After this page you can list and read files in a Space's source repository, search it, diff and merge branches, commit from files or a unified diff, download an archive, and create tags, all without a local clone. +After this page you can browse, search, and change a Space's source repository from the terminal — all without a local clone. 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/spaces.mdx b/content/cli/spaces.mdx index 5d9d0a4c..8f793a9a 100644 --- a/content/cli/spaces.mdx +++ b/content/cli/spaces.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -After this page you can manage a Space's whole lifecycle from the terminal: create one before you publish, look up what it is serving, rename it, pull its files back down, claim an anonymous one into your account, retire it, and hand it to another team. +After this page you can manage a Space's whole lifecycle from the terminal, from creation to transfer. A Space is one hosted site: a slug, a hostname, an owner, and a stack of versions. See [Spaces](/spaces) for the model. diff --git a/content/cli/storage.mdx b/content/cli/storage.mdx index 111794ee..8c1ef1e5 100644 --- a/content/cli/storage.mdx +++ b/content/cli/storage.mdx @@ -5,7 +5,7 @@ sidebar: order: 23 --- -After this page you can list what your app has stored, delete an object as the Space owner, page through the requests the edge served, and follow what your own code logged. +After this page you can see what your app has stored in runtime storage, and read the logs your Space produces. 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/teams.mdx b/content/cli/teams.mdx index f0f5cf17..68471961 100644 --- a/content/cli/teams.mdx +++ b/content/cli/teams.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -After this page you can create a team, pick which one your publishes land in, invite people and manage those invitations, remove members, and decide what access new Spaces start with. +After this page you can create a team, invite members, and set the access new Spaces start with. A team owns Spaces, domains, and billing. Every member holds one team role: `owner`, `admin`, or `member`. See [Teams](/teams) for the model, and [`sf share`](/cli/share) for per-Space access, which is a separate system. diff --git a/content/cli/versions.mdx b/content/cli/versions.mdx index 432d7bfd..2dec5cfb 100644 --- a/content/cli/versions.mdx +++ b/content/cli/versions.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -After this page you can find any version a Space has published, move live traffic to it, roll back after a bad publish, delete a version you no longer want served, and switch the live channel between automatic and promote-driven deploys. +After this page you can move live traffic to any version a Space has published, and roll back after a bad one. Every publish creates one immutable version. A channel is a named pointer at one version. Only `live` exists today. Promoting and rolling back move that pointer; they never rebuild anything. See [Versions](/versions) for the model. diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index d12070f0..444bd9b6 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -5,7 +5,7 @@ sidebar: order: 25 --- -After this page you can list what a Space's capsule lets an agent call, call one of those Abilities, generate TypeScript for what the platform owns, run any WP-CLI command against a Space, see exactly what is serving right now, own the default page templates, and read the traffic numbers. +After this page you can list and call what a Space's capsule exposes to agents — its Zero Abilities — and run any WP-CLI command against a Space. 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). diff --git a/content/quickstart.mdx b/content/quickstart.mdx index b8a64338..1468c0ce 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -By the end of this page you have a site live on a `view.fast` hostname, a second version you can roll back to, and a custom domain pointed at it. +By the end of this page you have a site live on a `view.fast` hostname — and a custom domain pointed at it. ## Before you start From 01520ccaf9b7c751c08637e58cc2d935712cda38 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 15:08:59 -0400 Subject: [PATCH 02/20] Tighten body prose across all 62 touched pages for concision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the opener-sentence fix to the rest of each page: wherever body prose chained 3+ unrelated clauses into a run-on, split or trimmed it, leading with the most important fact. 124 sentences touched across 62 files. Left tables, code blocks, headings, and frontmatter (other than description) untouched; verified by diffing every file's fenced code blocks before/after (zero mismatches). Sentence-length suggestion hits (Spacefast.SentenceLength) drop from 63 to 42 repo-wide; 18 of the remaining 42 are in content/(reference)/changelog/**, which AGENTS.md exempts from style rules other than public-safety and brand-casing. No facts, commands, or flags were added or changed — only cut, reordered, or split existing text. Co-Authored-By: Claude Sonnet 5 --- content/(account)/api-keys.mdx | 6 +++--- content/(account)/authentication.mdx | 2 +- content/(account)/billing.mdx | 2 +- content/(concepts)/spaces.mdx | 6 +++--- content/(concepts)/teams.mdx | 8 ++++---- content/(concepts)/versions.mdx | 4 ++-- content/(dynamic)/crons.mdx | 2 +- content/(dynamic)/database.mdx | 4 ++-- content/(dynamic)/environment-variables.mdx | 6 +++--- content/(dynamic)/functions.mdx | 2 +- content/(dynamic)/storage.mdx | 4 ++-- content/(dynamic)/wordpress.mdx | 8 ++++---- content/(dynamic)/zero-runtime.mdx | 8 ++++---- content/(publish)/anonymous-and-claim.mdx | 2 +- content/(publish)/frameworks.mdx | 6 +++--- content/(publish)/git.mdx | 2 +- content/(publish)/publish.mdx | 2 +- content/(publish)/recipes/html.mdx | 2 +- content/(publish)/recipes/next.mdx | 2 +- content/(publish)/wordpress-data-sources.mdx | 2 +- content/(reference)/config-file.mdx | 2 +- content/(reference)/limits.mdx | 2 +- content/(serve)/access.mdx | 4 ++-- content/(serve)/caching.mdx | 2 +- content/(serve)/customization.mdx | 4 ++-- content/(serve)/routing.mdx | 6 +++--- content/(serve)/site-pages.mdx | 8 ++++---- content/(serve)/stats.mdx | 2 +- content/(serve)/urls.mdx | 2 +- content/agents/claude-code.mdx | 6 +++--- content/agents/codex.mdx | 2 +- content/agents/mcp-server.mdx | 6 +++--- content/agents/other-clients.mdx | 2 +- content/agents/permissions.mdx | 2 +- content/api/authentication.mdx | 4 ++-- content/api/errors.mdx | 4 ++-- content/api/idempotency.mdx | 4 ++-- content/api/pagination.mdx | 4 ++-- content/api/rate-limits.mdx | 6 +++--- content/api/webhooks.mdx | 4 ++-- content/cli/agent-commands.mdx | 2 +- content/cli/agents.mdx | 2 +- content/cli/api-keys.mdx | 2 +- content/cli/api.mdx | 2 +- content/cli/db.mdx | 2 +- content/cli/domains.mdx | 4 ++-- content/cli/env.mdx | 4 ++-- content/cli/index.mdx | 4 ++-- content/cli/project.mdx | 4 ++-- content/cli/publish.mdx | 6 +++--- content/cli/share.mdx | 2 +- content/cli/source.mdx | 2 +- content/cli/storage.mdx | 2 +- content/cli/teams.mdx | 2 +- content/cli/zero.mdx | 10 +++++----- content/index.mdx | 2 +- content/platforms/partner-api/configuration.mdx | 2 +- content/platforms/partner-api/customers.mdx | 2 +- content/platforms/partner-api/go-live.mdx | 8 ++++---- content/platforms/partner-api/index.mdx | 2 +- content/platforms/partner-api/tokens.mdx | 8 ++++---- content/troubleshooting.mdx | 16 ++++++++-------- 62 files changed, 123 insertions(+), 123 deletions(-) diff --git a/content/(account)/api-keys.mdx b/content/(account)/api-keys.mdx index 83ff3fe4..d66a2496 100644 --- a/content/(account)/api-keys.mdx +++ b/content/(account)/api-keys.mdx @@ -57,7 +57,7 @@ A preset is a named bundle of permissions. Pick the narrowest one that does the | `billing_viewer` | Billing viewer | Read team and billing information, nothing else | | `team_admin` | Team admin | Everything this team can do | -`space_publisher` and `ci_deploy` carry the same publish permissions. `space_admin` is the default when a request names no preset, and it deliberately stops short of `team_admin`: it can never mint keys, manage members, or touch billing. +`space_publisher` and `ci_deploy` carry the same publish permissions. `space_admin` is the default when a request names no preset. It deliberately stops short of `team_admin`: it can never mint keys, manage members, or touch billing. ## How permissions compile @@ -91,9 +91,9 @@ A credential that tries gets a `403` with `authorization_level_not_allowed`. Do ## Rotate a key -A secret is immutable, so rotation means a new key. In the dashboard, **Replace key…** mints a fresh key with the same policy and then offers to revoke the old one once your integrations have moved over. +A secret is immutable, so rotation means a new key. In the dashboard, **Replace key…** mints a fresh key with the same policy. It then offers to revoke the old one once your integrations have moved over. -Revoking is immediate. Anything using that key fails on its next request, and the row stays in the list marked revoked so the audit trail survives. +Revoking is immediate. Anything using that key fails on its next request. The row stays in the list marked revoked, so the audit trail survives. ## Every credential, by prefix diff --git a/content/(account)/authentication.mdx b/content/(account)/authentication.mdx index 3ab98fb9..0de871eb 100644 --- a/content/(account)/authentication.mdx +++ b/content/(account)/authentication.mdx @@ -15,7 +15,7 @@ There is no password signup. Passwords are something you add later, from setting Two other ways in create an account for you: -- A social provider. Spacefast supports WordPress.com, Google, and GitHub, and the sign-in page shows the ones that are turned on. The provider must have verified your email address, otherwise Spacefast refuses the sign-in and tells you to verify it with the provider or use a mailed code instead. +- A social provider. Spacefast supports WordPress.com, Google, and GitHub, and the sign-in page shows the ones that are turned on. The provider must have verified your email address. Otherwise Spacefast refuses the sign-in and tells you to verify it with the provider or use a mailed code instead. - A team invitation link. The link already proves the inbox, so the accept page offers **Create account & join** with one optional **Name (optional)** field and no code to type. ## Sign in diff --git a/content/(account)/billing.mdx b/content/(account)/billing.mdx index ba4229ce..ff670f5d 100644 --- a/content/(account)/billing.mdx +++ b/content/(account)/billing.mdx @@ -86,6 +86,6 @@ Reading is never blocked in either state. Settle the payment from the billing po ## Billing is dashboard-only -Plans, checkout, and cancellation live in the dashboard. There is no `sf` command and no API endpoint for changing a plan, and API keys and agent credentials cannot reach billing at all. +Plans, checkout, and cancellation live in the dashboard. There is no `sf` command and no API endpoint for changing a plan. API keys and agent credentials cannot reach billing at all. Reading is a different story. An agent can pull the resolved plan and limits from `GET /v1/teams/{teamId}/entitlements`, current counters from `GET /v1/teams/{teamId}/usage`, and the limits as they are enforced at publish time from `GET /v1/teams/{teamId}/plan-policy`. See [Usage](/usage) for what those numbers mean. diff --git a/content/(concepts)/spaces.mdx b/content/(concepts)/spaces.mdx index 1f086bf5..f8e87fad 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -60,10 +60,10 @@ Every Space answers at `https://.view.fast/` from the moment it exists, an Exactly one owner, one of two kinds: -- **A team.** The normal case. Team members reach the Space by their role, and slugs are unique per team rather than globally, so two teams can both own `docs`. +- **A team.** The normal case. Team members reach the Space by their role. Slugs are unique per team rather than globally, so two teams can both own `docs`. - **A space key.** A Space published without an account is owned by its `sfc_` key until someone claims it. See [Publish without an account](/anonymous-and-claim). -To move a Space to another team, run `sf spaces transfer --space docs`, or open **Space settings → General → Transfer this space → Transfer space…**. If you are an owner or admin of both teams the move is instant and the Space keeps serving at its current address. Otherwise the target team has 7 days to accept, and nothing changes until they do. Roles and membership are on [Teams](/teams). +To move a Space to another team, run `sf spaces transfer --space docs`, or open **Space settings → General → Transfer this space → Transfer space…**. If you are an owner or admin of both teams, the move is instant. The Space keeps serving at its current address. Otherwise the target team has 7 days to accept, and nothing changes until they do. Roles and membership are on [Teams](/teams). ## Access when you create one @@ -127,7 +127,7 @@ What changes: - The old slug stays reserved for this Space. - Version URLs do not move. They were computed once and stored. -One thing blocks a rename. While `name` is set in your `sf.jsonc`, the API refuses to rename the Space, because the file would win back the old name on the next publish. Remove the key or rename it in the file. See [Config file](/config-file). +One thing blocks a rename. While `name` is set in your `sf.jsonc`, the API refuses to rename the Space — the file would win back the old name on the next publish. Remove the key or rename it in the file. See [Config file](/config-file). ## Deleting and restoring diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index 2ca80a9b..b1ea99e4 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -9,9 +9,9 @@ After this page you know what each role can do and how to invite people to your ## What a team is -A team is the thing that owns work. Spaces, custom domains, API keys, billing, and members all hang off it. Every claimed Space belongs to exactly one team, even when you are its only member, so there is no separate personal container to reason about. +A team is the thing that owns work. Spaces, custom domains, API keys, billing, and members all hang off it. Every claimed Space belongs to exactly one team, even when you are its only member. There is no separate personal container to reason about. -A team has a name and a slug. The slug is the first segment of its dashboard URLs and you can change it in **Team settings → General**, where a live check tells you whether the new one is free. +A team has a name and a slug. The slug is the first segment of its dashboard URLs. You can change it in **Team settings → General**, where a live check tells you whether the new one is free. | Slug rule | Value | |---|---| @@ -98,7 +98,7 @@ The default is `team`. From a terminal, `sf teams defaults` prints the current v Open the Space, then **Space settings → General → Transfer this space** and press **Transfer space…**. Pick the destination team. - If you are an owner or admin of the destination, the button reads **Move space** and it happens immediately. The Space keeps serving at its current address. -- Otherwise the button reads **Request transfer**. Owners and admins of the destination team have 7 days to accept, nothing changes until they do, and you can cancel any time before then. +- Otherwise the button reads **Request transfer**. Owners and admins of the destination team have 7 days to accept. Nothing changes until they do, and you can cancel any time before then. From a terminal: @@ -108,7 +108,7 @@ sf transfers accept sf transfers cancel ``` -Only owners and admins can transfer a Space. Two consequences to plan for: the old team's Space-scoped API keys stop working the moment the transfer completes, and custom domains keep serving but stay owned by the original team. +Only owners and admins can transfer a Space. Two consequences to plan for: the old team's Space-scoped API keys stop working the moment the transfer completes. Custom domains keep serving but stay owned by the original team. ## Leave or delete a team diff --git a/content/(concepts)/versions.mdx b/content/(concepts)/versions.mdx index 0666df2f..0fe5fafa 100644 --- a/content/(concepts)/versions.mdx +++ b/content/(concepts)/versions.mdx @@ -116,7 +116,7 @@ This only affects versions still in `created`, `uploading`, or `uploaded`. A rea The upcoming plans introduce [time-based history windows](/usage#planned-history-and-log-retention). Those windows are planned; the following describes current cleanup. -Ready versions stay until you delete them, with one exception. Free Spaces keep 200 versions, counting everything that is not failed, canceled, or expired. The publish that takes a Free Space past 200 still goes through, and the oldest ready versions that nothing serves expire in the background: their bytes are purged and their history stays. A version the live pointer or a branch alias serves is never expired. Paid plans have no cap. See [Limits](/limits). +Ready versions stay until you delete them, with one exception. Free Spaces keep 200 versions, counting everything that is not failed, canceled, or expired. The publish that takes a Free Space past 200 still goes through. The oldest ready versions that nothing serves expire in the background — their bytes are purged, but their history stays. A version the live pointer or a branch alias serves is never expired. Paid plans have no cap. See [Limits](/limits). To delete one: @@ -128,7 +128,7 @@ A version the `live` channel points at cannot be deleted, and neither can one a ## Compare what you uploaded with what is served -A version stores two representations of every path. `uploaded` is the byte you sent, before any transform. `served` is the immutable byte the version answers with. They differ where Spacefast processes a file, and a private input such as `sf.jsonc` exists in `uploaded` and is absent from `served` entirely. +A version stores two representations of every path. `uploaded` is the byte you sent, before any transform. `served` is the immutable byte the version answers with. They differ where Spacefast processes a file. A private input such as `sf.jsonc` exists in `uploaded` and is absent from `served` entirely. [`listSpaceVersionFileLinks`](/api/reference/versions/listspaceversionfilelinks) mints short-lived read links for up to 200 paths at a time and takes `view=uploaded` or `view=served`. Fetch the same path both ways and diff them. diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index 2139a0dc..072df32e 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -105,4 +105,4 @@ Only the newest failures are kept, with no history behind them. A real failure a ## API -[`listSpaceCrons`](/api/reference/spaces/listspacecrons) is `GET /v1/spaces/{spaceId}/crons`. It returns the version the schedules came from, which is `null` before the first publish, and one entry per cron with its key, path, schedule, and recent failures. +[`listSpaceCrons`](/api/reference/spaces/listspacecrons) is `GET /v1/spaces/{spaceId}/crons`. It returns the version the schedules came from, which is `null` before the first publish. Each entry carries its key, path, schedule, and recent failures. diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index 0ba3a7ea..73b46b9a 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -11,7 +11,7 @@ After this page you can inspect your space's database from the CLI and apply a m 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`. -You do not create it, size it, or connect to it. Your capsule declares the tables and the platform applies the migration when a version finalizes. +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. :::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. @@ -120,7 +120,7 @@ The write is atomic and mode `0600`. If the export fails part-way, an existing d ## Migrations -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` is for applying the live plan again, or for previewing what the source tree in front of you would change on top of it. +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. ```bash sf db migrate diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index 7badb915..e9a3b91f 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -61,7 +61,7 @@ So a brand new variable with no flag is write-only. Pass `--no-secret` when you ### Build time, `{{ vars.NAME }}` -List a file under `templates` in `sf.jsonc` and every `{{ vars.NAME }}` token in it is substituted when the version finalizes. +List a file under `templates` in `sf.jsonc`. Every `{{ vars.NAME }}` token in it is substituted when the version finalizes. ```jsonc sf.jsonc { @@ -129,7 +129,7 @@ sf env pull .env.development --force sf env pull --stdout --format json ``` -`sf env pull` writes `.env.local` by default and refuses to overwrite an existing file without `--force`. The file is written `0600` through a temp file and a rename, and a pre-existing loose file gets tightened. +`sf env pull` writes `.env.local` by default and refuses to overwrite an existing file without `--force`. The file is written `0600` through a temp file and a rename. A pre-existing loose file gets tightened too. Only non-secret values are pullable. Write-only names come back in `omittedWriteOnly` with a warning instead of a blank line. Team-scope values are written as `shared.`. @@ -138,7 +138,7 @@ sf env import .env.production sf env import .env --from vercel --no-secret ``` -`sf env import` reads a dotenv file and sends one write per entry, in file order. It strips a BOM and a leading `export `, skips blank and `#` lines, requires quotes to close, and strips an inline `#` outside quotes. A later duplicate overwrites an earlier one. A missing `=` or an empty value is an error naming the line. `--from` accepts `dotenv`, `vercel`, `netlify`, and `cloudflare`, which all export dotenv files. +`sf env import` reads a dotenv file and sends one write per entry, in file order. It strips a BOM and a leading `export `, and skips blank and `#` lines. It also requires quotes to close and strips an inline `#` outside quotes. A later duplicate overwrites an earlier one. A missing `=` or an empty value is an error naming the line. `--from` accepts `dotenv`, `vercel`, `netlify`, and `cloudflare`, which all export dotenv files. ```bash sf env export-template diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index c7ec4d54..ce269161 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -49,7 +49,7 @@ A module's filename is its route and the HTTP methods it exports are the methods | `functions/haiku/[topic].ts` | `/haiku/:topic` | | `functions/posts/[...rest].ts` | `/posts` and everything under it | -Route modules can be `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, or `.cjs`. A name starting with `_` or `.` is a colocated helper and is never routed, and neither are `.d.ts` files or anything matching `*.test.*` / `*.spec.*`. A catch-all is terminal or nothing: `[...rest]` in the middle of a path is skipped. +Route modules can be `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, or `.cjs`. A name starting with `_` or `.` is a colocated helper and is never routed. Neither are `.d.ts` files or anything matching `*.test.*` / `*.spec.*`. A catch-all is terminal or nothing: `[...rest]` in the middle of a path is skipped. A path that exists at other methods answers 405 with an `Allow` header. A path in no file's table is a plain 404 and the worker never wakes. diff --git a/content/(dynamic)/storage.mdx b/content/(dynamic)/storage.mdx index c391e800..1714246d 100644 --- a/content/(dynamic)/storage.mdx +++ b/content/(dynamic)/storage.mdx @@ -76,13 +76,13 @@ sf storage rm 0f3a2c19b74e4d0f8c6e5a2b1d9f4c73 `sf storage` and `sf storage ls` do the same thing. When there is another page, the output ends with a `Next cursor:` line to pass back through `--cursor`. -`sf storage rm` confirms first, and 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. +`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. ## Dashboard -Open **Storage** for the space. It has a **Choose files to upload** input and an **Upload files** button, and it prints the per-file limit inline. Deleting asks first, because it removes the object for every visitor. +Open **Storage** for the space. It has a **Choose files to upload** input and an **Upload files** button. It also prints the per-file limit inline. Deleting asks first, because it removes the object for every visitor. ## Limits diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index c2246781..5ee45e14 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -21,7 +21,7 @@ Everything after `sf`'s own flags is handed to `wp` unchanged. `--format=csv` ab ### The `--` rule -`sf` stops claiming flags at the first token that is not one of its own, and `--` ends the argument outright. So a `wp` flag can never collide with an `sf` flag by accident, and a `wp` command that genuinely needs `--path` or `--space` is still reachable by putting it after `--` or after any positional. +`sf` stops claiming flags at the first token that is not one of its own, and `--` ends the argument outright. So a `wp` flag can never collide with an `sf` flag by accident. A `wp` command that genuinely needs `--path` or `--space` is still reachable — just put it after `--` or after any positional. ```bash sf wp -- --version @@ -40,7 +40,7 @@ Without the separator, `--version` would be read as `sf`'s own. The local path runs a pinned `wp-cli.phar` as a child process with inherited stdio, so it is a true passthrough. Interactive commands, pagers, and colored output all work. -The remote path cannot be. The host runs WP-CLI as a job against the site and reports whether it exited zero, with no channel carrying what the command printed. Rather than fake a stream, `sf wp` says so: +The remote path cannot be. The host runs WP-CLI as a job against the site and reports whether it exited zero. No channel carries what the command printed. Rather than fake a stream, `sf wp` says so: ```text ✓ wp option get blogname on docs in 1840ms @@ -68,7 +68,7 @@ sf wp --local --path ./wordpress core version | Characters per argument | 4,096 | | Run time before the API gives up | 360 seconds, answering `wp_cli_timeout` | -A remote run needs write access to the Space, because a `wp` command has unrestricted authority over the site's data. Every run is written to the Space's activity trail, successful or not, and a run whose outcome could not be determined is recorded as such. A Space that has not been provisioned yet answers `runtime_not_provisioned`. +A remote run needs write access to the Space, because a `wp` command has unrestricted authority over the site's data. Every run is written to the Space's activity trail, successful or not. A run whose outcome could not be determined is recorded as such. A Space that has not been provisioned yet answers `runtime_not_provisioned`. The API route is `runSpaceWpCliCommand`. It takes an already-split argument vector, `["option", "get", "blogname"]`, relayed verbatim. No shell parses it, so nothing re-splits, re-globs, or re-quotes what you send. @@ -138,7 +138,7 @@ curl -X POST \ -d '{ "perPage": 20 }' ``` -The plain `Authorization` header belongs to your application and passes through untouched, which is why the platform's bearer takes a header of its own. +The plain `Authorization` header belongs to your application and passes through untouched. That's why the platform's bearer takes a header of its own. 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. diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 2c5272a0..79a01fdb 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -9,7 +9,7 @@ After this page you can tell whether your project needs Zero and declare it in ` ## 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, and `sf publish` compiles it into a **capsule** the platform installs on the space. +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 on for every account. There is nothing to enable. @@ -160,13 +160,13 @@ Return one of the response helpers: `json(value)`, `text(value)`, `empty()` for 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. -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 and every browser holding a matching subscription re-runs its query. That is the whole realtime story. Do not poll. +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. 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. ## What a capsule is, and what compiles -A capsule is the value `capsule({...})` returns. `sf publish` runs esbuild over your tree and 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. +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. 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. @@ -259,7 +259,7 @@ Call them from the client with `useAction("invite")`. `scope: "/"` covers the wh 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. -Class names must be static strings. The compiler finds them by scanning source text, so `bg-${tone}-500` produces no CSS at all and the capsule ships unstyled. Branch to whole literals instead. +Class names must be static strings. The compiler finds them by scanning source text, so `bg-${tone}-500` produces no CSS at all. The capsule ships unstyled. Branch to whole literals instead. 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. diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index 62b934b6..df5b49e0 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -48,7 +48,7 @@ It is recoverable after that. The key stays valid for another **7 days**, during Opening the claim page when less than an hour remains moves the deadline to one hour from that load, up to 6 extra hours in total. Repeated publishes cannot push the deadline past 30 days from creation. -To get a warning before the clock runs out, leave an address. Set `email` on the publish body, or call [`createSpaceClaimReminder`](/api/reference/spaces/createspaceclaimreminder). The reminder goes out about 3 hours before expiry. The address is stored only until the Space is claimed or expires, then deleted, and it is never written to logs. +To get a warning before the clock runs out, leave an address. Set `email` on the publish body, or call [`createSpaceClaimReminder`](/api/reference/spaces/createspaceclaimreminder). The reminder goes out about 3 hours before expiry. The address is stored only until the Space is claimed or expires, then deleted. It is never written to logs. ## Caps diff --git a/content/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index 4ab7dae9..83fdcfbc 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -9,14 +9,14 @@ After this page you know whether to publish your own build output or let Spacefa ## Two paths -**Publish the output.** You run the build, Spacefast takes the directory. This is the fast path and the recommended default. Your machine or your CI already has the toolchain warm, failures show up where you can debug them, and the publish is a file upload. +**Publish the output.** You run the build, Spacefast takes the directory. This is the fast path and the recommended default. Your machine or your CI already has the toolchain warm, and failures show up where you can debug them. The publish itself is just a file upload. ```bash npm run build sf publish ./dist ``` -**Let Spacefast build.** You hand over source, Spacefast installs dependencies, runs the build in a sandbox, and publishes the output as a version. Use it when the source is the thing you have, especially on a push to a connected repository. +**Let Spacefast build.** You hand over source. Spacefast installs dependencies, runs the build in a sandbox, and publishes the output as a version. Use it when the source is the thing you have, especially on a push to a connected repository. ```bash sf publish --remote @@ -207,7 +207,7 @@ export default defineConfig({ }); ``` -It merges inline `redirects` and `headers` options with the `_redirects` and `_headers` files in your project root, public directory, and any `publishDir` you name, compiles them, applies them in `astro dev` so local behavior matches production, and writes the merged files into the build output. A routing error fails the build, because `failOnRoutingError` defaults to `true`. Needs Astro 6 or newer. +It merges inline `redirects` and `headers` options with the `_redirects` and `_headers` files in your project root, public directory, and any `publishDir` you name. It compiles the result and applies it in `astro dev`, so local behavior matches production, then writes the merged files into the build output. A routing error fails the build, because `failOnRoutingError` defaults to `true`. Needs Astro 6 or newer. For server-rendered Astro there is a second integration at `@spacefast/astro/adapter`, `spacefastAstroAdapter()`, which records route facts and emits the server entry. Sharp runs at build time only. diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 3ecc9dba..16af2dbe 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -158,7 +158,7 @@ sf git github installations --space docs sf git github repos --space docs ``` -`sf git update` takes `--clear-credential` to remove a stored token, and passing it together with `--credential` is a validation error. `sf git disconnect` confirms first. Disconnecting stops pushes from building the Space. What is already published stays live, your domains do not move, and the repository itself is untouched. +`sf git update` takes `--clear-credential` to remove a stored token, and passing it together with `--credential` is a validation error. `sf git disconnect` confirms first. Disconnecting stops pushes from building the Space. What is already published stays live and your domains do not move. The repository itself is untouched. ## Two connection-type vocabularies diff --git a/content/(publish)/publish.mdx b/content/(publish)/publish.mdx index fbac4e29..587d8b5c 100644 --- a/content/(publish)/publish.mdx +++ b/content/(publish)/publish.mdx @@ -161,7 +161,7 @@ The parts you actually read: | `diagnostics` | Warnings, dropped paths, and `noop_publish` | | `operation` | The handle to poll when finalize kept running | -`next.action` is the field to branch on. `done` means there is nothing left to do, and the `hint` says why, for example `Live. Nothing left to do.` An `unpromoted` outcome with `done` means the version is ready and you need to promote it yourself, and the promote URL is right there in `links.promote`. +`next.action` is the field to branch on. `done` means there is nothing left to do, and the `hint` says why, for example `Live. Nothing left to do.` An `unpromoted` outcome with `done` means the version is ready and you need to promote it yourself. The promote URL is right there in `links.promote`. Anonymous publishes add a `claim` block instead of the `access` link. See [Publish without an account](/anonymous-and-claim). diff --git a/content/(publish)/recipes/html.mdx b/content/(publish)/recipes/html.mdx index c61bde44..7880bd0a 100644 --- a/content/(publish)/recipes/html.mdx +++ b/content/(publish)/recipes/html.mdx @@ -45,7 +45,7 @@ Published: https://quiet-harbor.view.fast ## The gotcha -`sf publish` decides between "upload this" and "build this" from the file names in the folder. A `package.json`, a lockfile, or a framework config file sitting next to your `index.html` flips it to the build path, and then it installs dependencies and looks for a build command you may not have. +`sf publish` decides between "upload this" and "build this" from the file names in the folder. A `package.json`, a lockfile, or a framework config file sitting next to your `index.html` flips it to the build path. Then it installs dependencies and looks for a build command you may not have. Force the upload path: diff --git a/content/(publish)/recipes/next.mdx b/content/(publish)/recipes/next.mdx index 9c089cb5..345492b0 100644 --- a/content/(publish)/recipes/next.mdx +++ b/content/(publish)/recipes/next.mdx @@ -51,4 +51,4 @@ npx next build sf publish ./out --prebuilt ``` -One related failure mode. If a custom build command runs `next build` in a way that loses the environment the CLI set, the adapter never runs, and the publish stops with `build_failed` saying `.spacefast/next-build.json` was not written. Let the CLI invoke the build, or pass its environment through your wrapper. +One related failure mode. If a custom build command runs `next build` in a way that loses the environment the CLI set, the adapter never runs. The publish then stops with `build_failed`, saying `.spacefast/next-build.json` was not written. Let the CLI invoke the build, or pass its environment through your wrapper. diff --git a/content/(publish)/wordpress-data-sources.mdx b/content/(publish)/wordpress-data-sources.mdx index 50f34b01..af721b8a 100644 --- a/content/(publish)/wordpress-data-sources.mdx +++ b/content/(publish)/wordpress-data-sources.mdx @@ -5,7 +5,7 @@ description: Point a Space's repository build at a public WordPress site, read i After this page you can point a repository build at a public WordPress site and read its content while the build runs. -A data source is a public WordPress site a build reads content from. Nothing is copied into the Space and nothing is written back. The platform hands the build a URL and a source map, your build code fetches over the WordPress REST API, and the output publishes like any other static build. +A data source is a public WordPress site a build reads content from. Nothing is copied into the Space and nothing is written back. The platform hands the build a URL and a source map. Your build code fetches over the WordPress REST API, and the output publishes like any other static build. ## What you need first diff --git a/content/(reference)/config-file.mdx b/content/(reference)/config-file.mdx index ef4585e6..cbc05dc4 100644 --- a/content/(reference)/config-file.mdx +++ b/content/(reference)/config-file.mdx @@ -272,6 +272,6 @@ Under `version: 1` the flat spellings are rejected. Four of them get `config_unk | `cleanUrls` | `serve.cleanUrls` | | `meta` | `metadata` | -The v1 root accepts `$schema`, `version`, `build`, `serve`, `redirects`, `rewrites`, `headers`, `metadata`, `substitute`, `access`, and `crons`. Anything else is an error: `listing`, `superpowers`, `templates`, `theme`, `placement`, and `markdownNegotiation` report `config_key_removed`, and `space` reports `config_identity_moved`. +The v1 root accepts `$schema`, `version`, `build`, `serve`, `redirects`, `rewrites`, `headers`, `metadata`, `substitute`, `access`, and `crons`. Anything else is an error. `listing`, `superpowers`, `templates`, `theme`, `placement`, and `markdownNegotiation` report `config_key_removed`; `space` reports `config_identity_moved`. The flat shape above is what the finalizer reads and what the CLI writes. Use `version: 1` only if you want the stricter contract. diff --git a/content/(reference)/limits.mdx b/content/(reference)/limits.mdx index dbfac93f..19d8d893 100644 --- a/content/(reference)/limits.mdx +++ b/content/(reference)/limits.mdx @@ -87,7 +87,7 @@ Until a space is claimed, the runtime serves a restricted set of content types: | Activity events | Plan window, 48 hours on Free | Events are deleted | | HTTP access logs | Plan window, 48 hours on Free | Reads are clamped to the window | -The two anonymous rows work together. A space that stops serving at its deadline, which claim-page loads can push out by up to 6 hours, is still recoverable through the claim link for another 7 days. See [Publish without an account](/anonymous-and-claim). +The two anonymous rows work together. A space that stops serving at its deadline is still recoverable through the claim link for another 7 days. Claim-page loads can push that deadline out by up to 6 hours. See [Publish without an account](/anonymous-and-claim). ## What a paused or removed space serves diff --git a/content/(serve)/access.mdx b/content/(serve)/access.mdx index 7a54eb53..5611bc59 100644 --- a/content/(serve)/access.mdx +++ b/content/(serve)/access.mdx @@ -201,13 +201,13 @@ Approving invites the requester to the path they asked about. **On a public Space**, the page loads. No access code runs at all. -**With a share link**, the link is the page. The token is redeemed before anything is read from disk, so the first request returns the document. The response is never stored in any cache, and it carries `Referrer-Policy: no-referrer` so the secret does not leak through the referrer header on outbound clicks. +**With a share link**, the link is the page. The token is redeemed before anything is read from disk, so the first request returns the document. The response is never stored in any cache. It also carries `Referrer-Policy: no-referrer`, so the secret does not leak through the referrer header on outbound clicks. **On a protected Space with no proof**, the request answers **403** with the Space's access page and `Cache-Control: private, no-store`. The page offers whichever lanes you configured: a password box, an email code, a request-access form, or a sign-in with a Spacefast account. **Signing in** sends the visitor to Spacefast, then back. The path they originally asked for is carried through the round trip, so they land on the page they wanted rather than the homepage. -**Submitting a password** posts it from the same origin, checks it against the platform rather than anything on the box, and on success sets the visitor's session cookie and sends a **303** back to where they were. Passwords are capped at 1024 bytes. Failures come back as rate limited, invalid password, or exchange unavailable. +**Submitting a password** posts it from the same origin and checks it against the platform, not anything on the box. On success, it sets the visitor's session cookie and sends a **303** back to where they were. Passwords are capped at 1024 bytes. Failures come back as rate limited, invalid password, or exchange unavailable. A visitor session is idle-expiring after 7 days and absolutely expiring after 30 days. diff --git a/content/(serve)/caching.mdx b/content/(serve)/caching.mdx index b6c557cc..01dec588 100644 --- a/content/(serve)/caching.mdx +++ b/content/(serve)/caching.mdx @@ -66,7 +66,7 @@ The non-GET rule is absolute. The shared cache in front of a Space keys on host, The edge keys a stored response on **host, path, and query**. It does not read `Vary`. -That single fact explains a behavior that surprises people. A routing rule with a `Country=`, `Language=`, `Cookie=` or `Agent=` condition would otherwise let one visitor's response be handed to the next, so any URL a conditional rule touches is forced to `no-store` for everyone, whether the condition matched or not. +That single fact explains a behavior that surprises people. A routing rule with a `Country=`, `Language=`, `Cookie=` or `Agent=` condition would otherwise let one visitor's response reach the next visitor. So any URL a conditional rule touches is forced to `no-store` for everyone, whether the condition matched or not. If a page needs to differ by country, language or client, give each variant its own URL rather than one URL with a condition on it. That keeps the whole set shared-cacheable. diff --git a/content/(serve)/customization.mdx b/content/(serve)/customization.mdx index d9e9a3c3..173b03e7 100644 --- a/content/(serve)/customization.mdx +++ b/content/(serve)/customization.mdx @@ -9,7 +9,7 @@ Open the Space and go to **Customization**. Four tabs, in order: **Theme**, **Se ## What lives on the Space, not the version -Everything on this page is stored on the Space rather than on a version, so it survives a publish and a rollback alike. It also wins over the published `sf.jsonc`: the Space's `theme` replaces the file's rather than merging with it, so a project that sets `theme` in its config and then edits Theme here keeps seeing the dashboard's answer until you clear it. The same holds for `meta`. +Everything on this page is stored on the Space rather than on a version, so it survives a publish and a rollback alike. It also wins over the published `sf.jsonc`: the Space's `theme` replaces the file's rather than merging with it. So a project that sets `theme` in its config, then edits Theme here, keeps seeing the dashboard's answer until you clear it. The same holds for `meta`. Theme and share metadata apply without republishing. Save, and the next request gets them. **Custom JS** is the exception, and its own field note says so: a script change takes effect on the next publish. @@ -34,7 +34,7 @@ Colors accept hex, `rgb()`, `rgba()`, `hsl()`, `hsla()`, and CSS color names, up Leave **Background** blank and the pages follow each visitor's device, light or dark. The field's default note says exactly that. -Set it, and it stops following. Spacefast reads the color you gave, decides whether it is light or dark, and serves that one scheme to everyone. The field flips to **Pinned.** with a line saying the color reads as dark or light so every visitor gets that scheme, plus **Clear to follow visitors again**. The preview's Auto/Light/Dark switcher is replaced by a locked **Pinned · Dark** or **Pinned · Light** chip, because there is no longer anything to simulate. +Set it, and it stops following. Spacefast reads the color you gave, decides whether it is light or dark, and serves that one scheme to everyone. The field flips to **Pinned.**, with a line saying the color reads as dark or light so every visitor gets that scheme. It also shows **Clear to follow visitors again**. The preview's Auto/Light/Dark switcher is replaced by a locked **Pinned · Dark** or **Pinned · Light** chip, because there is no longer anything to simulate. ### Check the branding entitlement diff --git a/content/(serve)/routing.mdx b/content/(serve)/routing.mdx index 618dc551..86e5eec1 100644 --- a/content/(serve)/routing.mdx +++ b/content/(serve)/routing.mdx @@ -83,11 +83,11 @@ Header names must match `^[A-Za-z0-9-]+$`. Some names are owned by the platform | `cdn-cache-control`, `cloudflare-cdn-cache-control`, `netlify-cdn-cache-control`, `surrogate-control` | `header_cdn_cache_unsupported` | | `Basic-Auth` | `header_basic_auth_unsupported` | -`Basic-Auth` is rejected before anything else, and the offending line is redacted out of the diagnostics and stripped from the compiled bytes so the credential never reaches a log. For real access control, see [Access and sharing](/access). +`Basic-Auth` is rejected before anything else. The offending line is redacted from the diagnostics and stripped from the compiled bytes, so the credential never reaches a log. For real access control, see [Access and sharing](/access). `Cache-Control` is allowed and warns with `header_cache_control_platform_managed`. It applies to browser responses; shared caching is managed for you. See [Caching](/caching). -`_headers` matchers cannot include a port, and they match the raw pathname, so they are sensitive to a trailing slash where `_redirects` is not. Header rules also apply to the URL the visitor asked for, not to the path a rewrite landed on. +`_headers` matchers cannot include a port. They match the raw pathname, so they are sensitive to a trailing slash where `_redirects` is not. Header rules also apply to the URL the visitor asked for, not to the path a rewrite landed on. ## Rules in `sf.jsonc` @@ -187,7 +187,7 @@ The edge cache key is host, path, and query. It does not read `Vary`. Any condit ### Rules versus real files -A `rewrite` or a `404` rule skips itself when the request path resolves to a committed file, unless you set `force`, and evaluation then continues to later rules. A `redirect` and a `proxy` never consult the file table at all. +A `rewrite` or a `404` rule skips itself when the request path resolves to a committed file, unless you set `force`. Evaluation then continues to later rules. A `redirect` and a `proxy` never consult the file table at all. ### What happens to the query string diff --git a/content/(serve)/site-pages.mdx b/content/(serve)/site-pages.mdx index 07cc73f2..1feeed40 100644 --- a/content/(serve)/site-pages.mdx +++ b/content/(serve)/site-pages.mdx @@ -100,7 +100,7 @@ A layout resolves upward from the request's directory, so `docs/_layout.html` wr ## `_pages/.html` Plus and up -A file at `_pages/.html` replaces that page's document entirely. Nothing of the built-in template survives, the layout is not applied, and the theme controls stop repainting it. This full takeover requires Plus or Enterprise. +A file at `_pages/.html` replaces that page's document entirely. Nothing of the built-in template survives. The layout does not apply, and the theme controls stop repainting the page. This full takeover requires Plus or Enterprise. Each file has to be a complete HTML document beginning with ``, and each is capped at 2 MiB. Like layouts, they resolve upward from the request's directory. @@ -122,7 +122,7 @@ The runtime fills a small set of elements. Which ones are legal depends on the p An element used on the wrong page is `page_element_not_allowed`, and an `sf-` name that is not on this list is `unknown_page_element`. -Two of those are load-bearing rather than decorative. The `access` page **must** render ``, otherwise the gate ships with no way in and publish fails with `access_lanes_missing`. And if that page declares a CSP with `form-action`, it has to include `'self'`, otherwise its own runtime forms are blocked and publish fails with `access_csp_form_action_missing_self`. +Two of those are load-bearing rather than decorative. The `access` page **must** render ``. Otherwise the gate ships with no way in, and publish fails with `access_lanes_missing`. And if that page declares a CSP with `form-action`, it has to include `'self'`. Otherwise its own runtime forms are blocked, and publish fails with `access_csp_form_action_missing_self`. An `index` page that never renders `` publishes with a warning rather than an error, since a listing with no list is a choice you may have meant. @@ -178,7 +178,7 @@ _pages/*.html takeovers require Plus or Enterprise. Remove the templates or upgr sf pages pull ``` - With no argument it writes `_layout.html` plus all five `_pages/.html`. Pass `layout` or one page id for just that. The command is fully local and never calls the API. An existing file makes the write fail, and there is no force flag, so move or delete it first. + With no argument it writes `_layout.html` plus all five `_pages/.html`. Pass `layout` or one page id for just that. The command is fully local and never calls the API. An existing file makes the write fail, and there is no force flag. Move or delete it first. @@ -225,4 +225,4 @@ Nothing is served leniently. Page compilation runs at finalize, and the first di The dashboard's [Customization](/customization) tab writes the same theme tokens onto the Space rather than into the version, which is why its values win over `sf.jsonc` and apply without a republish. -A page you ship as `_pages/.html` opts out of that entirely. Customization's preview badges it **Code override.** and says theme controls do not repaint it, because they do not: your document is served as written, with only the elements above filled in. +A page you ship as `_pages/.html` opts out of that entirely. Customization's preview badges it **Code override.** and says theme controls do not repaint it. That's accurate: your document is served as written, with only the elements above filled in. diff --git a/content/(serve)/stats.mdx b/content/(serve)/stats.mdx index 31c8bf7b..651e06f6 100644 --- a/content/(serve)/stats.mdx +++ b/content/(serve)/stats.mdx @@ -31,7 +31,7 @@ Buckets are labelled in UTC, not your timezone, because that is the boundary the A **request** is any HTTP request the Space answered. A **view** is a successful request from something that is not a crawler: status under 400, assets included. So `views` is always lower than `requests`, and the gap is redirects, 404s, and bots. -**Unique visitors** come from a separate daily measurement. It ignores the crawler and status filters, and it merges every hostname mapped to the Space into one count. Daily is the finest resolution available, and days are never summed into a window total, which is why the page lists them one by one instead of showing one number. +**Unique visitors** come from a separate daily measurement. It ignores the crawler and status filters, and it merges every hostname mapped to the Space into one count. Daily is the finest resolution available, and days are never summed into a window total. That's why the page lists them one by one instead of showing a single number. **Top paths** and top hosts are capped at 10 entries each. diff --git a/content/(serve)/urls.mdx b/content/(serve)/urls.mdx index cbeeafa3..bfdbb75b 100644 --- a/content/(serve)/urls.mdx +++ b/content/(serve)/urls.mdx @@ -28,7 +28,7 @@ Every version that reaches ready gets a permanent hostname of its own: https://v{number}--{label}.view.fast/ ``` -Version 7 of `my-site` is `https://v7--my-site.view.fast/`. The URL is computed once when the version turns ready and stored on the version row, so it survives a slug rename and keeps serving that exact build no matter what is live now. +Version 7 of `my-site` is `https://v7--my-site.view.fast/`. The URL is computed once when the version turns ready, and stored on the version row. So it survives a slug rename, and keeps serving that exact build no matter what is live now. The API and the CLI report it as `immutableUrl`, with an `immutableUrlStatus` of `pending` until the hostname is routable. diff --git a/content/agents/claude-code.mdx b/content/agents/claude-code.mdx index 3be930ed..6a3a1e74 100644 --- a/content/agents/claude-code.mdx +++ b/content/agents/claude-code.mdx @@ -14,7 +14,7 @@ claude plugin install spacefast@spacefast The plugin does not assume the `sf` CLI is installed. You sign in the first time the hosted server is used, through your client's OAuth flow. -Two other lanes exist. `npx -y plugins add spacefast/plugins -t claude-code -y` installs the same plugin, and `sf setup agent --agent claude-code` writes MCP config into `~/.claude.json` and installs the skill into `~/.claude/skills/spacefast/` instead. See [sf setup agent](/agents/sf-setup). +Two other lanes exist. `npx -y plugins add spacefast/plugins -t claude-code -y` installs the same plugin. `sf setup agent --agent claude-code` writes MCP config into `~/.claude.json` and installs the skill into `~/.claude/skills/spacefast/` instead. See [sf setup agent](/agents/sf-setup). ## What the plugin adds @@ -38,13 +38,13 @@ Two entries, hosted and local. } ``` -`spacefast` is the hosted server. `spacefast-local` runs the same server on your machine over stdio, which is what lets `publish` read a folder instead of taking inline files. The version in `args` is stamped at release; the shipped file pins whatever the release built. +`spacefast` is the hosted server. `spacefast-local` runs the same server on your machine over stdio, so `publish` can read a folder instead of taking inline files. The version in `args` is stamped at release; the shipped file pins whatever the release built. The `"type": "http"` field is load-bearing. Claude Code rejects a bare `{ "url": ... }` entry. ### The Spacefast skill -`skills/spacefast/` holds `SKILL.md`, `references.md`, and seven bash scripts. The skill routes each request to the right tool, sets the publish then poll then report loop, and holds the rules about never printing claim credentials. The scripts are a curl fallback for when MCP is unavailable. See [skills](/agents/skills). +`skills/spacefast/` holds `SKILL.md`, `references.md`, and seven bash scripts. The skill routes each request to the right tool and sets the publish-poll-report loop. It also holds the rule against printing claim credentials. The scripts are a curl fallback for when MCP is unavailable. See [skills](/agents/skills). ### One SessionStart hook diff --git a/content/agents/codex.mdx b/content/agents/codex.mdx index 599e61a2..7808cae4 100644 --- a/content/agents/codex.mdx +++ b/content/agents/codex.mdx @@ -12,7 +12,7 @@ codex plugin marketplace add spacefast/plugins codex plugin add spacefast@spacefast ``` -Two other lanes exist. `npx -y plugins add spacefast/plugins -t codex -y` installs the same plugin, and `sf setup agent --agent codex` writes the MCP table into `$CODEX_HOME/config.toml`, default `~/.codex/config.toml`, and installs the skill into `$CODEX_HOME/skills/spacefast/`. +Two other lanes exist. `npx -y plugins add spacefast/plugins -t codex -y` installs the same plugin. `sf setup agent --agent codex` writes the MCP table into `$CODEX_HOME/config.toml` (default `~/.codex/config.toml`) and installs the skill into `$CODEX_HOME/skills/spacefast/`. ## What the plugin adds diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index de20ec74..f3ba9fb4 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -25,9 +25,9 @@ There is a third lane, `sf mcp proxy`, which speaks stdio to your editor and HTT ### Hosted -The hosted server is an OAuth 2.0 protected resource. It is never anonymous. A request with no token gets a `401` and a `WWW-Authenticate` challenge that points at the resource metadata document, and your client walks the rest from there. Bearer and DPoP are both accepted; a DPoP-shaped request gets a DPoP challenge back. +The hosted server is an OAuth 2.0 protected resource. It is never anonymous. A request with no token gets a `401` and a `WWW-Authenticate` challenge pointing at the resource metadata document. Your client walks the rest from there. Bearer and DPoP are both accepted; a DPoP-shaped request gets a DPoP challenge back. -The resource identifier is the MCP origin itself. A platform OAuth token that was not bound to the MCP resource is rejected at this endpoint even though it is a valid Spacefast token. +The resource identifier is the MCP origin itself. A platform OAuth token not bound to the MCP resource is rejected here, even though it's a valid Spacefast token. By default a client asks for these scopes. @@ -54,7 +54,7 @@ Each tool declares the scopes it needs, and clients can read them from `tools/li A scope shortfall applies to the requested operation. A credential that can read analytics may still lack access to domains. -Your client never receives a Spacefast API token. Each authenticated request mints an internal delegation token that lives five minutes and carries the source credential's policy verbatim, so a leaked MCP session cannot be replayed against the wider API. +Your client never receives a Spacefast API token. Each authenticated request mints an internal delegation token that lives five minutes and carries the source credential's policy verbatim. That means a leaked MCP session cannot be replayed against the wider API. Hosted requests are rate limited to 600 per minute per credential and 300 per minute per IP. diff --git a/content/agents/other-clients.mdx b/content/agents/other-clients.mdx index d3c4fd91..d12db7d1 100644 --- a/content/agents/other-clients.mdx +++ b/content/agents/other-clients.mdx @@ -22,7 +22,7 @@ After this page you can wire Spacefast into any MCP client that has no first-par } ``` - The `"type"` field is required. Claude Code rejects a bare `{ "url": ... }` entry outright and VS Code reads one as a stdio command, so the short form is broken rather than smaller. + The `"type"` field is required. Claude Code rejects a bare `{ "url": ... }` entry outright, and VS Code reads one as a stdio command. The short form is broken, not smaller. The server runs on your machine and can read your working directory, so `publish` accepts a path. diff --git a/content/agents/permissions.mdx b/content/agents/permissions.mdx index 7d4e9bdd..8a867942 100644 --- a/content/agents/permissions.mdx +++ b/content/agents/permissions.mdx @@ -95,4 +95,4 @@ A handoff link lives 15 minutes and can be redeemed once. A team can have 10 pen - Revoking one connection kills the whole thing: its API keys, its OAuth tokens, its handoff links, and any temporary elevation. - **Stop all agent access** is the account-wide emergency stop. It revokes everything acting as you and cancels handoff links that were never redeemed. Team-owned automation is not yours alone, so it survives. -Team owners and admins have the same control from the team's **Agents** page, under **Who can act here**, where **Remove from team** takes an agent's reach into that one team away and leaves the rest alone. +Team owners and admins have the same control from the team's **Agents** page, under **Who can act here**. **Remove from team** takes an agent's reach into that one team away and leaves the rest alone. diff --git a/content/api/authentication.mdx b/content/api/authentication.mdx index 2f56a632..4b5ddcbc 100644 --- a/content/api/authentication.mdx +++ b/content/api/authentication.mdx @@ -99,7 +99,7 @@ An anonymous `POST /v1/publish` (no credential at all) creates an unclaimed Spac A Space key reaches only its own Space, and only the operations an unclaimed Space must still support: read the Space, open a new version, refresh uploads, finalize, promote, list activity, mint share links, restore after a delete. Operations declaring `bearerAuth` alone refuse it. -Unclaimed Spaces expire. The default window is 33 hours and 20 minutes from creation or the latest publish that changes files, capped at 30 days from creation. Reading the Space with its key, which is what the claim page does, moves the deadline to one hour out when less than an hour remains, up to 6 hours of extension in total. Claim it with `POST /v1/claim` to remove the deadline, or exchange the key for an API key with `POST /v1/claim/exchange`. +Unclaimed Spaces expire. The default window is 33 hours and 20 minutes from creation or the latest publish that changes files, capped at 30 days from creation. Reading the Space with its key moves the deadline to one hour out when less than an hour remains, up to 6 hours of extension in total. That's what the claim page does. Claim it with `POST /v1/claim` to remove the deadline, or exchange the key for an API key with `POST /v1/claim/exchange`. :::warning[Treat a Space key as a secret] It is a management capability for an unclaimed Space, not a share link. Anyone holding it can publish to that Space and claim it. @@ -147,7 +147,7 @@ A token that authenticates but lacks a scope set the operation accepts fails wit ## Device login for CLIs and agents -A headless client starts at `POST /v1/auth/device`, shows the user the verification code, then polls `POST /v1/auth/device/poll` until the user approves. Approval and denial run through `POST /v1/auth/device/approve` and `/deny`, which need a signed-in browser session. While the user has not decided yet, the poll answers `authorization_pending`. That is a `wait`, not a failure. +A headless client starts at `POST /v1/auth/device` and shows the user the verification code. It then polls `POST /v1/auth/device/poll` until the user approves. Approval and denial run through `POST /v1/auth/device/approve` and `/deny`, which need a signed-in browser session. While the user has not decided yet, the poll answers `authorization_pending`. That is a `wait`, not a failure. The code lifetime, the poll cadence, and what the approved credential is good for are on [Sign in and security](/authentication#log-the-cli-in). diff --git a/content/api/errors.mdx b/content/api/errors.mdx index 26c8df73..aa4fd957 100644 --- a/content/api/errors.mdx +++ b/content/api/errors.mdx @@ -42,7 +42,7 @@ The object is closed. No other members appear. ## Branch on `code`, not on `status` or `detail` -`code` is a typed union in the source, so the server cannot emit one outside the table below. `detail` is prose and may be reworded. `status` groups many codes together, and `title` and `type` are both mechanical functions of `code`, so they add nothing a branch can use. +`code` is a typed union in the source, so the server cannot emit one outside the table below. `detail` is prose and may be reworded. `status` groups many codes together. `title` and `type` are both mechanical functions of `code`, so they add nothing a branch can use. ## `pointer` @@ -79,7 +79,7 @@ The API uses this set, and no others: ## `recovery` and `retryable` -Every code carries two classifications in the shipped contract. They are not response fields. The `sf` CLI reads them to tell a caller what to do, and the tables on this page publish the same values so your own client can do the same. +Every code carries two classifications in the shipped contract. They are not response fields. The `sf` CLI reads them to tell a caller what to do. The tables on this page publish the same values, so your own client can do the same. | `recovery` | What the caller does | | --- | --- | diff --git a/content/api/idempotency.mdx b/content/api/idempotency.mdx index 4e84987a..12d117a3 100644 --- a/content/api/idempotency.mdx +++ b/content/api/idempotency.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -A dropped connection on a publish should not create two versions. After this page you can retry any `POST` and know exactly which of the two possible answers you will get: the original result replayed, or a conflict telling you the first attempt is still running. +A dropped connection on a publish should not create two versions. After this page you can retry any `POST` and get one of two answers: the original result replayed, or a conflict saying the first attempt is still running. ## Send a key @@ -90,7 +90,7 @@ Exactly 64 hexadecimal characters, stable across every retry of that attempt, an } ``` -The header is a secret, not an identifier. An anonymous publish receipt contains one-time claim and upload credentials, and the principal is what stops another anonymous caller from replaying your receipt out of a shared scope. Generate 32 random bytes per attempt and hex-encode them. +The header is a secret, not an identifier. An anonymous publish receipt contains one-time claim and upload credentials. The principal is what stops another anonymous caller from replaying your receipt out of a shared scope. Generate 32 random bytes per attempt and hex-encode them. ## Where it matters most diff --git a/content/api/pagination.mdx b/content/api/pagination.mdx index 405b4a3f..2e6a771d 100644 --- a/content/api/pagination.mdx +++ b/content/api/pagination.mdx @@ -16,7 +16,7 @@ Two optional query parameters. | `limit` | integer, 1 to 100 | 20 | How many items to return. | | `cursor` | string, up to 4096 characters | none | The `nextCursor` from the previous page. | -Both are optional. The generated spec marks `limit` as required because the schema supplies its own default, but a request that omits it gets 20. +Both are optional. The generated spec marks `limit` as required because the schema supplies its own default. A request that omits it still gets 20. ## Response @@ -45,7 +45,7 @@ A cursor is a base64url-encoded keyset. Do not parse it, build one, or store it ## The loop -Stop when `nextCursor` is `null`. Never re-send a cursor you have already used. If the response hands back the same cursor you sent, that is a broken page rather than the end, and looping on it spins forever. Treat it as an error. +Stop when `nextCursor` is `null`. Never re-send a cursor you have already used. If the response hands back the same cursor you sent, that's a broken page, not the end. Looping on it spins forever. Treat it as an error. The same walk with curl and [the SDK](/api/sdk). diff --git a/content/api/rate-limits.mdx b/content/api/rate-limits.mdx index cecf6752..65fb279a 100644 --- a/content/api/rate-limits.mdx +++ b/content/api/rate-limits.mdx @@ -35,7 +35,7 @@ A `429` adds `Retry-After`, also in seconds. When a stricter hourly limiter is the one that rejected you, these headers describe that limiter's window rather than the per-minute one. So the number you see on a publish 429 is the publish window, and waiting that long is the right move. :::warning[The headers can be missing] -Rate limiting fails open. If the accounting backend is unavailable the request goes through and the response carries no `RateLimit-*` headers at all, deliberately, rather than reporting invented numbers. Client code must not require them to be present. +Rate limiting fails open. If the accounting backend is unavailable, the request goes through anyway. The response then deliberately carries no `RateLimit-*` headers, rather than reporting invented numbers. Client code must not require them to be present. ::: ## Hourly limits on publishes, builds, and Spaces @@ -51,7 +51,7 @@ These are abuse protection, not plan features. Each has two buckets, one for a s | Space creation | per credential | 20 | 60 | 200 | 600 | | Space creation | per account | 40 | 150 | 600 | 3,000 | -Go and Plus use either their old or new API plan code during the plan change. All windows are one hour. Space creation is metered separately from publishing because a Space is a site provision, and the general 600 per minute budget would allow 36,000 of them an hour. +Go and Plus use either their old or new API plan code during the plan change. All windows are one hour. Space creation is metered separately from publishing because a Space is a site provision. The general 600-per-minute budget alone would allow 36,000 of them an hour. A plan change takes effect for these limits within about five seconds. @@ -101,4 +101,4 @@ Slow down before you hit zero rather than absorbing 429s. On a bulk job, a small -The [TypeScript SDK](/api/sdk) implements this loop already. Opt in per call with `{ retry: { maxAttempts: 3 } }` and it honours `Retry-After`, backs off exponentially with jitter, and retries only idempotent requests. +The [TypeScript SDK](/api/sdk) implements this loop already. Opt in per call with `{ retry: { maxAttempts: 3 } }`. It then honours `Retry-After`, backs off exponentially with jitter, and retries only idempotent requests. diff --git a/content/api/webhooks.mdx b/content/api/webhooks.mdx index 1e0769b3..39ab8630 100644 --- a/content/api/webhooks.mdx +++ b/content/api/webhooks.mdx @@ -61,7 +61,7 @@ The response is the only place the signing secret ever appears: Store `secret` now. Every later read returns `secretPreview` only. -Endpoints must be public HTTPS. Non-HTTPS URLs are rejected, each connection is pinned to the DNS result that was validated, and redirects are refused. +Endpoints must be public HTTPS. Non-HTTPS URLs are rejected. Each connection is pinned to the DNS result that was validated, and redirects are refused. In the dashboard the same thing lives under **Webhooks** in the team sidebar. **Add webhook** asks for an **Endpoint URL**, then **Events** as either **All events** or **Choose events**, then a **Status** toggle. @@ -182,7 +182,7 @@ export function verifySpacefastWebhook(rawBody, header, secret) { } ``` -Three things that break naive verifiers. The header can carry more than one `v1=` entry, so accept a match on any of them. The body must be the raw bytes. And the timestamp tolerance is yours to choose, since the header carries the signing time but the server does not enforce a window on your behalf. +Three things that break naive verifiers. The header can carry more than one `v1=` entry, so accept a match on any of them. The body must be the raw bytes. The timestamp tolerance is yours to choose: the header carries the signing time, but the server does not enforce a window on your behalf. ## Rotate the secret diff --git a/content/cli/agent-commands.mdx b/content/cli/agent-commands.mdx index ddae411e..ee272ff0 100644 --- a/content/cli/agent-commands.mdx +++ b/content/cli/agent-commands.mdx @@ -5,7 +5,7 @@ sidebar: order: 27 --- -The commands on this page answer questions and finish jobs rather than change what is published. After this page you can look up any Space you can reach, pull a private route from the terminal, push saved settings live, recover a publish after a claim, and diagnose a broken setup. +The commands on this page answer questions and finish jobs rather than change what is published. After this page you can look up any Space you can reach and pull a private route from the terminal. Every command here also takes the [global flags](/cli#global-flags). diff --git a/content/cli/agents.mdx b/content/cli/agents.mdx index bc0059f8..3e8df15d 100644 --- a/content/cli/agents.mdx +++ b/content/cli/agents.mdx @@ -27,7 +27,7 @@ That detects your agent clients, installs the Spacefast skill, and writes MCP co | `remote-oauth` (`--remote --oauth`) | A direct HTTP connection to the hosted server | Your editor, through OAuth | | `local` (`--local`) | A local stdio server that can read your checkout | Your CLI login | -**The default is `remote-proxy`, and here is what that means for you.** Your editor speaks stdio to `sf` on your machine. `sf` speaks HTTPS to the hosted MCP server using the login you already have. You never do an OAuth dance in the editor, and when you run `sf login` again, the proxy picks the new login up without an editor restart. The cost is that `sf` has to be on your `PATH` and you have to be logged in; an unauthenticated proxy answers JSON-RPC error `-32001` telling you to run `sf login`. +**The default is `remote-proxy`, and here is what that means for you.** Your editor speaks stdio to `sf` on your machine. `sf` speaks HTTPS to the hosted MCP server using the login you already have. You never do an OAuth dance in the editor. When you run `sf login` again, the proxy picks up the new login without an editor restart. The cost: `sf` has to be on your `PATH`, and you have to be logged in. An unauthenticated proxy answers JSON-RPC error `-32001` telling you to run `sf login`. Pick `--local` instead when you want the MCP server to publish files from your working directory. Pick `--remote --oauth` when you want the editor to own the credential rather than the CLI. diff --git a/content/cli/api-keys.mdx b/content/cli/api-keys.mdx index 49f252e4..62297c34 100644 --- a/content/cli/api-keys.mdx +++ b/content/cli/api-keys.mdx @@ -56,7 +56,7 @@ Store this secret now. It is only shown once. The secret appears once and is never retrievable. Copy it straight into your secret store. -With `--json`, `data` carries `apiKey` and the compiled `permissions` array, and the secret is replaced with a note saying it was shown once. Run the command without `--json` when you need the value, or pipe the human output where you want it. +With `--json`, `data` carries `apiKey` and the compiled `permissions` array. The secret is replaced with a note saying it was shown once. Run the command without `--json` when you need the value, or pipe the human output where you want it. Use the key by exporting `SPACEFAST_TOKEN`, passing `--token`, or storing it with `sf login --token`. diff --git a/content/cli/api.mdx b/content/cli/api.mdx index 6d478926..aa5813d7 100644 --- a/content/cli/api.mdx +++ b/content/cli/api.mdx @@ -78,7 +78,7 @@ On a non-GET request it fails with `--paginate is only valid for GET requests.` ## Output -A JSON response prints verbatim on stdout, envelope and all, so a success is `{ data }` from the API and a failure is its problem document. A `204`, `205`, or `304` has no body, so `sf api` prints `{"data":null}` instead of reporting an un-downloadable response. +A JSON response prints verbatim on stdout, envelope and all. A success is `{ data }` from the API; a failure is its problem document. A `204`, `205`, or `304` has no body, so `sf api` prints `{"data":null}` instead of reporting an un-downloadable response. A non-JSON response needs somewhere to go. Pass `--output ` to write it to disk or `--raw-stdout` to stream it, or the command fails rather than dumping bytes into your terminal. diff --git a/content/cli/db.mdx b/content/cli/db.mdx index 9ff14daf..c1f6a2e0 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -182,7 +182,7 @@ sf db console [target] [--space ] [--show-secret] | --- | --- | --- | | `--show-secret` | `false` | Print the single-use console URL instead of opening it | -The URL is short-lived, single-use, and grants full SQL authority, which is why it opens in a browser instead of printing. It requires write access. +The URL is short-lived, single-use, and grants full SQL authority. That's why it opens in a browser instead of printing. It requires write access. ```bash sf db console diff --git a/content/cli/domains.mdx b/content/cli/domains.mdx index 24dee8ae..657c4514 100644 --- a/content/cli/domains.mdx +++ b/content/cli/domains.mdx @@ -36,7 +36,7 @@ sf domains add [--space ] [--role standard|primary|redirect] The command makes three calls: it creates the domain in the Space's team, attaches it to the Space with the role you asked for, and queues a DNS check. -`--wait` polls the Space's runtime until it is active with a live version, then sends a `HEAD` request to the hostname and requires the response to carry `x-spacefast-runtime: 1` and the live version id. Redirect-role domains skip that serve check. +`--wait` polls the Space's runtime until it is active with a live version, then sends a `HEAD` request to the hostname. The response must carry `x-spacefast-runtime: 1` and the live version id. Redirect-role domains skip that serve check. ```bash sf domains add example.com --space docs @@ -103,7 +103,7 @@ Verification: verified Operation: opr_xxxxxxxx ``` -On `pending` or `failed` it reprints the required DNS records and tells you to rerun the check, or to run `sf domains diagnostics ` for the records it actually observed. +On `pending` or `failed` it reprints the required DNS records and tells you to rerun the check. Run `sf domains diagnostics ` instead to see the records it actually observed. ## sf domains diagnostics diff --git a/content/cli/env.mdx b/content/cli/env.mdx index ba67f8fa..fc8a0263 100644 --- a/content/cli/env.mdx +++ b/content/cli/env.mdx @@ -11,7 +11,7 @@ Every command here takes the global flags (`--api-url`, `--profile`, `--token`, ## How variables behave -A write is applied **when a version next finalizes**. The live version keeps the values it was sealed with, so setting a variable does not change what is serving until you publish again or the debounced re-finalize lands. +A write is applied **when a version next finalizes**. The live version keeps the values it was sealed with. Setting a variable doesn't change what's serving until you publish again or the debounced re-finalize lands. Names are 1 to 128 characters matching `^[A-Za-z_][A-Za-z0-9_]*$`. Names starting with `SPACEFAST_` are reserved and rejected. @@ -123,7 +123,7 @@ sf env import [--space ] [--secret] [--production] [--preview] | `--branch=` | none | Also store each imported value for an exact branch. Repeatable | | `--from=` | `dotenv` | Platform export format to import. Vercel, Netlify, and Cloudflare all use dotenv exports | -The parser strips a BOM, skips blank and `#` lines, strips a leading `export `, and unescapes `\n`, `\r`, `\t`, `\"`, `\\`, and `\$` inside double quotes. An inline `#` outside quotes is stripped. A missing `=` is an error naming the line, an unclosed quote is an error, and **an empty value is an error**. A later duplicate overwrites an earlier one. Imports are one write per entry, in file order. +The parser strips a BOM, skips blank and `#` lines, strips a leading `export `, and unescapes `\n`, `\r`, `\t`, `\"`, `\\`, and `\$` inside double quotes. An inline `#` outside quotes is stripped. A missing `=` is an error naming the line. An unclosed quote is an error. **An empty value is an error.** A later duplicate overwrites an earlier one. Imports are one write per entry, in file order. ```bash sf env import .env --space docs diff --git a/content/cli/index.mdx b/content/cli/index.mdx index 54d93a7d..bc8eb3eb 100644 --- a/content/cli/index.mdx +++ b/content/cli/index.mdx @@ -35,7 +35,7 @@ sf publish ./dist `sf login` prints a code and a link, opens your browser, and polls until you approve. See [Authentication](/cli/login) for the read-only variant and for signing in with an API key. -Bare `sf` runs `sf status`, so typing `sf` in a project tells you who you are, which team is default, and which Space the directory is linked to. +Bare `sf` runs `sf status`. Typing `sf` in a project tells you who you are, which team is default, and which Space the directory is linked to. ## Global flags @@ -85,7 +85,7 @@ Commands that act on a Space add `--space`, `-o, --team`, and `--claim-token`. C } ``` -`error.code` is stable, `error.retryable` says whether re-running the same command unchanged could work, and `error.recovery` is one runnable hint. Validation failures add `error.pointer`, an RFC 6901 JSON Pointer into the failing input. List endpoints pass their `{ data, pagination }` envelope through without double-wrapping. Capability query parameters and blob paths are replaced with `[redacted]`. +`error.code` is stable. `error.retryable` says whether re-running the same command unchanged could work. `error.recovery` is one runnable hint. Validation failures add `error.pointer`, an RFC 6901 JSON Pointer into the failing input. List endpoints pass their `{ data, pagination }` envelope through without double-wrapping. Capability query parameters and blob paths are replaced with `[redacted]`. ## Exit codes diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 380f2d14..09888e58 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -7,7 +7,7 @@ sidebar: After this page you can scaffold a project and link a directory to a Space so `sf publish` needs no flags. -Two files carry the link. `.spacefast/space.json` is the committable one: which Space, which team, which API base URL. `.spacefast/state.json` is the local cache and holds credentials, so it never gets committed and the CLI adds it to `.gitignore` for you. +Two files carry the link. `.spacefast/space.json` is the committable one: which Space, which team, which API base URL. `.spacefast/state.json` is the local cache. It holds credentials, so it's never committed; the CLI adds it to `.gitignore` for you. Every command here also takes the [global flags](/cli#global-flags). @@ -219,7 +219,7 @@ sf profiles set acme --api-url https://api.acme-host.example --token sfa_xxxxxxx sf profiles set acme --token "" ``` -Neither flag reads an environment variable here, so an exported `SPACEFAST_TOKEN` cannot be captured into a profile by accident and `--token ""` keeps meaning "clear it". An `--api-url` with embedded credentials is rejected. +Neither flag reads an environment variable here. An exported `SPACEFAST_TOKEN` can't be captured into a profile by accident, and `--token ""` still means "clear it". An `--api-url` with embedded credentials is rejected. ### sf profiles use diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index 09381d1b..f2c0e2a2 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -165,7 +165,7 @@ The team-wide default for future Spaces is `sf teams defaults`, on the [teams pa ## Anonymous publish and the claim key -With no login and no linked Space, publish creates an anonymous Space and saves its key (`sfc_…`), claim URL, and expiry in `.spacefast/state.json`, which the CLI adds to `.gitignore`. The claim link prints once, on the publish that created the Space. Later publishes point at `sf spaces claim` instead. +With no login and no linked Space, publish creates an anonymous Space. It saves the key (`sfc_…`), claim URL, and expiry in `.spacefast/state.json`, which the CLI adds to `.gitignore`. The claim link prints once, on the publish that created the Space. Later publishes point at `sf spaces claim` instead. ```bash sf spaces claim # claim the Space saved here @@ -253,7 +253,7 @@ The headline verb is `Published`, `Updated`, `No changes`, `Ready`, or `Held`, d } ``` -`activation.outcome` is one of `activated`, `unpromoted`, `superseded`, or `pending`. `next.action` is `poll` while the version is still building and `done` once it is ready; when the version is ready but not live, `next.url` is the promote endpoint and `next.hint` says why. An anonymous receipt adds a `claim` block with `key`, `url`, `claimUrl`, and `expiresAt`. +`activation.outcome` is one of `activated`, `unpromoted`, `superseded`, or `pending`. `next.action` is `poll` while the version is still building, and `done` once it's ready. When the version is ready but not live, `next.url` is the promote endpoint and `next.hint` says why. An anonymous receipt adds a `claim` block with `key`, `url`, `claimUrl`, and `expiresAt`. With `--stream`, stdout becomes JSONL, one event per line: @@ -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, so for a framework project use that framework's own dev server (`next dev`, `vite`) and run `sf publish` when you are ready. +`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. | Flag | Default | What it does | | --- | --- | --- | diff --git a/content/cli/share.mdx b/content/cli/share.mdx index 4bab512d..a50abf6a 100644 --- a/content/cli/share.mdx +++ b/content/cli/share.mdx @@ -157,7 +157,7 @@ The output is the Link row plus its share URL. The URL is `[REDACTED]` in JSON a `sf share link copy ID` prints a Link's durable share URL. It takes `--show-secret` with the same meaning as on `create`. -`sf share link edit ID` replaces a Link's metadata or Grant dimensions without rotating its URL. It takes the same `--name`, `--landing`, `--path`, `--exclude`, `--role`, and `--target` flags as `create` and the full constraint set, each defaulting to unchanged, plus the clear flags below. Every flag replaces rather than merges, `--exclude` requires `--path`, and passing no field is a validation error. +`sf share link edit ID` replaces a Link's metadata or Grant dimensions without rotating its URL. It takes the same `--name`, `--landing`, `--path`, `--exclude`, `--role`, and `--target` flags as `create` and the full constraint set, each defaulting to unchanged, plus the clear flags below. Every flag replaces rather than merges. `--exclude` requires `--path`, and passing no field is a validation error. | Flag | Default | What it does | | --- | --- | --- | diff --git a/content/cli/source.mdx b/content/cli/source.mdx index 09188156..e15f0a65 100644 --- a/content/cli/source.mdx +++ b/content/cli/source.mdx @@ -9,7 +9,7 @@ After this page you can browse, search, and change a Space's source repository f 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). -The `--connection-type` values here are the older spelling, `remote|push|import`. `remote` is the same connection as `connected` on the `sf git` commands and `push` is the same as `hosted`. `import` is the one-time ingestion remote and has no `sf git` equivalent. See [two connection types, two spellings](/cli/git#two-connection-types-two-spellings). +The `--connection-type` values here are the older spelling, `remote|push|import`. `remote` is the same connection as `connected` on the `sf git` commands. `push` is the same as `hosted`. `import` is the one-time ingestion remote and has no `sf git` equivalent. See [two connection types, two spellings](/cli/git#two-connection-types-two-spellings). ## sf source ls diff --git a/content/cli/storage.mdx b/content/cli/storage.mdx index 8c1ef1e5..94248fb2 100644 --- a/content/cli/storage.mdx +++ b/content/cli/storage.mdx @@ -86,7 +86,7 @@ Read a Space's logs. `access` is every request the edge served. `runtime` is what your code logged, with the request id and handler for each line. A static Space has no runtime lines. -Runtime lines are written on the origin, shipped, and indexed before they can be read back, so a line you just triggered takes a while to appear. **An empty page means not yet, not broken.** A page can come back empty while there is still more to read, so keep paging while the cursor is there. +A line you just triggered takes a while to appear — it's written on the origin, shipped, and indexed before it can be read back. **An empty page means not yet, not broken.** Keep paging while the cursor is there: a page can come back empty while there's still more to read. ```text sf logs [target] [kind] [--space ] [--limit ] [-f] [--cursor ] diff --git a/content/cli/teams.mdx b/content/cli/teams.mdx index 68471961..9a88497c 100644 --- a/content/cli/teams.mdx +++ b/content/cli/teams.mdx @@ -39,7 +39,7 @@ Set default team. sf teams switch [TEAM] ``` -Sets the default team for future CLI publishes. The selection is stored per login and cleared when you log in again, so it can never outlive the identity that made it. +Sets the default team for future CLI publishes. The selection is stored per login and cleared when you log in again. It can never outlive the identity that made it. ### Arguments diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index 444bd9b6..d921b155 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -50,7 +50,7 @@ sf zero call [--space ] [--credential ] [--input ` | 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 | -The minted token travels in `X-SF-Authorization`, because the plain `Authorization` header belongs to your application and passes through untouched. +The minted token travels in `X-SF-Authorization`. The plain `Authorization` header belongs to your application and passes through untouched. ```bash sf zero call content.posts.list @@ -102,7 +102,7 @@ With `--json`, `data` is `{ out, spaceId, versionId, contentModelRevision, abili 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, because a content model missing the fields that refused would publish a site that silently lost content. +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. ```text sf zero import [--out ] [--capsule ] [--force] @@ -138,7 +138,7 @@ sf zero import emdash ../my-emdash-site --out . Run WP-CLI against a Space's WordPress, or against a local one with `--local`. -Everything after the flags is passed to `wp` unchanged, so `sf wp plugin list --format=csv` runs exactly that. With `--local`, WP-CLI's own output and exit code pass through untouched. Against a Space, the command runs remotely and reports whether it succeeded and how long it took; the platform returns no stdout, so read state back with a follow-up command such as `sf wp option get`. `--json` wraps only Spacefast's own errors, never `wp`'s output. See [WordPress on every Space](/wordpress). Use `--` to pass a flag `sf` would otherwise claim. +Everything after the flags is passed to `wp` unchanged, so `sf wp plugin list --format=csv` runs exactly that. With `--local`, WP-CLI's own output and exit code pass through untouched. Against a Space, the command runs remotely and reports whether it succeeded and how long it took. The platform returns no stdout, so read state back with a follow-up command such as `sf wp option get`. `--json` wraps only Spacefast's own errors, never `wp`'s output. See [WordPress on every Space](/wordpress). Use `--` to pass a flag `sf` would otherwise claim. ```text sf wp [--space ] [--local] [--path ] [--mode full|limited] -- @@ -209,7 +209,7 @@ sf pages pull [target] | --- | --- | | `[target]` | `all`, `layout`, or a site page id. Defaults to `all` | -The site page ids are `404`, `denied`, `access`, `index`, and `preview`. It writes `_layout.html` at the project root and `_pages/.html` for each id. An existing file makes the write fail; there is no force flag, so move or delete the file first. +The site page ids are `404`, `denied`, `access`, `index`, and `preview`. It writes `_layout.html` at the project root and `_pages/.html` for each id. An existing file makes the write fail. There is no force flag, so move or delete the file first. ```bash sf pages pull @@ -280,7 +280,7 @@ With `--json`, `data` is `{ written, skippedExisting, warnings }`. Print a Space's runtime analytics series. -The numbers are read live from the edge, not from a local store, and they cover every hostname mapped to the Space. `views` counts successful non-crawler requests, status under 400, assets included. Daily uniques are the finest resolution available and are never summed across days, which is why the per-day series prints instead of one total. +The numbers are read live from the edge, not from a local store, and they cover every hostname mapped to the Space. `views` counts successful non-crawler requests, status under 400, assets included. That's why the per-day series prints instead of one total: daily uniques are the finest resolution available and are never summed across days. ```text sf analytics [--space ] [--window 48h|7d|30d] diff --git a/content/index.mdx b/content/index.mdx index f2e2dfb1..27150779 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -5,7 +5,7 @@ sidebar: order: 0 --- -Spacefast hosts the sites people build with AI agents. You hand it a folder, it hands back a URL. Everything you can do in the dashboard you can also do from the `sf` CLI, the HTTP API, or an MCP server, so the agent that built the site can publish it too. +Spacefast hosts the sites people build with AI agents. You hand it a folder, it hands back a URL. Everything you can do in the dashboard you can also do from the `sf` CLI, the HTTP API, or an MCP server. That means the agent that built the site can publish it too. ## Publish in one command diff --git a/content/platforms/partner-api/configuration.mdx b/content/platforms/partner-api/configuration.mdx index 9e7b1416..9b5904c0 100644 --- a/content/platforms/partner-api/configuration.mdx +++ b/content/platforms/partner-api/configuration.mdx @@ -86,7 +86,7 @@ CORS origins cannot contain wildcards, credentials, query strings, fragments, or } ``` -`features` grants capabilities. `branding.hide` hides the Spacefast badge, `pages.templates` allows `_pages/*.html` full-page takeovers, and `domains.proxy_bindings` allows proxy routes to external upstreams. A partner plan grants none of them unless listed. +`features` grants capabilities. `branding.hide` hides the Spacefast badge. `pages.templates` allows `_pages/*.html` full-page takeovers. `domains.proxy_bindings` allows proxy routes to external upstreams. A partner plan grants none of them unless listed. `quotas` takes `spaces`, `storageBytes`, `buildMinutesPerMonth`, and `customDomains`, each a nonnegative integer. An omitted quota has no cap. `quotaPolicy` is `block` (the default) or `warn`. diff --git a/content/platforms/partner-api/customers.mdx b/content/platforms/partner-api/customers.mdx index bf23f1b1..80221c5a 100644 --- a/content/platforms/partner-api/customers.mdx +++ b/content/platforms/partner-api/customers.mdx @@ -48,7 +48,7 @@ Follow `pagination.nextCursor` until `hasMore` is false. Filters include `from`, Records are immutable and identify the tenant, principal, Space, dimension, bucket, and mode. Quantities are signed integer strings. Keep integer precision when importing them into your billing system. -A correction is a new record linked through `corrects`. Deduplicate by `usageRecordId`, retain the original, and account for the correction. Do not overwrite an earlier record or assume all quantities are positive. Test-mode records are never billable. +A correction is a new record linked through `corrects`. Deduplicate by `usageRecordId` and retain the original. Then account for the correction. Do not overwrite an earlier record or assume all quantities are positive. Test-mode records are never billable. Use `GET /v1/usage/periods` and `GET /v1/usage/periods/{periodId}` for monthly period status and totals. Usage arrives after measurement settles. A newly published Space can have no measured usage yet. diff --git a/content/platforms/partner-api/go-live.mdx b/content/platforms/partner-api/go-live.mdx index 7189f853..1b0b7a0c 100644 --- a/content/platforms/partner-api/go-live.mdx +++ b/content/platforms/partner-api/go-live.mdx @@ -26,7 +26,7 @@ Verify that your application handles each failure it can expose to a customer. A ## Prepare the live tenant -Create the live tenant's system Space, [designate it](/platforms/partner-api/configuration#designate-a-system-space) with the live tenant API key, and publish it once. Both the test and live system Spaces need a live version before promotion. Keep the live content you want to serve in the live system Space. +Create the live tenant's system Space and [designate it](/platforms/partner-api/configuration#designate-a-system-space) with the live tenant API key. Publish it once. Both the test and live system Spaces need a live version before promotion. Keep the live content you want to serve in the live system Space. Register and activate a separate live signing issuer. Promotion preserves the live issuer declaration and never copies the test issuer. Store live API keys and webhook secrets separately from test secrets. @@ -43,7 +43,7 @@ curl --fail-with-body -sS -X POST \ -d '{}' ``` -The body is required, even though it is empty. Generate `PROMOTION_KEY` (a UUID) once per promotion and reuse it when retrying after a timeout, so a retry replays the first result instead of promoting again. +The body is required, even though it is empty. Generate `PROMOTION_KEY` (a UUID) once per promotion. Reuse it when retrying after a timeout, so a retry replays the first result instead of promoting again. Promotion publishes the test system configuration onto the live system Space while retaining its live content. The test tenant remains unchanged. New domains and other live resources require their own verification. @@ -57,10 +57,10 @@ Complete this sequence with a dedicated live test customer before admitting cust 2. Open its returned HTTPS URL and verify the content. Repeat through the custom customer hostname if you use one. 3. Mint a live customer JWT and read that customer's Space. Confirm that a different customer's JWT cannot read it. 4. Receive a webhook on your HTTPS receiver. Verify its raw-body signature, record the event ID, and acknowledge with `2xx`. -5. Return a temporary error from the receiver. Confirm that the same event is retried and that new matching events have delivery records while the endpoint is `failing`. +5. Return a temporary error from the receiver. Confirm that the same event is retried. New matching events should have delivery records while the endpoint is `failing`. 6. Restore the receiver and confirm delivery recovery. Exercise manual redelivery and deduplicate by event ID. 7. Suspend and restore the test customer. Check the serving page and mutation refusals in both states. -8. Rotate a tenant API key and a signing key. Verify the replacement before revoking the old credential, then confirm that the revoked credential fails. +8. Rotate a tenant API key and a signing key. Verify the replacement before revoking the old credential. Then confirm that the revoked credential fails. 9. Check the custom API gateway with a foreign tenant credential and a caller-supplied tenant header. The gateway must enforce its configured tenant. 10. Delete the dedicated test customer's resources when verification is complete. diff --git a/content/platforms/partner-api/index.mdx b/content/platforms/partner-api/index.mdx index 79b3de30..5b9438ff 100644 --- a/content/platforms/partner-api/index.mdx +++ b/content/platforms/partner-api/index.mdx @@ -65,7 +65,7 @@ Use a separate client and query cache for each tenant and customer identity. A t Create a replacement with `POST /v1/tenants/{tenantId}/api-keys`. The body accepts `name`, optional `permissions`, `expiresAt`, `notBefore`, `ipAllowlist`, and `metadata`. Omitted permissions grant partner administration. Explicit permissions narrow the grant. -Save the returned secret, update your integration, and verify a request with the replacement. Then revoke the old key with `DELETE /v1/tenants/{tenantId}/api-keys/{apiKeyId}`. Revocation takes effect on subsequent requests. `GET /v1/tenants/{tenantId}/api-keys` returns masked previews, never existing secrets. +Save the returned secret and update your integration, then verify a request with the replacement. Revoke the old key with `DELETE /v1/tenants/{tenantId}/api-keys/{apiKeyId}`. Revocation takes effect on subsequent requests. `GET /v1/tenants/{tenantId}/api-keys` returns masked previews, never existing secrets. ## Continue the integration diff --git a/content/platforms/partner-api/tokens.mdx b/content/platforms/partner-api/tokens.mdx index 59128111..e8405461 100644 --- a/content/platforms/partner-api/tokens.mdx +++ b/content/platforms/partner-api/tokens.mdx @@ -43,7 +43,7 @@ const proof = await new SignJWT({ challenge }) .sign(privateKey); ``` -Add the signed JWT to that issuer's `proofs` array, retain its `issuer` and `keys`, and republish. Wait for the manifest item to become `active`. If the key proposal changes, read the new challenge and sign it again. +Add the signed JWT to that issuer's `proofs` array, and retain its `issuer` and `keys`. Then republish. Wait for the manifest item to become `active`. If the key proposal changes, read the new challenge and sign it again. ## Sign a customer JWT @@ -80,13 +80,13 @@ Send it as `Authorization: Bearer `. Authenticate the customer in your ow JWTs may be reused until expiry. The maximum encoded token size is 8,192 bytes. Headers that supply remote or embedded keys, including `jku`, `x5u`, `jwk`, and `x5c`, are refused. -A partner JWT acts only for its customer. It cannot administer principals, mint API keys, or delete Spaces, and team listings return no teams. Use the tenant API key for management work. +A partner JWT acts only for its customer. It cannot administer principals, mint API keys, or delete Spaces. Team listings return no teams. Use the tenant API key for management work. ## Rotate or revoke signing keys -Publish the existing key and the new public key together. Read the new challenge, sign it with each new private key, and republish the proofs. Existing keys remain active while the proposal is pending. +Publish the existing key and the new public key together. Read the new challenge and sign it with each new private key, then republish the proofs. Existing keys remain active while the proposal is pending. -After activation, start signing with the new key. Keep the old public key until its outstanding JWTs expire, then remove it and republish. Removing an active key invalidates JWTs signed by that key on subsequent requests. +After activation, start signing with the new key. Keep the old public key until its outstanding JWTs expire. Then remove it and republish. Removing an active key invalidates JWTs signed by that key on subsequent requests. Remove `tokenIssuers`, or publish an empty array, to revoke the tenant issuer. There is no individual JWT revocation endpoint. For urgent revocation, remove the affected signing key or revoke the issuer. diff --git a/content/troubleshooting.mdx b/content/troubleshooting.mdx index ffac2611..7ae171ff 100644 --- a/content/troubleshooting.mdx +++ b/content/troubleshooting.mdx @@ -43,15 +43,15 @@ sf builds ls --space my-site sf builds logs bld_xxxxxxxx --follow ``` -The codes name the stage. `build_failed` is your command exiting non-zero. `build_timeout` hit the time limit. `build_oom` was killed, most likely out of memory. `build_output_dir_missing` means the build finished but the output directory you named was never produced, and `build_no_index_html` means it produced output with no `index.html` at the root. Detection defaults, override flags, and the log limits are on [Frameworks and builds](/frameworks). +The codes name the stage. `build_failed` is your command exiting non-zero. `build_timeout` hit the time limit. `build_oom` was killed, most likely out of memory. `build_output_dir_missing` means the build finished but the output directory you named was never produced. `build_no_index_html` means it produced output with no `index.html` at the root. Detection defaults, override flags, and the log limits are on [Frameworks and builds](/frameworks). ## Serving ### The site still shows the old version -Shared caches hold a copy for ten minutes. Non-immutable files are sent with `public, s-maxage=600, max-age=0, must-revalidate`, so your own browser revalidates on every load but a shared copy can be up to ten minutes stale. +Shared caches hold a copy for ten minutes. Non-immutable files are sent with `public, s-maxage=600, max-age=0, must-revalidate`. Your own browser revalidates on every load, but a shared copy can be up to ten minutes stale. -A publish purges every hostname the Space serves, which normally makes that moot. When the purge does not confirm you get a `runtime_purge_failed` diagnostic, the runtime retries, and the ten-minute lifetime is the backstop. Check which version answered you: +A publish purges every hostname the Space serves, which normally makes that moot. When the purge does not confirm, you get a `runtime_purge_failed` diagnostic and the runtime retries. The ten-minute lifetime is the backstop. Check which version answered you: ```bash curl -sI https://my-site.view.fast/ | grep -i x-spacefast-version @@ -63,7 +63,7 @@ Full policy on [Caching](/caching). Three rules decide this, and your local dev server implements none of them. -Clean URLs are on by default, so `about.html` answers `/about` and `/about/` redirects to `/about` with a 308. A directory serves its `index.html`. A single-page app needs an explicit `fallback` in `sf.jsonc`, and the fallback is skipped for around 110 asset extensions so a missing `.js` returns 404 instead of your HTML shell. +Clean URLs are on by default, so `about.html` answers `/about` and `/about/` redirects to `/about` with a 308. A directory serves its `index.html`. A single-page app needs an explicit `fallback` in `sf.jsonc`. The fallback is skipped for around 110 asset extensions, so a missing `.js` returns 404 instead of your HTML shell. If two files would answer the same URL after that resolution, the publish stops with `publish_path_collision` rather than letting lookup order decide. See [Redirects, rewrites, and headers](/routing). @@ -87,7 +87,7 @@ Verification failure never takes a live site down. Compiled routes and certifica ### The certificate is `broken` -Issuance failed and something in the zone is blocking it. The usual cause is a CAA record that does not permit the issuing authority, which shows up as `ssl_renewal_blocked` in the diagnostics. +Issuance failed and something in the zone is blocking it. The usual cause is a CAA record that does not permit the issuing authority. That shows up as `ssl_renewal_blocked` in the diagnostics. Fix the CAA record, then retry. Verification and certificate retries are capped at 6 per hour per domain and 120 per hour per team, answering `rate_limited` past that. Looping the retry does not make a certificate issue faster. @@ -121,9 +121,9 @@ A credential from `sf login` lasts 30 days and expires after 7 days without use, Claude Code on the web and mobile, Codex cloud, and similar hosted sandboxes block unknown hosts by default, and Spacefast is one of them. The CLI fails with `network_error` or `upload_transport_error`, and the agent cannot read these docs either. Three ways out, best first: -1. **Add the Spacefast connector to Claude.** [Open the connector form](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Spacefast&connectorUrl=https%3A%2F%2Fmcp.spacefast.com), choose **Connect**, approve, and enable it for the session. Connector traffic goes through Anthropic rather than the sandbox, so publishing works with no network changes, including from the mobile app. +1. **Add the Spacefast connector to Claude.** [Open the connector form](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Spacefast&connectorUrl=https%3A%2F%2Fmcp.spacefast.com) and choose **Connect**. Approve it, then enable it for the session. Connector traffic goes through Anthropic rather than the sandbox, so publishing works with no network changes, including from the mobile app. 2. **Push to a connected GitHub repository.** Sandboxes can push to GitHub, and Spacefast builds the push itself. Connect the repository once from the Space's **Builds** page; see [Connect a GitHub repository](/git#connect-a-github-repository). Claude Code on the web pushes a working branch, which builds a preview; merge the pull request to go live. -3. **Allow the hosts.** Add `spacefast.com`, `*.spacefast.com`, and `*.view.fast` to the environment's allowlist. In Claude Code that is **Network access → Custom → Allowed domains**; keep **Also include default list of common package managers** checked, or npm, and with it the CLI, is blocked too. In Codex cloud, turn on **Agent internet access**, add the domains, and leave HTTP methods unrestricted, since uploads use `PUT` and `POST`. Allowing only `api.spacefast.com` is not enough: uploads go to `*.view.fast`. +3. **Allow the hosts.** Add `spacefast.com`, `*.spacefast.com`, and `*.view.fast` to the environment's allowlist. In Claude Code that is **Network access → Custom → Allowed domains**. Keep **Also include default list of common package managers** checked, or npm — and the CLI with it — is blocked too. In Codex cloud, turn on **Agent internet access** and add the domains. Leave HTTP methods unrestricted, since uploads use `PUT` and `POST`. Allowing only `api.spacefast.com` is not enough: uploads go to `*.view.fast`. ### An agent gets 403 on one tool @@ -135,4 +135,4 @@ The failure is `insufficient_scope`, and the `WWW-Authenticate` header names the Every rate-limited response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` in seconds. A `429` adds `Retry-After`, also in seconds. Wait that long. The code is `rate_limited`. -When an hourly limiter is the one that rejected you, those headers describe the hourly window rather than the per-minute one, so the number can be much larger than you expect. Rate limiting fails open, so the headers are sometimes absent entirely and client code must not require them. See [Rate limits](/api/rate-limits). +When an hourly limiter is the one that rejected you, those headers describe the hourly window rather than the per-minute one. The number can be much larger than you expect. Rate limiting fails open, so the headers are sometimes absent entirely and client code must not require them. See [Rate limits](/api/rate-limits). From 67cb9ab6b06756f1d6900f4cb4bc31fcd92c0549 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 15:09:05 -0400 Subject: [PATCH 03/20] Add STYLE_GUIDE.md and point AGENTS.md at it Consolidates the voice rules, the concision principle (adapted from WooCommerce's Automattic-owned docs style guide, keeping Spacefast's own 2nd-person voice rather than WooCommerce's 3rd-person), and every banned-word/wordiness/vocabulary rule from the 20 styles/Spacefast/*.yml Vale files into one document a contributor can actually read. AGENTS.md now points to it from both the Content and Prose style sections instead of carrying the rules inline. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 12 +-- STYLE_GUIDE.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 STYLE_GUIDE.md diff --git a/AGENTS.md b/AGENTS.md index f9bbca1b..e28927ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,12 +9,9 @@ Assume every commit and every line of history will be public. flags, routes, defaults, or timelines. - Keep the voice direct, no-BS, and a little playful. Prefer the best path over an encyclopedia of alternatives. -- Open each page with one short sentence naming the single most important - outcome, not an inventory of every outcome the page covers — the page's own - headings already carry the rest. Be concise; lead with importance (see - WooCommerce's developer docs style guide). `styles/Spacefast/SentenceLength.yml` - flags sentences over 30 words as a suggestion; an opener that trips it is a - sign to cut, not to find a way to keep every clause. +- Be concise; lead with importance. See [STYLE_GUIDE.md](STYLE_GUIDE.md) for + the full rationale, worked examples, and the complete list of banned words, + wordiness swaps, and vocabulary rules Vale enforces. - Do not add navigation to a section until that section has a real page or generated source. - Authored navigation comes from `content/**` and its `meta.ts` files. @@ -74,6 +71,9 @@ bun run verify:routes ## Prose style (Vale) +See [STYLE_GUIDE.md](STYLE_GUIDE.md) for the human-readable version of every +rule below, with rationale and worked examples. + `bun run verify:prose` runs [Vale](https://vale.sh) over every docs page and over the `summary`/`description` fields of the generated OpenAPI snapshot (extracted to markdown, reported by spec + JSON Pointer). CI enforces it; zero diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 00000000..2c8cdc9d --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,196 @@ +# Spacefast docs style guide + +This is the human-readable version of the rules `bun run verify:prose` +enforces by machine, via [Vale](https://vale.sh) and the 20 rule files in +`styles/Spacefast/`. If you're writing or editing a page and want to know +*why* something got flagged, or just want to write it right the first time, +start here. The Vale files are the source of truth for exact patterns; this +document explains the reasoning and gives the full picture in one place. + +## Where this comes from + +Spacefast didn't have a documentation style guide — it had Vale rules and a +few bullets in `AGENTS.md`, but nothing a person could read start to finish. +This guide fills that gap. The concision principle below is adapted from +WooCommerce's (Automattic-owned) developer documentation guides: + +- [Technical Documentation Style Guide](https://developer.woocommerce.com/docs/contribution/contributing-docs/style-guide/) +- [Grammar, Punctuation, and Capitalization guide](https://developer.woocommerce.com/docs/best-practices/coding-standards/grammar-punctuation-capitalization/) + +One deliberate deviation: WooCommerce's guide writes in 3rd-person/imperative +voice ("Add an embed block to your page"). Spacefast's docs are 2nd-person +("you") throughout, and that doesn't change here — only the concision and +mechanical rules are adopted, not the voice. + +## Voice + +Direct, no-BS, and a little playful. Second person — "you," not "the user" +or "one." Prefer the best path over an encyclopedia of alternatives: one +good way to do a thing beats three options with trade-off tables, unless the +trade-off is the point. + +## Be concise, lead with importance + +This is the rule behind the biggest cleanup pass this corpus has had: state +the key fact first, in the sentence, the paragraph, and the page. Don't +make a reader walk through three subordinate clauses to find the one thing +that matters. + +In practice: + +- **One idea per sentence, where reasonable.** A sentence chaining three or + more unrelated clauses ("...which X, when Y, how Z, and where W...") is a + sign to split it or cut the least important clause, not a sign you've + covered the topic thoroughly. The page's own headings carry the rest — + an opening sentence or a summary line doesn't need to be a table of + contents. +- **`styles/Spacefast/SentenceLength.yml` flags sentences over 30 words**, + at suggestion level — it doesn't block CI. Treat it as a floor, not the + definition of "wordy." A 22-word sentence that chains four clauses is + still a problem this rule won't catch; use judgment, not just the word + count. +- **Cut filler and hedging.** If a sentence works with a phrase removed, + remove it. + +Worked example, from the page-opener cleanup: + +> Before: "After this page you know which directory your framework +> produces, when to publish that directory yourself versus letting +> Spacefast build, how detection picks commands, and where to read a +> failing build." (33 words, 4 clauses) +> +> After: "After this page you know whether to publish your own build +> output or let Spacefast build it — and where to look when a build +> fails." (25 words, 2 ideas joined by an em dash) + +Another: + +> Before: "After this page you can mint an API key with the right +> permissions, use it against the API, rotate it without downtime, and +> recognize every other credential Spacefast hands you by its prefix." (29 +> words, 4 clauses) +> +> After: "After this page you can mint a scoped API key and rotate it +> without downtime." (14 words — the credential-prefix table further down +> the page already covers the dropped clause.) + +## Banned words and phrases + +Vale errors on these. They're not style preferences — they're specific, +named failure modes. + +**Hype** (`Hype.yml`) — say what the thing does instead: seamless(ly), +delve, robust, cutting-edge, state-of-the-art, best-in-class, world-class, +game-changing/changer, revolutionize, supercharged, effortless(ly), +hassle-free, empowers, streamlines, blazing(ly) fast, tapestry, symphony, "a +beacon of," "a testament to," transformative, groundbreaking, pivotal, +multifaceted, holistic, "shed light on," "paves the way for," "at its +core," "in essence," "that being said," "in today's." + +**Condescension** (`Condescension.yml`) — assumes it's easy for the reader; +the instruction works without it: simply, easily, "just click," "just run," +obviously, "of course." + +**Weasel words** (`WeaselWords.yml`) — hedges instead of committing: "helps +ensure," "may be able to," "can potentially," "could potentially." Commit +to what the thing does, or state the real condition. + +**Intensifiers** (`Intensifiers.yml`) — intensity without information. Show +the number or cut it: extremely, dramatically, exceptionally, incredibly, +remarkably, truly, undoubtedly, significantly. + +**AI-speak** (`AISpeak.yml`) — corporate filler: "leverage" → use, +"utilize" → use, "facilitate" → help, "in order to" → to, +"furthermore"/"moreover" → also. Strip "it is important to note that" and +"please note that" entirely — they add nothing. + +## Tighten these phrases + +`Wordiness.yml` swaps wordy constructions for plain ones: + +| Instead of | Use | +|---|---| +| due to the fact that | because | +| in the event that | if | +| at this point in time | now | +| has/have the ability to | can | +| is/are able to | can | +| in a timely manner | promptly | +| a number of | several | +| the majority of | most | +| on a regular basis | regularly | +| make use of | use | +| in the process of | *(cut it)* | +| a wide range of | many | +| a variety of | many | + +`PhrasalVerbs.yml` — the verb is two words, the noun is one: "login to" → +log in to, "logout of" → log out of, "setup a/the/your" → set up a/the/your, +"backup your" → back up your, "sign into" → sign in to. + +`LatinAbbreviations.yml` — spell it out: "e.g." → for example, "i.e." → +that is. + +## Brand and vocabulary + +- **GitHub, JavaScript, TypeScript, npm, Node.js, macOS, OpenAPI, email, + website** — exact casing, every time (`Branding.yml`). +- **Spacefast** is capitalized in prose. Identifiers stay lowercase and get + backticked: `@spacefast/sdk`, `/spacefast`, `spacefast.com` + (`ProductName.yml`). +- **WP Cloud** — exact casing, never "wp cloud," "wp.cloud," or hyphenated + (`WpCloudBrand.yml`). Prefer "infra" generally; name WP Cloud only when + the external reference is genuinely necessary. Never say "the hosting + provider" for Spacefast's own infra (`HostingProvider.yml`) — generic + references to *other* hosting providers are fine. +- **"API key," never "access token"** (`ApiKey.yml`) — "OAuth access + token" is the one exempted phrase, since that's the protocol's own term. + +## What never appears in public copy + +This repo is public, including its history. Two rule files exist +specifically to keep internal infrastructure and vendor names out of it +(`Internals.yml`, `Providers.yml`): + +- Internal infra names: batcache, PlanetScale, pgbouncer, nginx, Caddy, + "web server," "runtime engine," "monorepo," local filesystem paths + (`/Users/...`, `/home/...`). Say "CDN," "edge," or name the user-facing + behavior instead. +- Internal vendor names: E2B, Pierre, `code.storage`. Say "Spacefast + Builds," "Spacefast CI," or "infra" instead. + +If you're not sure whether a name is safe to publish, assume it isn't and +ask — `bun run verify:public-safety` catches a lot of this mechanically, +but not everything. + +## Mechanics + +- **Oxford comma** — use it. Advisory only (`OxfordComma.yml`); the + checker can't tell a serial list from a compound verb phrase, so it never + blocks, but the house style is it belongs there. +- **Link text names the destination.** Never "[click here]" or "[this]" — + Vale errors on both (`LinkText.yml`). +- **Unknown technical words** go in `styles/Spacefast/spelling-exceptions.txt` + (sorted, case matters for proper nouns) rather than getting flagged as + typos. +- **Identifiers in prose** — commands, enum values, claim names, package + names — get backticks, not prose styling. Vale skips code spans, so this + also sidesteps most false-positive spelling/casing alerts. + +## What this guide doesn't cover + +The changelog (`content/changelog/**`) is a historical record — only +public-safety and brand-casing rules apply there; shipped release notes +keep their original wording. Generated reference content +(`generated/openapi/**`, `generated/errors/**`, `generated/cli/**`, etc.) is +producer-owned and fixed at its source in the product monorepo, not edited +here — but its prose (OpenAPI `summary`/`description` fields) is still +linted by the same Vale rules. + +## Before you open a PR + +```bash +bun run verify:prose +``` + +Zero alerts is the bar. See `AGENTS.md` for the full verification chain. From 752b3c8a360d8981f4984ef33c9157e5c32fba2d Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 14:46:25 -0400 Subject: [PATCH 04/20] Surface the dashboard Drop path and add audience routing Resurfaces the existing no-code Drop publishing path (dashboard drag-and-drop, no CLI, no account) which was documented but buried two clicks past a CLI-only Quickstart. Adds a homepage pointer and card, a Quickstart pointer, and extends the existing CLI/Dashboard Tabs convention to the domain-setup steps that already mixed both in prose. Also adds a cross-link from the anonymous-claim agent credential section to the Agents permissions page. Co-Authored-By: Claude Sonnet 5 --- content/(publish)/anonymous-and-claim.mdx | 2 ++ content/(serve)/domains.mdx | 24 +++++++++++++++++++++-- content/index.mdx | 5 +++++ content/quickstart.mdx | 2 ++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index df5b49e0..bf3b68f4 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -124,6 +124,8 @@ On the claim page that is the **Claim & keep access** button. On the CLI it is ` If you say yes, the agent's next request trades the demoted key for a real API key scoped to the Space, using `exchangeSpaceClaim`. That key is shown exactly once. If you say no, the old key stops working and the agent has to be reconnected the normal way. +For the full picture of what a credential like this can and cannot do, see [what agents are allowed to do](/agents/permissions). + ## Rotating the key If the key leaked, replace it: diff --git a/content/(serve)/domains.mdx b/content/(serve)/domains.mdx index 9a5090e2..868dedf3 100644 --- a/content/(serve)/domains.mdx +++ b/content/(serve)/domains.mdx @@ -12,6 +12,9 @@ A domain belongs to a **team**, not to a Space. You add it once to the team's in + + + The CLI does the whole first step in one command: it creates the domain on the team, assigns it to the Space, and queues the first DNS check. ```bash @@ -20,7 +23,13 @@ A domain belongs to a **team**, not to a Space. You add it once to the team's in Add `--role primary` to make it the Space's live address as soon as it verifies. - In the dashboard, open the Space, go to **Domains**, and click **Add domain**. The wizard walks **Enter**, **Setup**, **Verify**. On the **Enter** step a root domain asks you to pick a **Primary address**, either the apex or `www`. + + + + Open the Space, go to **Domains**, and click **Add domain**. The wizard walks **Enter**, **Setup**, **Verify**. On the **Enter** step a root domain asks you to pick a **Primary address**, either the apex or `www`. + + + Over the API it is `POST /v1/domains` to create the record, then `PATCH /v1/domains/{domainId}` with a `spaceId` to assign it. See the [API reference](/api/reference). @@ -38,11 +47,22 @@ A domain belongs to a **team**, not to a Space. You add it once to the team's in + + + ```bash sf domains add example.com --space my-site --role primary ``` - Or in the dashboard, edit the row on the Space's **Domains** page and choose **Serve this space** with **Primary domain, the live URL**. Once the domain reaches `active`, it becomes the Space's `liveUrl`. + + + + Edit the row on the Space's **Domains** page and choose **Serve this space** with **Primary domain, the live URL**. + + + + + Once the domain reaches `active`, it becomes the Space's `liveUrl`. diff --git a/content/index.mdx b/content/index.mdx index 27150779..168623af 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -7,6 +7,8 @@ sidebar: Spacefast hosts the sites people build with AI agents. You hand it a folder, it hands back a URL. Everything you can do in the dashboard you can also do from the `sf` CLI, the HTTP API, or an MCP server. That means the agent that built the site can publish it too. +Prefer a browser to a terminal? Drag a folder onto the dashboard instead — no install, no account required. See [Drop](/publish#publish). + ## Publish in one command ```bash @@ -45,6 +47,9 @@ Static output is the default path. Point `sf publish` at a build directory and i Install, sign in, publish, and attach a domain in five minutes. + + Drag a folder onto the dashboard. No install, no account required. + What gets uploaded, how versions work, and how to roll back. diff --git a/content/quickstart.mdx b/content/quickstart.mdx index 1468c0ce..d65d454b 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -11,6 +11,8 @@ By the end of this page you have a site live on a `view.fast` hostname — and a You need Node.js 20.3 or newer and a folder of built static files. Any framework's output directory works (`dist`, `out`, `build`, `public`). If you only have source, `sf publish` can build it for you, see [Frameworks and builds](/frameworks). +Don't want to install anything? Drag a folder onto the dashboard instead — see [Drop](/publish#publish). + From 3251ae931777bf38d4ba3151e7487e85abe6fcd6 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 15:28:04 -0400 Subject: [PATCH 05/20] Remove the templated 'After this page...' opener site-wide Every page's opening sentence used the same learning-objectives frame ('After this page you can/know X' / 'By the end of this page you have Y') verbatim, including the 11 pages where it was buried mid-paragraph rather than as the literal first words. That's a recognizable AI-writing tell: generic scaffolding repeated identically 64 times regardless of what's actually on the page, which no Vale rule catches since it's a structural pattern, not a banned word. Replaces each opener with a direct statement of the page's key fact, deliberately varied in construction (you-voiced, mechanism-as-fact, gerund openers, etc.) so the 64 sentences stop reading as one template. Fixed two Vale regressions this pass introduced: added 'subcommand' to spelling-exceptions.txt (the plural was already accepted) and reworded a 'just as easily' that tripped Condescension. Documents the pattern in STYLE_GUIDE.md so it doesn't drift back. Co-Authored-By: Claude Sonnet 5 --- STYLE_GUIDE.md | 34 ++++++++++++++++++++ content/(account)/api-keys.mdx | 2 +- content/(account)/authentication.mdx | 2 +- content/(concepts)/spaces.mdx | 2 +- content/(concepts)/teams.mdx | 2 +- content/(concepts)/versions.mdx | 2 +- content/(dynamic)/crons.mdx | 2 +- content/(dynamic)/database.mdx | 2 +- content/(dynamic)/environment-variables.mdx | 2 +- content/(dynamic)/functions.mdx | 2 +- content/(dynamic)/logs.mdx | 2 +- content/(dynamic)/storage.mdx | 2 +- content/(dynamic)/wordpress.mdx | 2 +- content/(dynamic)/zero-runtime.mdx | 2 +- content/(publish)/anonymous-and-claim.mdx | 2 +- content/(publish)/ci.mdx | 2 +- content/(publish)/frameworks.mdx | 2 +- content/(publish)/git.mdx | 2 +- content/(publish)/publish.mdx | 2 +- content/(publish)/wordpress-data-sources.mdx | 2 +- content/(serve)/access.mdx | 2 +- content/(serve)/caching.mdx | 2 +- content/(serve)/customization.mdx | 2 +- content/(serve)/domains.mdx | 2 +- content/(serve)/routing.mdx | 2 +- content/(serve)/site-pages.mdx | 2 +- content/(serve)/stats.mdx | 2 +- content/(serve)/urls.mdx | 2 +- content/agents/claude-code.mdx | 2 +- content/agents/claude-desktop.mdx | 2 +- content/agents/codex.mdx | 2 +- content/agents/cursor.mdx | 2 +- content/agents/mcp-server.mdx | 2 +- content/agents/other-clients.mdx | 2 +- content/agents/permissions.mdx | 2 +- content/agents/sf-setup.mdx | 2 +- content/agents/skills.mdx | 2 +- content/api/authentication.mdx | 2 +- content/api/errors.mdx | 2 +- content/api/idempotency.mdx | 2 +- content/api/index.mdx | 2 +- content/api/operations.mdx | 2 +- content/api/pagination.mdx | 2 +- content/api/rate-limits.mdx | 2 +- content/api/sdk.mdx | 2 +- content/api/webhooks.mdx | 2 +- content/cli/agent-commands.mdx | 2 +- content/cli/agents.mdx | 2 +- content/cli/api-keys.mdx | 2 +- content/cli/builds.mdx | 2 +- content/cli/db.mdx | 2 +- content/cli/domains.mdx | 2 +- content/cli/env.mdx | 2 +- content/cli/git.mdx | 2 +- content/cli/login.mdx | 2 +- content/cli/project.mdx | 2 +- content/cli/publish.mdx | 2 +- content/cli/share.mdx | 2 +- content/cli/source.mdx | 2 +- content/cli/spaces.mdx | 2 +- content/cli/storage.mdx | 2 +- content/cli/teams.mdx | 2 +- content/cli/versions.mdx | 2 +- content/cli/zero.mdx | 2 +- content/quickstart.mdx | 2 +- styles/Spacefast/spelling-exceptions.txt | 1 + 66 files changed, 99 insertions(+), 64 deletions(-) diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 2c8cdc9d..783e81e0 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -74,6 +74,40 @@ Another: > without downtime." (14 words — the credential-prefix table further down > the page already covers the dropped clause.) +## Don't template the opening sentence + +The example above still has a problem, caught in a later pass: "After this +page you can/know X" and "By the end of this page you have Y" are +templates — the same framing device, repeated verbatim as the literal first +move on every page. That's a recognizable AI-writing tell, not a style +choice: it's the generic "learning objectives" scaffold a model defaults to +when it has to open a doc page without judging what's actually most +interesting about *that* page. A human varies the opening move page to +page. No Vale rule catches this — `AISpeak.yml` bans specific filler words, +not repeated sentence-level structures, so a templated opener can pass +every mechanical check and still read as generic. + +Open with the fact itself, not a sentence announcing that a fact is coming: + +> Templated: "After this page you can mint a scoped API key and rotate it +> without downtime." +> +> Direct: "You can mint a scoped API key with exactly the permissions it +> needs, then rotate it without downtime." + +> Templated: "After this page you know whether to publish your own build +> output or let Spacefast build it." +> +> Direct: "Publish your own build output directly, or hand Spacefast the +> source and let it build — the logs tell you which one went wrong if it +> fails." + +Vary the construction — "you can," the mechanism stated as fact, a gerund +opener ("Attaching a custom domain is..."), whatever fits that page — and +don't let the opener become a word-for-word echo of the page's frontmatter +`description` either; say the same thing in different words, or pick a +different angle entirely. + ## Banned words and phrases Vale errors on these. They're not style preferences — they're specific, diff --git a/content/(account)/api-keys.mdx b/content/(account)/api-keys.mdx index d66a2496..e7fa65ef 100644 --- a/content/(account)/api-keys.mdx +++ b/content/(account)/api-keys.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you can mint a scoped API key and rotate it without downtime. +You can mint a scoped API key with exactly the permissions it needs, then rotate it without taking anything down. ## Create an API key diff --git a/content/(account)/authentication.mdx b/content/(account)/authentication.mdx index 0de871eb..841f1f49 100644 --- a/content/(account)/authentication.mdx +++ b/content/(account)/authentication.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can create an account, sign in the way that suits you, and lock it down with two-factor. You'll also log the `sf` CLI in from a terminal. +Creating a Spacefast account takes nothing but an email address and a six-digit code — no password required until you add one later. ## Create an account diff --git a/content/(concepts)/spaces.mdx b/content/(concepts)/spaces.mdx index f8e87fad..802aa321 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can pick a slug the API will accept — and know exactly what a rename or delete does to links you've already shared. +A Space's slug has to pass a strict set of validation rules before the API accepts it — and it becomes part of every hostname that Space answers on. ## What a Space is diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index b1ea99e4..b69c676c 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you know what each role can do and how to invite people to your team. +A team owns everything — Spaces, domains, API keys, billing, members — and the role you hold there caps what you can do with any of it. ## What a team is diff --git a/content/(concepts)/versions.mdx b/content/(concepts)/versions.mdx index 0fe5fafa..252b5208 100644 --- a/content/(concepts)/versions.mdx +++ b/content/(concepts)/versions.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you know how the `live` pointer works — and how to roll back to any earlier version in seconds. +A rollback doesn't rebuild anything — it just repoints `live` at a version that already exists, which is why it takes seconds, not minutes. ## A version is a snapshot diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index 072df32e..bde3533f 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -After this page you can schedule recurring work and write a valid schedule the first time. +There's no dashboard editor for crons — you declare schedules entirely in `sf.jsonc`, and they only take effect when you publish. ## How a run happens diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index 73b46b9a..bd12f7ea 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you can inspect your space's database from the CLI and apply a migration safely. +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. ## What it is diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index e9a3b91f..4dcd0625 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can set a variable from the CLI or dashboard — and know when it actually reaches your site. +You can scope a variable to a single Space or to the whole team, and a Space-level value always wins over a team one with the same name. ## Two scopes diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index ce269161..5341fec1 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can add a worker to a Space and write a handler with the right signature. +Detection decides your runtime: Spacefast scans your project tree and picks the first layout it recognizes, so most projects need no `runtime` block at all. 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. diff --git a/content/(dynamic)/logs.mdx b/content/(dynamic)/logs.mdx index bf0babc1..40737cbc 100644 --- a/content/(dynamic)/logs.mdx +++ b/content/(dynamic)/logs.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -After this page you can tail your logs and find every line one request wrote. +`sf logs --follow` doesn't open a websocket — it polls every two seconds and only prints what it hasn't shown you yet. ## Three streams diff --git a/content/(dynamic)/storage.mdx b/content/(dynamic)/storage.mdx index 1714246d..ef6c019e 100644 --- a/content/(dynamic)/storage.mdx +++ b/content/(dynamic)/storage.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -After this page you can accept a file upload in a running app and get its URL. +Storage holds the files your running app uploads at runtime — separate from what you publish — and each object is addressed by nothing but a 32-character id. ## What it is diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index 5ee45e14..b5b4ffe6 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -3,7 +3,7 @@ title: WordPress on every Space description: Every Space runs a WordPress behind its static files. Run WP-CLI against it, call its Abilities with a short-lived token, and open its database. --- -After this page you can run any WP-CLI command against a Space — and call its Abilities with a token you mint from the API. +Run `sf wp` against a Space's WordPress and you won't see what the command printed — only whether it succeeded, unless you add `--local`. Every Space is backed by a WordPress install. Your published files serve in front of it, 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](/storage) is its media library, and [Database](/database) is its MySQL. This page covers the two ways to reach that WordPress directly. diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 79a01fdb..1e3d490e 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -5,7 +5,7 @@ sidebar: order: 0 --- -After this page you can tell whether your project needs Zero and declare it in `sf.jsonc`. +Every account gets Zero for free, but it only turns on when your `sf.jsonc` explicitly declares `kind: "zero"`. ## What Zero is diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index bf3b68f4..42e3bbe3 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can put a site online with no signup — and claim it later without losing the URL. +You can publish with no account at all; the CLI creates a Space keyed to a secret, which you can claim into a team later without changing its URL. ## Publish with no login diff --git a/content/(publish)/ci.mdx b/content/(publish)/ci.mdx index 92290d17..3c2c523d 100644 --- a/content/(publish)/ci.mdx +++ b/content/(publish)/ci.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -After this page your pipeline publishes automatically on every push — and ships a preview version for pull requests without touching live traffic. +A CI pipeline can publish automatically on every push to main, and ship pull requests as preview versions that never touch live traffic. ## Set it up diff --git a/content/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index 83fdcfbc..d3f960bf 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you know whether to publish your own build output or let Spacefast build it — and where to look when a build fails. +Publish your own build output directly, or hand Spacefast the source and let it build — the logs tell you which one went wrong if it fails. ## Two paths diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 16af2dbe..33748019 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -After this page you can publish straight from `git push`, or connect a GitHub repository so every push builds and publishes on its own. +Push to a Spacefast remote and `git push` becomes a publish, or connect a GitHub repository so every push builds and publishes on its own. Two lanes. The Spacefast remote needs no external host and no app install. A GitHub connection keeps your repository where it is and reacts to its webhooks. diff --git a/content/(publish)/publish.mdx b/content/(publish)/publish.mdx index 587d8b5c..8df147d1 100644 --- a/content/(publish)/publish.mdx +++ b/content/(publish)/publish.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -After this page you can publish a folder from the CLI, dashboard, or API — and read the receipt well enough to know whether your site is live. +A folder can be published from the CLI, dashboard, or API, and the receipt it returns tells you whether the site is live. ## The mental model diff --git a/content/(publish)/wordpress-data-sources.mdx b/content/(publish)/wordpress-data-sources.mdx index af721b8a..21b58309 100644 --- a/content/(publish)/wordpress-data-sources.mdx +++ b/content/(publish)/wordpress-data-sources.mdx @@ -3,7 +3,7 @@ title: Build from a WordPress site description: Point a Space's repository build at a public WordPress site, read its content over the REST API during the build, and publish the static output --- -After this page you can point a repository build at a public WordPress site and read its content while the build runs. +A repository build can point at a public WordPress site and read its content live, over the REST API, while the build runs. A data source is a public WordPress site a build reads content from. Nothing is copied into the Space and nothing is written back. The platform hands the build a URL and a source map. Your build code fetches over the WordPress REST API, and the output publishes like any other static build. diff --git a/content/(serve)/access.mdx b/content/(serve)/access.mdx index 5611bc59..eeb5c157 100644 --- a/content/(serve)/access.mdx +++ b/content/(serve)/access.mdx @@ -4,7 +4,7 @@ description: Make a Space public, hand out scoped share links or a password, inv sidebar: { order: 5 } --- -After this page you can decide exactly who opens a Space, down to one shared link or password, and revoke any of it instantly. +A Space's access is built from additive grants, so you can scope who gets in down to one shared link or password, and revoke any of it instantly. ## Access is a list, not a switch diff --git a/content/(serve)/caching.mdx b/content/(serve)/caching.mdx index 01dec588..2963caf7 100644 --- a/content/(serve)/caching.mdx +++ b/content/(serve)/caching.mdx @@ -4,7 +4,7 @@ description: The two cache policies a published Space sends, which files get whi sidebar: { order: 4 } --- -After this page you know exactly which `Cache-Control` header each file in your Space gets — and how to force a fresh response when you need one. +Every file in a published Space gets one of exactly two `Cache-Control` headers, and republishing is how you force a fresh response. ## Two policies diff --git a/content/(serve)/customization.mdx b/content/(serve)/customization.mdx index 173b03e7..e262e3be 100644 --- a/content/(serve)/customization.mdx +++ b/content/(serve)/customization.mdx @@ -3,7 +3,7 @@ title: Customization description: Theme the pages Spacefast draws, set the title and image links unfurl with, add an analytics tag, and run one site-wide script --- -After this page you can brand the pages Spacefast draws for your Space, and control how a link to it unfurls when shared. +You can brand the pages Spacefast draws for your Space, from logo and accent color to how a shared link unfurls. Open the Space and go to **Customization**. Four tabs, in order: **Theme**, **Search & sharing**, **Google Analytics**, and **Custom JS**. A large live preview sits beside the controls, labelled **Visitor sees**, painted from your unsaved draft. diff --git a/content/(serve)/domains.mdx b/content/(serve)/domains.mdx index 868dedf3..05503126 100644 --- a/content/(serve)/domains.mdx +++ b/content/(serve)/domains.mdx @@ -4,7 +4,7 @@ description: Connect a domain you own, add the DNS records Spacefast asks for, g sidebar: { order: 2 } --- -After this page you can point a domain you own at a Space, and make it the Space's primary, live address. +A domain you own can be pointed at a Space and promoted to its primary, live address. A domain belongs to a **team**, not to a Space. You add it once to the team's inventory, then assign it to a Space. One hostname serves exactly one Space; one Space can hold many hostnames. diff --git a/content/(serve)/routing.mdx b/content/(serve)/routing.mdx index 86e5eec1..4ca37efc 100644 --- a/content/(serve)/routing.mdx +++ b/content/(serve)/routing.mdx @@ -4,7 +4,7 @@ description: The _redirects and _headers files, the routing rules in sf.jsonc, h sidebar: { order: 3 } --- -After this page you can redirect, rewrite, proxy, and set response headers on a published Space — and know which rule wins when two overlap. +You can redirect, rewrite, proxy, and set response headers on a published Space, and when two rules overlap the first match always wins. Nothing here is evaluated per request. Spacefast compiles your rules at publish time into a table the edge reads, so a rule that behaves unexpectedly is almost always a compile-time problem. `sf routing inspect` shows you the compiled result before you publish. diff --git a/content/(serve)/site-pages.mdx b/content/(serve)/site-pages.mdx index 1feeed40..0f637205 100644 --- a/content/(serve)/site-pages.mdx +++ b/content/(serve)/site-pages.mdx @@ -3,7 +3,7 @@ title: Layout, theme, and site pages description: Theme the pages Spacefast draws with theme.json, wrap them in your own _layout.html, or take one over completely with _pages/id.html --- -After this page you know three ways to make Spacefast's built-in pages your own — from design tokens up to full page takeovers. +Spacefast's built-in pages can be made your own three ways, from design tokens up to a full page takeover. Spacefast draws five pages for a Space that has not written its own. Three levels of control, in increasing order of how much you own: diff --git a/content/(serve)/stats.mdx b/content/(serve)/stats.mdx index 651e06f6..a3661cfc 100644 --- a/content/(serve)/stats.mdx +++ b/content/(serve)/stats.mdx @@ -3,7 +3,7 @@ title: Traffic stats description: Requests, views, unique visitors, and top paths for a Space, where the numbers come from, and how to read them from the CLI and the API --- -After this page you know what each of a Space's traffic numbers counts — and what it deliberately leaves out. +Requests, views, and unique visitors each count something different on a Space's stats page, and crawler traffic is deliberately left out. ## Where to look diff --git a/content/(serve)/urls.mdx b/content/(serve)/urls.mdx index bfdbb75b..4ef89130 100644 --- a/content/(serve)/urls.mdx +++ b/content/(serve)/urls.mdx @@ -4,7 +4,7 @@ description: Every hostname a Space answers on, which one counts as the live URL sidebar: { order: 1 } --- -After this page you know every hostname a Space answers on, and which one counts as the live URL. +Every Space answers on several hostnames at once, but only one of them counts as the live URL. ## The default hostname diff --git a/content/agents/claude-code.mdx b/content/agents/claude-code.mdx index 6a3a1e74..4e0f80ec 100644 --- a/content/agents/claude-code.mdx +++ b/content/agents/claude-code.mdx @@ -3,7 +3,7 @@ title: Claude Code description: Install the Spacefast plugin in Claude Code, what it adds to your session, and the prompts that work --- -After this page Claude Code can publish your project to Spacefast and manage your account — without leaving the terminal. +The Spacefast plugin lets Claude Code publish your project and manage your account without leaving the terminal. ## Install diff --git a/content/agents/claude-desktop.mdx b/content/agents/claude-desktop.mdx index 5aabf557..ccb4f7d4 100644 --- a/content/agents/claude-desktop.mdx +++ b/content/agents/claude-desktop.mdx @@ -3,7 +3,7 @@ title: Claude Desktop description: Install the Spacefast desktop extension for Claude Desktop, or paste the hosted MCP server into your config --- -After this page Claude Desktop can publish files to Spacefast and read your Spaces. +The Spacefast desktop extension lets Claude Desktop publish files and read your Spaces directly. ## Install the extension diff --git a/content/agents/codex.mdx b/content/agents/codex.mdx index 7808cae4..b04759c9 100644 --- a/content/agents/codex.mdx +++ b/content/agents/codex.mdx @@ -3,7 +3,7 @@ title: Codex description: Install the Spacefast plugin in Codex, or add the MCP server to config.toml by hand --- -After this page Codex can publish your project to Spacefast — and fall back to curl scripts when MCP isn't available. +Codex publishes your project to Spacefast through the plugin, and falls back to curl scripts when MCP isn't available. ## Install diff --git a/content/agents/cursor.mdx b/content/agents/cursor.mdx index 9ce174d5..c56aeaaa 100644 --- a/content/agents/cursor.mdx +++ b/content/agents/cursor.mdx @@ -3,7 +3,7 @@ title: Cursor description: Install the Spacefast plugin in Cursor, or add the MCP server to mcp.json by hand --- -After this page Cursor can publish the project you have open and run the rest of the Spacefast API through one sandboxed tool. +Cursor can publish the project you have open and reach the rest of the Spacefast API through one sandboxed tool. ## Install diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index f3ba9fb4..43923780 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -3,7 +3,7 @@ title: MCP server description: Connect to the Spacefast MCP server over hosted HTTP or local stdio, and use its five tools, execute sandbox, and approval model --- -After this page you can connect any MCP client to Spacefast and know exactly when a call needs your approval. +The Spacefast MCP server exposes five tools over hosted HTTP or local stdio, and pauses for your approval before anything risky runs. ## Two runtimes, one tool set diff --git a/content/agents/other-clients.mdx b/content/agents/other-clients.mdx index d12db7d1..cf99d601 100644 --- a/content/agents/other-clients.mdx +++ b/content/agents/other-clients.mdx @@ -3,7 +3,7 @@ title: Any MCP client description: Connect any MCP client to Spacefast over hosted HTTP or local stdio, and what sf mcp proxy does --- -After this page you can wire Spacefast into any MCP client that has no first-party plugin, and know what the CLI's default proxy lane actually does. +Any MCP client without a first-party plugin can still reach Spacefast over hosted HTTP, local stdio, or the CLI's proxy lane. ## Pick a transport diff --git a/content/agents/permissions.mdx b/content/agents/permissions.mdx index 8a867942..9f8fb77f 100644 --- a/content/agents/permissions.mdx +++ b/content/agents/permissions.mdx @@ -3,7 +3,7 @@ title: What agents are allowed to do description: How an agent gets access to your Spacefast account, what it can never do without you, and how approvals, handoffs, and revocation work --- -After this page you can decide how much access to hand an agent, and cut it off in one click when you're done. +You decide exactly how much access an agent holds, and can cut it off in one click whenever you're done. ## How an agent gets access diff --git a/content/agents/sf-setup.mdx b/content/agents/sf-setup.mdx index c89e9da3..744d1d4e 100644 --- a/content/agents/sf-setup.mdx +++ b/content/agents/sf-setup.mdx @@ -3,7 +3,7 @@ title: sf setup agent description: Configure MCP and install the Spacefast skill for every agent on your machine with one CLI command --- -After this page you can point every agent client on your machine at Spacefast with one command, and drive it non-interactively in a script. +One command points every agent client on your machine at Spacefast, and the same command runs non-interactively in a script. ## The command diff --git a/content/agents/skills.mdx b/content/agents/skills.mdx index 48f54324..cfb16ea6 100644 --- a/content/agents/skills.mdx +++ b/content/agents/skills.mdx @@ -3,7 +3,7 @@ title: Skills description: The Spacefast agent skill, what it tells your agent to do, and how to install or remove it with the sf CLI --- -After this page you know what the shipped skill instructs your agent to do, and how to install, check, or remove it. +The `spacefast` skill ships with every agent lane and routes publish, inspect, and manage requests to the right tool. ## One skill, called `spacefast` diff --git a/content/api/authentication.mdx b/content/api/authentication.mdx index 4b5ddcbc..4a12327c 100644 --- a/content/api/authentication.mdx +++ b/content/api/authentication.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -The API takes one header. After this page you know which credential to mint for your case, how to scope it, and why a valid token still gets a 403 on some routes. +The API takes one header, though which credential you mint, how you scope it, and why a valid token still gets a 403 on some routes depends on your case. ## The header diff --git a/content/api/errors.mdx b/content/api/errors.mdx index aa4fd957..90ced264 100644 --- a/content/api/errors.mdx +++ b/content/api/errors.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -Every failure from this API is an RFC 9457 problem document with a stable `code`. After this page you can branch on that code, point a user at the exact bad field, and decide whether to retry. +Every failure from this API is an RFC 9457 problem document with a stable `code`, letting you branch on that code, point a user at the exact bad field, and decide whether to retry. ## The shape diff --git a/content/api/idempotency.mdx b/content/api/idempotency.mdx index 12d117a3..7bb64dfe 100644 --- a/content/api/idempotency.mdx +++ b/content/api/idempotency.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -A dropped connection on a publish should not create two versions. After this page you can retry any `POST` and get one of two answers: the original result replayed, or a conflict saying the first attempt is still running. +A dropped connection on a publish should not create two versions, so retrying any `POST` gets one of two answers: the original result replayed, or a conflict saying the first attempt is still running. ## Send a key diff --git a/content/api/index.mdx b/content/api/index.mdx index 7e7695de..52f2792a 100644 --- a/content/api/index.mdx +++ b/content/api/index.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -Everything the dashboard and the `sf` CLI do runs through this API. After this page you can authenticate a request, read any success or failure body, and publish a folder to a live URL with four curl calls. +Everything the dashboard and the `sf` CLI do runs through this same API — authenticate a request, read any success or failure body, and publish a folder to a live URL with four curl calls. ## Base URL and versioning diff --git a/content/api/operations.mdx b/content/api/operations.mdx index f7489760..e8e6bee6 100644 --- a/content/api/operations.mdx +++ b/content/api/operations.mdx @@ -5,7 +5,7 @@ sidebar: order: 7 --- -Publishing, promoting, and domain changes can take longer than one HTTP request. After this page you know when the API waits for you, when it hands back a handle instead, and how to follow that handle to a terminal state. +Publishing, promoting, and domain changes can take longer than one HTTP request, so the API either waits for you or hands back a handle for you to follow to a terminal state. ## Waiting is the default diff --git a/content/api/pagination.mdx b/content/api/pagination.mdx index 2e6a771d..0fa3516a 100644 --- a/content/api/pagination.mdx +++ b/content/api/pagination.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -Every list in this API pages the same way. After this page you can walk a collection to the end without writing a special case for any endpoint. +Every list in this API pages the same way, so you can walk a collection to the end without writing a special case for any endpoint. ## Request diff --git a/content/api/rate-limits.mdx b/content/api/rate-limits.mdx index 65fb279a..91475c87 100644 --- a/content/api/rate-limits.mdx +++ b/content/api/rate-limits.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -Two layers of limits sit in front of the API: a general per-minute request budget, and hourly budgets on the expensive actions. After this page you can read the headers on any response and back off correctly when one trips. +Two layers of limits sit in front of the API: a general per-minute request budget, and hourly budgets on the expensive actions. Read the headers on any response to back off correctly when one trips. ## The general budget diff --git a/content/api/sdk.mdx b/content/api/sdk.mdx index 592afd96..02b2bd42 100644 --- a/content/api/sdk.mdx +++ b/content/api/sdk.mdx @@ -5,7 +5,7 @@ sidebar: order: 9 --- -`@spacefast/sdk` is a typed client generated from the same OpenAPI document that produces [the reference](/api/reference). After this page you can make any call in the API with path params, query params, request body, and response body all checked at compile time. +`@spacefast/sdk` is a typed client generated from the same OpenAPI document that produces [the reference](/api/reference), so every call in the API gets path params, query params, request body, and response body checked at compile time. ## Install diff --git a/content/api/webhooks.mdx b/content/api/webhooks.mdx index 39ab8630..3167203e 100644 --- a/content/api/webhooks.mdx +++ b/content/api/webhooks.mdx @@ -5,7 +5,7 @@ sidebar: order: 8 --- -Spacefast `POST`s a signed JSON body to your endpoint whenever something happens in your team. After this page you can register an endpoint, verify a signature correctly through a secret rotation, and know when a delivery has given up. +Spacefast `POST`s a signed JSON body to your endpoint whenever something happens in your team — register one here, verify its signature correctly through a secret rotation, and know when a delivery has given up. ## Create an endpoint diff --git a/content/cli/agent-commands.mdx b/content/cli/agent-commands.mdx index ee272ff0..85ac217b 100644 --- a/content/cli/agent-commands.mdx +++ b/content/cli/agent-commands.mdx @@ -5,7 +5,7 @@ sidebar: order: 27 --- -The commands on this page answer questions and finish jobs rather than change what is published. After this page you can look up any Space you can reach and pull a private route from the terminal. +The commands on this page answer questions and finish jobs rather than change what is published — look up any Space you can reach, or pull a private route straight from the terminal. Every command here also takes the [global flags](/cli#global-flags). diff --git a/content/cli/agents.mdx b/content/cli/agents.mdx index 3e8df15d..566359f7 100644 --- a/content/cli/agents.mdx +++ b/content/cli/agents.mdx @@ -5,7 +5,7 @@ sidebar: order: 26 --- -After this page you can point Claude Code, Cursor, Codex, or any other MCP client at Spacefast in one command. +One command points Claude Code, Cursor, Codex, or any other MCP client at Spacefast. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`). `sf mcp` and `sf mcp proxy` do not support `--json`, because stdout carries the MCP protocol. diff --git a/content/cli/api-keys.mdx b/content/cli/api-keys.mdx index 62297c34..6b5efba8 100644 --- a/content/cli/api-keys.mdx +++ b/content/cli/api-keys.mdx @@ -5,7 +5,7 @@ sidebar: order: 10 --- -API keys authenticate anything that cannot run a browser login: CI jobs, scripts, servers, agents. A key belongs to a team and carries the permissions of the preset you created it with. After this page you can mint one, see what exists, and turn one off. +API keys authenticate anything that cannot run a browser login: CI jobs, scripts, servers, agents. A key belongs to a team and carries the permissions of the preset you created it with, and the three subcommands below mint one, list what exists, and turn one off. All three subcommands need a login or a `--token` of their own, and they act on your default team unless you name another. Every command here also takes the [global flags](/cli#global-flags). diff --git a/content/cli/builds.mdx b/content/cli/builds.mdx index 0113b945..3447e50f 100644 --- a/content/cli/builds.mdx +++ b/content/cli/builds.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -After this page you can pack a build output archive locally, and drive every remote build a Space runs. +You can pack a build output archive locally, or drive every remote build a Space runs, from list to retry. A build is the thing that turns source into a version. `sf publish --remote` and repository pushes create builds; `sf build` runs the same build locally and leaves you an archive. See [Frameworks and builds](/frameworks) for detection and the build settings model. diff --git a/content/cli/db.mdx b/content/cli/db.mdx index c1f6a2e0..a816354f 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -5,7 +5,7 @@ sidebar: order: 22 --- -After this page you can inspect a Space's database, apply schema migrations, and fire a scheduled job on demand. +A Space's database can be inspected, migrated, and dumped without ever touching a connection string — there isn't one. You can also fire a declared cron job on demand instead of waiting for its schedule. 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/domains.mdx b/content/cli/domains.mdx index 657c4514..a63a735a 100644 --- a/content/cli/domains.mdx +++ b/content/cli/domains.mdx @@ -5,7 +5,7 @@ sidebar: order: 20 --- -After this page you can attach a custom domain to a Space and watch its DNS and TLS come up. +Attaching a custom domain to a Space is three API calls in one command, with DNS and TLS readiness tracked as they come up. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`) and, except `sf routing inspect`, the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli) for those. diff --git a/content/cli/env.mdx b/content/cli/env.mdx index fc8a0263..58847695 100644 --- a/content/cli/env.mdx +++ b/content/cli/env.mdx @@ -5,7 +5,7 @@ sidebar: order: 21 --- -After this page you can set a Space's variables without putting secrets in your shell history, and know exactly when changes take effect. +A Space's variables can be set without ever putting secrets in your shell history, and a write only takes effect when the next version finalizes. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). All except `sf env export-template` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). diff --git a/content/cli/git.mdx b/content/cli/git.mdx index b5e0c14e..b0f530a1 100644 --- a/content/cli/git.mdx +++ b/content/cli/git.mdx @@ -5,7 +5,7 @@ sidebar: order: 24 --- -After this page you can connect a GitHub repository to a Space and trigger a remote build from it. +You can connect a GitHub repository to a Space in one command, then trigger a remote build from it right away. 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/login.mdx b/content/cli/login.mdx index bbefab04..a6e677e6 100644 --- a/content/cli/login.mdx +++ b/content/cli/login.mdx @@ -5,7 +5,7 @@ sidebar: order: 8 --- -After this page you can sign this machine in through the browser, or with an API key when there's no browser. +Signing this machine in works through the browser, or with an API key when there's no browser to open. Every command here also takes the [global flags](/cli#global-flags). diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 09888e58..af87fa39 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -5,7 +5,7 @@ sidebar: order: 9 --- -After this page you can scaffold a project and link a directory to a Space so `sf publish` needs no flags. +Scaffolding a project and linking a directory to a Space means `sf publish` needs no flags afterward. Two files carry the link. `.spacefast/space.json` is the committable one: which Space, which team, which API base URL. `.spacefast/state.json` is the local cache. It holds credentials, so it's never committed; the CLI adds it to `.gitignore` for you. diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index f2c0e2a2..cdc788fa 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -After this page you can publish any folder, project, or archive to a Space with one command. `sf dev` is documented at the end because it runs a project locally, not a copy of what publish serves. +One command, `sf publish`, ships any folder, project, or archive to a Space. `sf dev` is documented at the end because it runs a project locally, not a copy of what publish serves. ## sf publish diff --git a/content/cli/share.mdx b/content/cli/share.mdx index a50abf6a..2c83c807 100644 --- a/content/cli/share.mdx +++ b/content/cli/share.mdx @@ -5,7 +5,7 @@ sidebar: order: 7 --- -After this page you can grant access to a Space with links, passwords, or machine tokens — and explain exactly why a visitor can or can't see a URL. +Access to a Space can be granted with links, passwords, or machine tokens. You can also explain exactly why a visitor can or can't see a URL. ## The model in one minute diff --git a/content/cli/source.mdx b/content/cli/source.mdx index e15f0a65..1eefb739 100644 --- a/content/cli/source.mdx +++ b/content/cli/source.mdx @@ -5,7 +5,7 @@ sidebar: order: 25 --- -After this page you can browse, search, and change a Space's source repository from the terminal — all without a local clone. +Every `sf source` command reads or writes a repository directly through the API, so there's nothing to clone, branch, or pull locally first. 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/spaces.mdx b/content/cli/spaces.mdx index 8f793a9a..3bd9b1d0 100644 --- a/content/cli/spaces.mdx +++ b/content/cli/spaces.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -After this page you can manage a Space's whole lifecycle from the terminal, from creation to transfer. +A Space's entire lifecycle, from creation to transfer, runs through this one set of terminal commands. A Space is one hosted site: a slug, a hostname, an owner, and a stack of versions. See [Spaces](/spaces) for the model. diff --git a/content/cli/storage.mdx b/content/cli/storage.mdx index 94248fb2..14b17b40 100644 --- a/content/cli/storage.mdx +++ b/content/cli/storage.mdx @@ -5,7 +5,7 @@ sidebar: order: 23 --- -After this page you can see what your app has stored in runtime storage, and read the logs your Space produces. +You can see what your app has stored in runtime storage, and read every log line your Space produces. 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/teams.mdx b/content/cli/teams.mdx index 9a88497c..d4d78cd4 100644 --- a/content/cli/teams.mdx +++ b/content/cli/teams.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -After this page you can create a team, invite members, and set the access new Spaces start with. +Creating a team, inviting members, and setting the access new Spaces start with are all one `sf teams` subcommand away. A team owns Spaces, domains, and billing. Every member holds one team role: `owner`, `admin`, or `member`. See [Teams](/teams) for the model, and [`sf share`](/cli/share) for per-Space access, which is a separate system. diff --git a/content/cli/versions.mdx b/content/cli/versions.mdx index 2dec5cfb..d664a58f 100644 --- a/content/cli/versions.mdx +++ b/content/cli/versions.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -After this page you can move live traffic to any version a Space has published, and roll back after a bad one. +Live traffic can be moved to any version a Space has published, and rolled back the same way after a bad one. Every publish creates one immutable version. A channel is a named pointer at one version. Only `live` exists today. Promoting and rolling back move that pointer; they never rebuild anything. See [Versions](/versions) for the model. diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index d921b155..70c7af6e 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -5,7 +5,7 @@ sidebar: order: 25 --- -After this page you can list and call what a Space's capsule exposes to agents — its Zero Abilities — and run any WP-CLI command against a Space. +A Space's capsule exposes a catalog of Zero Abilities that agents can list and call, and any WP-CLI command can run against a Space directly. 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). diff --git a/content/quickstart.mdx b/content/quickstart.mdx index d65d454b..ef4fb267 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -By the end of this page you have a site live on a `view.fast` hostname — and a custom domain pointed at it. +You can publish a folder of static files and get a live `view.fast` URL in one command, then point your own domain at it. ## Before you start diff --git a/styles/Spacefast/spelling-exceptions.txt b/styles/Spacefast/spelling-exceptions.txt index 23e2521a..1e664206 100644 --- a/styles/Spacefast/spelling-exceptions.txt +++ b/styles/Spacefast/spelling-exceptions.txt @@ -207,6 +207,7 @@ stderr stdin stdout streamable +subcommand subcommands subdomain subdomains From 0d740c1175aae62705906c96cf33651300d3e114 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 15:33:50 -0400 Subject: [PATCH 06/20] Review pass: fix regressions and inaccuracies Found while reviewing the full branch: - content/api/authentication.mdx and content/cli/api-keys.mdx: the opener de-templating pass had reintroduced a 3-clause run-on in the first and a vestigial "here's what this page covers" clause in the second. Both tightened. - STYLE_GUIDE.md: its own "be concise" worked examples still showed the old "After this page you know..." phrasing as the *good* example, directly contradicting the "don't template the opener" section two headings later. Updated both to the actual final wording. Also corrected four word counts that were eyeballed instead of machine-counted (31/33 words before, 27/18 after). Co-Authored-By: Claude Sonnet 5 --- STYLE_GUIDE.md | 20 ++++++++++++-------- content/api/authentication.mdx | 2 +- content/cli/api-keys.mdx | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 783e81e0..4b19d70d 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -57,22 +57,26 @@ Worked example, from the page-opener cleanup: > Before: "After this page you know which directory your framework > produces, when to publish that directory yourself versus letting > Spacefast build, how detection picks commands, and where to read a -> failing build." (33 words, 4 clauses) +> failing build." (31 words, 4 clauses) > -> After: "After this page you know whether to publish your own build -> output or let Spacefast build it — and where to look when a build -> fails." (25 words, 2 ideas joined by an em dash) +> After: "Publish your own build output directly, or hand Spacefast the +> source and let it build — the logs tell you which one went wrong if it +> fails." (27 words, 2 ideas joined by an em dash) Another: > Before: "After this page you can mint an API key with the right > permissions, use it against the API, rotate it without downtime, and -> recognize every other credential Spacefast hands you by its prefix." (29 +> recognize every other credential Spacefast hands you by its prefix." (33 > words, 4 clauses) > -> After: "After this page you can mint a scoped API key and rotate it -> without downtime." (14 words — the credential-prefix table further down -> the page already covers the dropped clause.) +> After: "You can mint a scoped API key with exactly the permissions it +> needs, then rotate it without downtime." (18 words — the credential-prefix +> table further down the page already covers the dropped clause.) + +(These two "after" versions also show the fix from the next section — no +"after this page" framing. The intermediate step, where the sentence was +merely shorter but still templated, is history now; don't resurrect it.) ## Don't template the opening sentence diff --git a/content/api/authentication.mdx b/content/api/authentication.mdx index 4a12327c..b27b23a0 100644 --- a/content/api/authentication.mdx +++ b/content/api/authentication.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -The API takes one header, though which credential you mint, how you scope it, and why a valid token still gets a 403 on some routes depends on your case. +The API takes one header, but a valid token can still get a 403 on some routes. ## The header diff --git a/content/cli/api-keys.mdx b/content/cli/api-keys.mdx index 6b5efba8..faf00ff0 100644 --- a/content/cli/api-keys.mdx +++ b/content/cli/api-keys.mdx @@ -5,7 +5,7 @@ sidebar: order: 10 --- -API keys authenticate anything that cannot run a browser login: CI jobs, scripts, servers, agents. A key belongs to a team and carries the permissions of the preset you created it with, and the three subcommands below mint one, list what exists, and turn one off. +API keys authenticate anything that cannot run a browser login: CI jobs, scripts, servers, agents. A key belongs to a team and carries the permissions of the preset you created it with. All three subcommands need a login or a `--token` of their own, and they act on your default team unless you name another. Every command here also takes the [global flags](/cli#global-flags). From ac44253bda83e89413228391ea5ed975eb102827 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Thu, 1 Oct 2026 15:48:12 -0400 Subject: [PATCH 07/20] Editorial pass: fix opener/description duplication on 9 pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught during review: several of the rewritten openers paraphrased the page's own frontmatter description so closely that the subtitle and the first sentence of the body read as the same content twice — e.g. frameworks.mdx's description and opener both opened "Publish your [framework's/own build] output directly, or hand Spacefast the source and let it..." Checked every page's description against its opener with a word-overlap script; found 9 with real duplication (vs. generic shared technical nouns, which are fine and expected). Replaced each with a genuinely distinct hook already stated later on the same page — the internal vocabulary quirk in git.mdx, the no-connection-string-equivalent "no public/private flag" fact in access.mdx, the delegation-token security detail in mcp-server.mdx, and similar for the rest — rather than a reworded restatement of the description. One page (platforms/index.mdx) has pre-existing overlap that predates this branch; left alone as out of scope. Co-Authored-By: Claude Sonnet 5 --- content/(publish)/anonymous-and-claim.mdx | 2 +- content/(publish)/frameworks.mdx | 2 +- content/(publish)/git.mdx | 2 +- content/(publish)/wordpress-data-sources.mdx | 2 +- content/(serve)/access.mdx | 2 +- content/(serve)/customization.mdx | 2 +- content/(serve)/urls.mdx | 2 +- content/agents/mcp-server.mdx | 2 +- content/agents/other-clients.mdx | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index 42e3bbe3..6b7beb91 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -You can publish with no account at all; the CLI creates a Space keyed to a secret, which you can claim into a team later without changing its URL. +There's exactly one copy of the key that lets you keep publishing — lose the folder you published from, and the Space runs out its clock with no way to get it back. ## Publish with no login diff --git a/content/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index d3f960bf..e378149d 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -Publish your own build output directly, or hand Spacefast the source and let it build — the logs tell you which one went wrong if it fails. +`--prebuilt` forces the upload path and `--build` forces the build path — but a `.tar.gz` archive always goes through a build, since archives are classified remotely. ## Two paths diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 33748019..0c65ac4a 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -Push to a Spacefast remote and `git push` becomes a publish, or connect a GitHub repository so every push builds and publishes on its own. +The `sf git` and `sf source` command families name the same two connection types in different words — on purpose, not a typo. Two lanes. The Spacefast remote needs no external host and no app install. A GitHub connection keeps your repository where it is and reacts to its webhooks. diff --git a/content/(publish)/wordpress-data-sources.mdx b/content/(publish)/wordpress-data-sources.mdx index 21b58309..6f777566 100644 --- a/content/(publish)/wordpress-data-sources.mdx +++ b/content/(publish)/wordpress-data-sources.mdx @@ -3,7 +3,7 @@ title: Build from a WordPress site description: Point a Space's repository build at a public WordPress site, read its content over the REST API during the build, and publish the static output --- -A repository build can point at a public WordPress site and read its content live, over the REST API, while the build runs. +Data sources only reach repository builds — a folder publish, or a local `sf publish` of a built directory, never sees them. A data source is a public WordPress site a build reads content from. Nothing is copied into the Space and nothing is written back. The platform hands the build a URL and a source map. Your build code fetches over the WordPress REST API, and the output publishes like any other static build. diff --git a/content/(serve)/access.mdx b/content/(serve)/access.mdx index eeb5c157..682758bb 100644 --- a/content/(serve)/access.mdx +++ b/content/(serve)/access.mdx @@ -4,7 +4,7 @@ description: Make a Space public, hand out scoped share links or a password, inv sidebar: { order: 5 } --- -A Space's access is built from additive grants, so you can scope who gets in down to one shared link or password, and revoke any of it instantly. +A Space has no public-or-private flag — access is a list of additive grants, and removing every one of them is what makes it private. ## Access is a list, not a switch diff --git a/content/(serve)/customization.mdx b/content/(serve)/customization.mdx index e262e3be..fdd33ac3 100644 --- a/content/(serve)/customization.mdx +++ b/content/(serve)/customization.mdx @@ -3,7 +3,7 @@ title: Customization description: Theme the pages Spacefast draws, set the title and image links unfurl with, add an analytics tag, and run one site-wide script --- -You can brand the pages Spacefast draws for your Space, from logo and accent color to how a shared link unfurls. +Everything you set here lives on the Space, not a version — it survives every publish, and it wins over whatever `theme` or `meta` your `sf.jsonc` declares. Open the Space and go to **Customization**. Four tabs, in order: **Theme**, **Search & sharing**, **Google Analytics**, and **Custom JS**. A large live preview sits beside the controls, labelled **Visitor sees**, painted from your unsaved draft. diff --git a/content/(serve)/urls.mdx b/content/(serve)/urls.mdx index 4ef89130..bf1ff580 100644 --- a/content/(serve)/urls.mdx +++ b/content/(serve)/urls.mdx @@ -4,7 +4,7 @@ description: Every hostname a Space answers on, which one counts as the live URL sidebar: { order: 1 } --- -Every Space answers on several hostnames at once, but only one of them counts as the live URL. +Adding a custom domain never retires a Space's default `view.fast` hostname — it keeps serving for the life of the Space. ## The default hostname diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index 43923780..8c86bd48 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -3,7 +3,7 @@ title: MCP server description: Connect to the Spacefast MCP server over hosted HTTP or local stdio, and use its five tools, execute sandbox, and approval model --- -The Spacefast MCP server exposes five tools over hosted HTTP or local stdio, and pauses for your approval before anything risky runs. +Your client never actually holds a Spacefast API token — each authenticated request mints a short-lived delegation token instead, so a leaked MCP session can't be replayed against the wider API. ## Two runtimes, one tool set diff --git a/content/agents/other-clients.mdx b/content/agents/other-clients.mdx index cf99d601..6986e592 100644 --- a/content/agents/other-clients.mdx +++ b/content/agents/other-clients.mdx @@ -3,7 +3,7 @@ title: Any MCP client description: Connect any MCP client to Spacefast over hosted HTTP or local stdio, and what sf mcp proxy does --- -Any MCP client without a first-party plugin can still reach Spacefast over hosted HTTP, local stdio, or the CLI's proxy lane. +Every MCP client names its config key differently — `mcp` for OpenCode, `servers` for VS Code, `context_servers` for Zed — so `sf setup agent` writes the right one for you. ## Pick a transport From 7f9631e1bd4cfe70ebb25444cda4e2531fb34b81 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Fri, 2 Oct 2026 09:16:37 -0400 Subject: [PATCH 08/20] Fix duplication my own fix introduced: opener vs. later body text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 8 fixed 9 pages where the opener paraphrased the frontmatter description. For several of them the replacement hook was a fact pulled from later in the same page, and in 9 cases (some overlapping with commit 8's list, some not: functions.mdx, mcp-server.mdx, other-clients.mdx, access.mdx, anonymous-and-claim.mdx, frameworks.mdx, git.mdx, wordpress-data-sources.mdx, urls.mdx) I used a sentence already present there almost verbatim, moving the duplication instead of removing it — e.g. frameworks.mdx's new opener was nearly identical to an existing sentence in its own "Two paths" section. Wrote a script comparing every opener against every later paragraph on the same page (not just the description) across all 85 pages. Found these 9 with real duplication (two more — database.mdx, api/idempotency.mdx — share a short technical phrase but serve different purposes: a forward reference and a bolded error-code signpost, both legitimate patterns used elsewhere in the corpus; left alone). Fixed each by trimming the now-redundant later sentence to keep only what it adds beyond the opener, rather than rewriting the opener a third time. Co-Authored-By: Claude Sonnet 5 --- content/(dynamic)/functions.mdx | 2 +- content/(publish)/anonymous-and-claim.mdx | 2 +- content/(publish)/frameworks.mdx | 2 +- content/(publish)/git.mdx | 2 +- content/(publish)/wordpress-data-sources.mdx | 4 +--- content/(serve)/access.mdx | 2 +- content/(serve)/urls.mdx | 2 -- content/agents/mcp-server.mdx | 2 +- content/agents/other-clients.mdx | 2 +- 9 files changed, 8 insertions(+), 12 deletions(-) diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index 5341fec1..494ce1b2 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -88,7 +88,7 @@ Requests and responses are the standard `Request` and `Response`. There is no Sp ## Declare it -Detection means most projects need no `runtime` block at all. Declare one when detection cannot guess the layout. +Declare a `runtime` block when detection cannot identify your layout or you need to specify `entry` or `compatibilityDate`. ```jsonc sf.jsonc { diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index 6b7beb91..23f923aa 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -30,7 +30,7 @@ https://swift-otter-4821.view.fast/__/sfc_Kd2mQ7xR4tYb9wLp1sVn3F https://my.spacefast.com/claim#sfc_Kd2mQ7xR4tYb9wLp1sVn3F ``` -The key is returned once, at creation, and once more if you rotate it. The CLI caches it in `.spacefast/state.json` in the directory you published from, which is why `sf publish` from that same folder keeps updating the same Space. Nothing else in Spacefast can tell you the key later. Lose the folder and lose the key, and the Space runs out its clock. +The key is returned once, at creation, and once more if you rotate it. The CLI caches it in `.spacefast/state.json` in the directory you published from, which is why `sf publish` from that same folder keeps updating the same Space. Nothing else in Spacefast can tell you the key later. In `--json` or non-interactive output the key and the claim link are masked. Pass `--show-secret` to print them. diff --git a/content/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index e378149d..dcf5eebc 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -22,7 +22,7 @@ sf publish ./dist sf publish --remote ``` -With no flags, `sf publish` builds a detected project, and publishes the directory as-is if there is nothing to build. `--prebuilt` forces the upload path. `--build` forces the build path. Archives are always classified remotely, so a `.tar.gz` is always a build. +With no flags, `sf publish` builds a detected project, and publishes the directory as-is if there is nothing to build. ## Which directory to publish diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 0c65ac4a..e87586fe 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -162,7 +162,7 @@ sf git github repos --space docs ## Two connection-type vocabularies -The `sf git *` commands and the `sf source *` commands name the same two connection kinds differently. This is real, not a typo. +The exact values each command family uses: | Command family | Values | Meaning | | --- | --- | --- | diff --git a/content/(publish)/wordpress-data-sources.mdx b/content/(publish)/wordpress-data-sources.mdx index 6f777566..e4e7a511 100644 --- a/content/(publish)/wordpress-data-sources.mdx +++ b/content/(publish)/wordpress-data-sources.mdx @@ -9,9 +9,7 @@ A data source is a public WordPress site a build reads content from. Nothing is ## What you need first -Data sources reach **repository builds only**. A folder publish never sees them, and neither does a local `sf publish` of a built directory. - -So a Space needs a connected repository before a source does anything. See [Publish from Git](/git). Until one is connected, the dashboard shows **Connect a repository to use data sources** with a **Connect repository** button, and saved sources sit there doing nothing. Once it is connected the banner reads **Ready for repository builds**. +A Space needs a connected repository before a source does anything — see [Publish from Git](/git). Until one is connected, the dashboard shows **Connect a repository to use data sources** with a **Connect repository** button, and saved sources sit there doing nothing. Once it is connected the banner reads **Ready for repository builds**. Sources live on the Space, not in `sf.jsonc`. `dataSources` and `defaultDataSource` are not keys of the committed config file, and putting them there does nothing. A build's content source is a deployment fact, so it is set through the dashboard or the API. diff --git a/content/(serve)/access.mdx b/content/(serve)/access.mdx index 682758bb..71ab91ce 100644 --- a/content/(serve)/access.mdx +++ b/content/(serve)/access.mdx @@ -8,7 +8,7 @@ A Space has no public-or-private flag — access is a list of additive grants, a ## Access is a list, not a switch -A Space has no public or private flag. Access is a set of additive **grants**, each one naming an audience, a set of capabilities, the paths it covers, and which published target it applies to. A visitor is admitted when at least one grant matches. Removing every grant makes a Space private, because nothing admits anyone. +Access is a set of additive **grants**, each one naming an audience, a set of capabilities, the paths it covers, and which published target it applies to. A visitor is admitted when at least one grant matches, and nothing admits anyone once every grant is gone. That model is why a Space can be public at `/docs/**` and closed everywhere else, and why revoking a share link takes effect on the next request rather than at the next publish. diff --git a/content/(serve)/urls.mdx b/content/(serve)/urls.mdx index bf1ff580..2efd2aca 100644 --- a/content/(serve)/urls.mdx +++ b/content/(serve)/urls.mdx @@ -16,8 +16,6 @@ https://{label}.view.fast/ The label comes from the Space slug, lowercased, with every character outside `a-z`, `0-9` and `-` replaced by `-`, runs of hyphens collapsed, and a 63-character cap. A Space with slug `my-site` serves at `https://my-site.view.fast/`. -The default hostname keeps serving for the life of the Space. Adding a custom domain never retires it. - A slug cannot contain `--`, because `--` is the separator between a version or branch label and the Space label in the hostnames below. ## Version URLs diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index 8c86bd48..6f20cecd 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -54,7 +54,7 @@ Each tool declares the scopes it needs, and clients can read them from `tools/li A scope shortfall applies to the requested operation. A credential that can read analytics may still lack access to domains. -Your client never receives a Spacefast API token. Each authenticated request mints an internal delegation token that lives five minutes and carries the source credential's policy verbatim. That means a leaked MCP session cannot be replayed against the wider API. +That delegation token lives five minutes and carries the source credential's policy verbatim. Hosted requests are rate limited to 600 per minute per credential and 300 per minute per IP. diff --git a/content/agents/other-clients.mdx b/content/agents/other-clients.mdx index 6986e592..764d5ae8 100644 --- a/content/agents/other-clients.mdx +++ b/content/agents/other-clients.mdx @@ -59,7 +59,7 @@ Every MCP client names its config key differently — `mcp` for OpenCode, `serve -Some clients use different key names. `mcp` for OpenCode, `servers` for VS Code, `context_servers` for Zed, and Amp puts the server map at the top level with no root key at all. `sf setup agent` knows each dialect, so let it write the file when you can. +Amp goes further still, putting the server map at the top level with no root key at all. `sf setup agent` knows every dialect, so let it write the file when you can. Ask the agent to find a documentation page through `execute`. It should search for the documentation operation, describe it, and call it before attempting a publish. From c037d3f0d877f080f033bbe6d5ced3309709b3db Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Fri, 2 Oct 2026 10:32:31 -0400 Subject: [PATCH 09/20] Act on persona review: glossary, Troubleshooting on-ramp, agent-tab cliff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A subagent read the whole site in character as a non-technical, eager-but-impatient new user and reported back critically. Checked every one of its "duplicative" claims against the actual content and house policy (brief, contextual, cross-linked restatement is this repo's own documented convention, not a defect) — none held up, so no changes there. Four other findings did hold up: - New content/(reference)/glossary.mdx: Ability, Build, Capsule, Claim, Drop, Functions, Grant, Live, Space, Team, Version, Zero — every term that piles up starting on the homepage, defined once, cross-linked to its canonical deep page. Linked from the homepage (card + inline pointer) and added to the Reference nav. - content/troubleshooting.mdx: added a plain-language "Not sure where to start?" note pointing at the dashboard's Overview page, before the error-code reference begins — it was the first page flagged as "not written for someone like me," on only the third page of the site. - content/agents/mcp-server.mdx: added a note flagging that the page is technical reference, with a pointer back to the one-click Claude Desktop/Claude Code setup — the agent hit this as an unmarked jump from no-code to developer-only one click deep into the Agents tab. - content/quickstart.mdx: enriched the one-line Drop pointer (what you can drag, no account needed) rather than creating a new page, which AGENTS.md's route policy ("one page per task, not one page per toggle") argues against — Drop is the dashboard interface to the same publish task Quickstart covers for the CLI, not a separate task. Co-Authored-By: Claude Sonnet 5 --- content/(reference)/glossary.mdx | 56 ++++++++++++++++++++++++++++++++ content/(reference)/meta.ts | 2 +- content/agents/mcp-server.mdx | 4 +++ content/index.mdx | 5 ++- content/quickstart.mdx | 2 +- content/troubleshooting.mdx | 4 +++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 content/(reference)/glossary.mdx diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx new file mode 100644 index 00000000..d9acca3e --- /dev/null +++ b/content/(reference)/glossary.mdx @@ -0,0 +1,56 @@ +--- +title: Glossary +description: Every Spacefast-specific term used across these docs, defined once, in one place +sidebar: + order: 3 +--- + +The terms below show up starting on the homepage and get used without re-explanation everywhere after. If a word stopped you somewhere else in these docs, it's probably here. + +## Ability + +A named operation with a schema that a Space's WordPress publishes for agents to call. Some ship with the runtime itself and exist on every Space; others come from an app you publish. See [WordPress on every Space](/wordpress). + +## Build + +What happens when you hand Spacefast source code instead of a built output directory: it installs dependencies, runs your framework's build command in a sandbox, and publishes the result as a version. 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). + +## Drop + +The dashboard's drag-and-drop publish flow: drag a folder, a `.zip`, or a single `index.html` onto the window. No install, no account required. See [Publishing](/publish#publish). + +## Functions + +The runtime for shipping a worker alongside your site — npm packages, framework server output, outbound HTTP. The other option, alongside [Zero](#zero), for sites that need more than static files. See [Functions](/functions). + +## Grant + +One rule in a Space's access list: it names an audience (public, team, one person, a link, a password, a machine, or an external identity) and what that audience can do. A Space has no public-or-private flag — its access is just the sum of its grants. See [Access and sharing](/access). + +## Live (the live channel) + +The pointer that decides which version a Space actually serves right now. Publishing, promoting, and rolling back all just move this pointer — nothing gets rebuilt. See [Versions and the live channel](/versions). + +## Space + +One published site with a stable hostname. Everything else in these docs — versions, domains, access, functions — belongs to a Space. See [Spaces](/spaces). + +## Team + +The thing that owns your Spaces, domains, API keys, and billing. Every claimed Space belongs to exactly one team, even if you're its only member. See [Teams and members](/teams). + +## Version + +An immutable snapshot created by every publish, with its own permanent URL that never changes. The [live](#live-the-live-channel) pointer always points at exactly one version. See [Versions and the live channel](/versions). + +## Zero + +The runtime for a Space that needs a database, live queries, or scheduled jobs: one TypeScript app, a MySQL database next to your code, and HTTP endpoints, compiled into a [capsule](#capsule). The other dynamic option, alongside [Functions](#functions). See [Dynamic sites with Zero](/zero-runtime). diff --git a/content/(reference)/meta.ts b/content/(reference)/meta.ts index db8f51b3..7745cf16 100644 --- a/content/(reference)/meta.ts +++ b/content/(reference)/meta.ts @@ -4,5 +4,5 @@ export default defineMeta({ title: "Reference", collapsed: false, order: 7, - pages: ["limits", "config-file", "errors"], + pages: ["glossary", "limits", "config-file", "errors"], }); diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index 6f20cecd..7721d67e 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -5,6 +5,10 @@ description: Connect to the Spacefast MCP server over hosted HTTP or local stdio Your client never actually holds a Spacefast API token — each authenticated request mints a short-lived delegation token instead, so a leaked MCP session can't be replayed against the wider API. +:::note[Already connected through Claude Desktop or a one-click plugin?] +You don't need this page. It's the technical reference for wiring up the MCP server by hand — see [Claude Desktop](/agents/claude-desktop) or [Claude Code](/agents/claude-code) for the point-and-click setup instead. +::: + ## Two runtimes, one tool set The same server ships in two runtimes. Both register the same five tools. The difference is the filesystem. diff --git a/content/index.mdx b/content/index.mdx index 168623af..0991633e 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -39,7 +39,7 @@ Publish this folder to Spacefast and give me the live URL. ## How it fits together -A **Space** is one site with a stable hostname. Every publish creates a new **version**, an immutable snapshot with its own URL that never changes. The `live` channel points at one version. Promoting or rolling back moves that pointer. Nothing gets rebuilt. +A **Space** is one site with a stable hostname. Every publish creates a new **version**, an immutable snapshot with its own URL that never changes. The `live` channel points at one version. Promoting or rolling back moves that pointer. Nothing gets rebuilt. (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 opt into the Zero runtime with one line of config. @@ -50,6 +50,9 @@ Static output is the default path. Point `sf publish` at a build directory and i Drag a folder onto the dashboard. No install, no account required. + + Space, version, live, capsule, Ability, grant — every term, defined once. + What gets uploaded, how versions work, and how to roll back. diff --git a/content/quickstart.mdx b/content/quickstart.mdx index ef4fb267..ab868325 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -11,7 +11,7 @@ You can publish a folder of static files and get a live `view.fast` URL in one c You need Node.js 20.3 or newer and a folder of built static files. Any framework's output directory works (`dist`, `out`, `build`, `public`). If you only have source, `sf publish` can build it for you, see [Frameworks and builds](/frameworks). -Don't want to install anything? Drag a folder onto the dashboard instead — see [Drop](/publish#publish). +Don't want to install anything? Open the dashboard and drag a folder, a `.zip`, or a single `index.html` onto the window — no account needed. See [Drop](/publish#publish) for the full walkthrough. diff --git a/content/troubleshooting.mdx b/content/troubleshooting.mdx index 7ae171ff..bdf0e927 100644 --- a/content/troubleshooting.mdx +++ b/content/troubleshooting.mdx @@ -7,6 +7,10 @@ sidebar: Find your symptom, get the code the platform actually returns, then go to the page that owns it. Every code here is one Spacefast emits. +:::note[Not sure where to start?] +Open your Space in the dashboard first — its **Overview** page shows the last publish and its status in plain language, and often that's enough. Come back here when you have a specific error code or a symptom from the list below. +::: + ## Publishing ### The publish says nothing changed From 24a1fbcebac1e9f2541b5a7e2d687fb83d011507 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Fri, 2 Oct 2026 11:46:04 -0400 Subject: [PATCH 10/20] Apply the style guide to the style guide's own newest content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You asked directly: is the style guide actually being adhered to on all pages? Checked rather than asserted. Vale (the mechanically enforced half: banned words, brand casing, wordiness, link text) passes repo-wide including everything added this session — confirmed by rerunning verify:prose. The non-mechanical half (concision, SentenceLength as a suggestion) hadn't been checked against the content added in the last two commits. Found 3 real hits: two glossary entries (Build, Zero) that chained clauses past the point of earning them, tightened to match the pattern used everywhere else. One was a genuine Vale tokenizer artifact, not a writing problem: an em-dash immediately before bold markup in the new troubleshooting.mdx note caused two short sentences to be scored as one long one; rewrote the punctuation to avoid it (confirmed by testing against pre-existing :::note[] blocks elsewhere that don't trip the same false positive). Two remaining hits are legitimate, stated exceptions: the Grant glossary entry's audience-type enumeration needs the full list to be a complete definition, and one opener sits at 31 words using the explicitly sanctioned em-dash two-clause construction. Co-Authored-By: Claude Sonnet 5 --- content/(reference)/glossary.mdx | 4 ++-- content/troubleshooting.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx index d9acca3e..6b9147ee 100644 --- a/content/(reference)/glossary.mdx +++ b/content/(reference)/glossary.mdx @@ -13,7 +13,7 @@ A named operation with a schema that a Space's WordPress publishes for agents to ## Build -What happens when you hand Spacefast source code instead of a built output directory: it installs dependencies, runs your framework's build command in a sandbox, and publishes the result as a version. See [Frameworks and builds](/frameworks). +What happens when you hand Spacefast source code instead of a built output directory. It installs your dependencies, runs the build in a sandbox, and publishes the result as a version. See [Frameworks and builds](/frameworks). ## Capsule @@ -53,4 +53,4 @@ An immutable snapshot created by every publish, with its own permanent URL that ## Zero -The runtime for a Space that needs a database, live queries, or scheduled jobs: one TypeScript app, a MySQL database next to your code, and HTTP endpoints, compiled into a [capsule](#capsule). The other dynamic option, alongside [Functions](#functions). See [Dynamic sites with Zero](/zero-runtime). +The runtime for a Space that needs a database, live queries, or scheduled jobs. One TypeScript app compiles into a [capsule](#capsule) with a MySQL database next to your code. The other dynamic option, alongside [Functions](#functions). See [Dynamic sites with Zero](/zero-runtime). diff --git a/content/troubleshooting.mdx b/content/troubleshooting.mdx index bdf0e927..6f13dd03 100644 --- a/content/troubleshooting.mdx +++ b/content/troubleshooting.mdx @@ -8,7 +8,7 @@ sidebar: Find your symptom, get the code the platform actually returns, then go to the page that owns it. Every code here is one Spacefast emits. :::note[Not sure where to start?] -Open your Space in the dashboard first — its **Overview** page shows the last publish and its status in plain language, and often that's enough. Come back here when you have a specific error code or a symptom from the list below. +Open your Space in the dashboard first. Its **Overview** page shows the last publish and its status in plain language, and that's often enough. Come back here once you have a specific error code or symptom from the list below. ::: ## Publishing From 3328cf61de703a57a2ab6b997e2e717b73bf3a50 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Fri, 2 Oct 2026 12:37:39 -0400 Subject: [PATCH 11/20] Add a docs test suite: 20 user click-path tests, 20 command-accuracy tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No test suite existed for the docs beyond evals.yaml (AI-assistant factual retrieval). Added DOCS_TEST_SUITE.md with two independent 20-question suites: - User click-path: can a real user find the answer, stated clearly, in 3 clicks or less from the homepage? Run live against the rendered site, not source files. 18/20 passed. Two failures: (1) the Database page assumes Zero is already running and never says how to turn it on — fixed with a note cross-linking to Zero setup; (2) there is no discoverable support/contact channel anywhere in the site ("contact support" appears 3 times, never with an actual email, form, or link) — flagged, not fixed, since inventing a plausible-looking contact method would be worse than the honest gap. - Agent/command-accuracy: is the exact command or code shown actually correct, cross-checked against the frozen generated CLI/API reference? 18/20 passed. Found two real, pre-existing bugs neither of the prior five review passes caught (those were about prose quality, not command accuracy): the generated CLI reference's own `sf api-keys create` example uses `--preset full_access`, which isn't a valid preset per its own documented enum; and the hand-authored api-keys page omits a real preset (`partner_admin`) and states two contradictory defaults. The first is in frozen, producer-owned content and needs a monorepo-side fix, not a hand-edit here. The second is authored and editable, but needs a judgment call this suite can't make on its own — flagged for a deliberate follow-up. Co-Authored-By: Claude Sonnet 5 --- DOCS_TEST_SUITE.md | 143 +++++++++++++++++++++++++++++++++ content/(dynamic)/database.mdx | 4 + 2 files changed, 147 insertions(+) create mode 100644 DOCS_TEST_SUITE.md diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md new file mode 100644 index 00000000..abab5fc8 --- /dev/null +++ b/DOCS_TEST_SUITE.md @@ -0,0 +1,143 @@ +# Spacefast docs test suite + +Two 20-question suites, each testing something the other can't. Neither +existed before this suite; `evals.yaml` already covers a third, related +thing (an AI assistant's factual-retrieval accuracy answering from the docs) +and isn't duplicated here. + +- **Part 1 — can a real user find the answer, stated clearly, in 3 clicks or + less?** Tests navigation and clarity, not content accuracy. +- **Part 2 — is the exact command/code shown actually correct?** Tests + content accuracy against the frozen, producer-owned reference + (`generated/cli/`, `generated/openapi/`), not navigation. + +Both are point-in-time results, not a standing guarantee. Re-run by hand +when the content changes meaningfully — there's no CI gate for either yet +(see "Running this again," below). + +## Part 1: User click-path tests (20 questions) + +Methodology: every test starts fresh from the homepage +(`http://localhost:4321/docs`), using only real navigation a user would use +(nav bar, sidebar, in-page links/cards). A "click" is one navigation action. +"Stated clearly" means the answer is plain and near the top of where you +land — not something inferred from paragraphs of surrounding technical +detail. + +| # | Question | Clicks | Landed on | Verdict | +|---|---|---|---|---| +| 1 | Publish without installing anything? | 0 | Homepage | PASS | +| 2 | How much does it cost? | 1 | `/billing` | PASS | +| 3 | Add a custom domain? | 1 | `/domains` | PASS | +| 4 | Undo a bad publish? | 1 | `/versions` | PASS | +| 5 | Publish without an account — what happens? | 1 | `/anonymous-and-claim` | PASS | +| 6 | Let one specific person see my site? | 1 | `/access` | PASS | +| 7 | Put a password on my site? | 1 | `/access` | PASS | +| 8 | What exactly is a "Space"? | 1 | `/spaces` | PASS | +| 9 | Connect Claude to publish for me? | 1 | `/agents` | PASS | +| 10 | Does it work with Next.js? | 1 | `/recipes/next` | PASS | +| 11 | Add a database to my site? | 2 | `/database` → `/zero-runtime` | **FAIL → fixed** | +| 12 | My build failed — what do I do? | 1 | `/troubleshooting` | PASS | +| 13 | See my site's traffic? | 1 | `/stats` | PASS | +| 14 | Use my own logo on error pages? | 2 | `/customization` → `/site-pages` | PASS (second page needed for full clarity) | +| 15 | Invite a teammate? | 1 | `/teams` | PASS | +| 16 | Free plan limits? | 1 | `/limits` | PASS | +| 17 | Connect GitHub for auto-deploy? | 1 | `/git` | PASS | +| 18 | Schedule something hourly? | 1 | `/crons` | PASS | +| 19 | Something broke — where do I get help? | 1 | `/troubleshooting` | **FAIL — real gap, not fixed** | +| 20 | Can I resell this under my own brand? | 1 | `/platforms` | PASS | + +**Score: 18/20 pass at ≤3 clicks, 2 failures.** + +### Failure 1 (Q11) — fixed + +The single most predictable click for "add a database" — the page literally +titled **Database** — silently assumed Zero was already running and never +explained how to turn it on. The real instructions live on a differently +named page (**Dynamic sites with Zero**) that the question wouldn't point +you to. Fixed: added a note at the top of `content/(dynamic)/database.mdx` +("Don't have a database yet?") naming the exact `sf.jsonc` key and `sf init` +flag, cross-linking to Zero. + +### Failure 2 (Q19) — flagged, not fixed + +There is no discoverable support/contact/community surface anywhere in the +site — no footer, no "Help" nav item, no status page. "Contact support" +appears exactly 3 times in the whole corpus +(`troubleshooting.mdx`, `domains.mdx` ×2) and **never once says how** — no +email, form, or link. This is not a documentation bug I can fix with facts +I have: inventing a plausible-looking support email or link would violate +the same "document only shipped, verifiable behavior" rule this whole +project has followed, and would be actively worse than the current honest +gap. **This needs real input — an actual support channel — before anyone +can write the fix.** + +## Part 2: Agent/command-accuracy tests (20 questions) + +Methodology: every answer cross-checked against `generated/cli/index.md` and +`generated/openapi/api.json` — the frozen, producer-owned ground truth — not +executed against a live backend (none available in this environment). +"Works" means the command, flag, or endpoint shown is real, current, and +internally consistent with the generated reference. + +| # | Question | Result | +|---|---|---| +| 1 | Install command (npm)? | PASS | +| 2 | Publish the current directory? | PASS | +| 3 | Force upload vs. force build? | PASS | +| 4 | List all versions? | PASS | +| 5 | Roll back to a version? | PASS | +| 6 | Add a domain as primary? | PASS | +| 7 | Check domain/DNS verification? | PASS | +| 8 | Create an API key with a preset? | **FAIL — 2 bugs found** | +| 9 | curl to publish via HTTP API? | PASS | +| 10 | Bearer-token header? | PASS | +| 11 | Set an environment variable? | PASS | +| 12 | View build logs? | PASS | +| 13 | Connect a GitHub repository? | PASS | +| 14 | Create a team? | PASS | +| 15 | CLI exit code for auth failure? | PASS | +| 16 | MCP server URL + transport? | PASS | +| 17 | Run a cron on demand? | PASS | +| 18 | Open a SQL console? | PASS | +| 19 | Password-protect a path? | PASS | +| 20 | Webhook signature header + algorithm? | PASS | + +**Score: 18/20 pass, 2 real bugs found in one question.** + +### Bug 1 — in the frozen generated reference (not fixed here) + +`generated/cli/index.md` documents `sf api-keys create --preset` with a +7-value enum (`ci_deploy`, `space_publisher`, `space_admin`, +`domain_manager`, `team_admin`, `billing_viewer`, `partner_admin`) — then its +own usage example runs `--preset full_access`, a value that isn't in that +list. Anyone who copies the example gets a validation error. This exact text +is also baked into `content/cli/reference.md` (the materialized build +overlay). Per `AGENTS.md`, generated content is fixed at its source in the +product monorepo and re-exported, never hand-edited here — **this needs a +fix in the monorepo's CLI source**, flagged, not touched, in this repo. + +### Bug 2 — in hand-authored content (not fixed here, flagging for a decision) + +`content/(account)/api-keys.mdx`'s preset table lists only 6 of the 7 real +presets (missing `partner_admin` entirely), and contradicts itself on the +default: line 34 says `sf api-keys create` defaults to `space_publisher` +(matching the generated reference); line 60 says `space_admin` is "the +default when a request names no preset." Unlike Bug 1, this one *is* +editable here — it's authored content, not generated — but fixing it needs +a judgment call on what the second "default" claim actually means (CLI vs. +a different code path), which this test suite can't resolve on its own. +Left for a deliberate follow-up rather than guessing. + +## Running this again + +- **Part 1** needs a human or an agent with browser access, the dev server + running (`bun run dev`), and ~15–20 minutes. No automation exists for + this yet — it's a manual QA script, not a CI check. +- **Part 2** needs `grep`/`Read` access to `generated/cli/index.md` and + `generated/openapi/api.json`, cross-checked against whatever commands the + content pages show. Also manual today. A future version of this could + become a script (extract every fenced `sf ...` command from `content/**`, + parse `generated/cli/index.md`'s own usage/example blocks, diff them) — + out of scope for this pass, but the two real bugs found by hand suggest + it would pay for itself. diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index bd12f7ea..b64481ce 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -7,6 +7,10 @@ sidebar: 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. +:::note[Don't have a database yet?] +This page covers the database you get once [Zero](/zero-runtime) is running. Turn Zero on first — declare `kind: "zero"` in `sf.jsonc`, or run `sf init --runtime zero` to scaffold a new project with it. +::: + ## 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`. From 5224645f6534e05843134236db6e7e1b01cdc410 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Mon, 5 Oct 2026 12:02:07 -0400 Subject: [PATCH 12/20] Correct docs QA question-level scores --- DOCS_TEST_SUITE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md index abab5fc8..7e90fa04 100644 --- a/DOCS_TEST_SUITE.md +++ b/DOCS_TEST_SUITE.md @@ -36,7 +36,7 @@ detail. | 8 | What exactly is a "Space"? | 1 | `/spaces` | PASS | | 9 | Connect Claude to publish for me? | 1 | `/agents` | PASS | | 10 | Does it work with Next.js? | 1 | `/recipes/next` | PASS | -| 11 | Add a database to my site? | 2 | `/database` → `/zero-runtime` | **FAIL → fixed** | +| 11 | Add a database to my site? | 2 | `/database` → `/zero-runtime` | PASS (initial failure fixed) | | 12 | My build failed — what do I do? | 1 | `/troubleshooting` | PASS | | 13 | See my site's traffic? | 1 | `/stats` | PASS | | 14 | Use my own logo on error pages? | 2 | `/customization` → `/site-pages` | PASS (second page needed for full clarity) | @@ -47,7 +47,8 @@ detail. | 19 | Something broke — where do I get help? | 1 | `/troubleshooting` | **FAIL — real gap, not fixed** | | 20 | Can I resell this under my own brand? | 1 | `/platforms` | PASS | -**Score: 18/20 pass at ≤3 clicks, 2 failures.** +**Final score: 19/20 pass at ≤3 clicks, 1 unresolved failure.** Q11 +failed on the first pass and was fixed before this result was recorded. ### Failure 1 (Q11) — fixed @@ -103,7 +104,8 @@ internally consistent with the generated reference. | 19 | Password-protect a path? | PASS | | 20 | Webhook signature header + algorithm? | PASS | -**Score: 18/20 pass, 2 real bugs found in one question.** +**Score: 19/20 questions pass.** Q8 fails because of 2 real bugs in that +one question. ### Bug 1 — in the frozen generated reference (not fixed here) From 61c899992ce60e3921b55f75620317d47f592455 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Mon, 5 Oct 2026 12:26:40 -0400 Subject: [PATCH 13/20] Refresh docs QA comparison against current main --- DOCS_TEST_SUITE.md | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md index 7e90fa04..24f1edad 100644 --- a/DOCS_TEST_SUITE.md +++ b/DOCS_TEST_SUITE.md @@ -15,11 +15,36 @@ Both are point-in-time results, not a standing guarantee. Re-run by hand when the content changes meaningfully — there's no CI gate for either yet (see "Running this again," below). +## Current main versus this branch (October 5, 2026) + +Both sites were built from the same current `main` base and tested in a browser +from their homepages. The candidate adds one authored route (Glossary); the +generated CLI and API reference trees are identical on both branches. + +| Test | Current `main` | This branch | Change | +|---|---:|---:|---| +| Part 1: clear answer in at most 3 clicks | 18/20 | 19/20 | +1 question | +| Part 2: command/API accuracy against generated reference | 19/20 | 19/20 | No change | + +Part 1 improvement comes from Q11: the Database page now tells readers how +to enable Zero before using its database (one click on both sites; the old +answer was incomplete). Q1 already passed on `main`, but the no-install Drop +path moved from Publishing → Dashboard (two actions) to the homepage (zero). +Q19 still fails on both sites because “contact support” has no linked or named +support channel. Q14 can be answered on Customization in one click; Site pages +adds detail about error-page layout in a second click. + +Part 2 Q8 is one failed question with two issues on both branches: the +generated CLI example uses a preset outside its own enum, and the authored +API-key page omits `partner_admin` while giving conflicting default-preset +guidance. The checks compare documentation with the generated reference; they +do not execute a live publish or API request. + ## Part 1: User click-path tests (20 questions) Methodology: every test starts fresh from the homepage -(`http://localhost:4321/docs`), using only real navigation a user would use -(nav bar, sidebar, in-page links/cards). A "click" is one navigation action. +(`/docs/`), using only real navigation a user would use +(nav bar, sidebar, tabs, in-page links/cards). A "click" is one navigation action. "Stated clearly" means the answer is plain and near the top of where you land — not something inferred from paragraphs of surrounding technical detail. @@ -36,10 +61,10 @@ detail. | 8 | What exactly is a "Space"? | 1 | `/spaces` | PASS | | 9 | Connect Claude to publish for me? | 1 | `/agents` | PASS | | 10 | Does it work with Next.js? | 1 | `/recipes/next` | PASS | -| 11 | Add a database to my site? | 2 | `/database` → `/zero-runtime` | PASS (initial failure fixed) | +| 11 | Add a database to my site? | 1 | `/database` | PASS (initial failure fixed) | | 12 | My build failed — what do I do? | 1 | `/troubleshooting` | PASS | | 13 | See my site's traffic? | 1 | `/stats` | PASS | -| 14 | Use my own logo on error pages? | 2 | `/customization` → `/site-pages` | PASS (second page needed for full clarity) | +| 14 | Use my own logo on error pages? | 1 | `/customization` | PASS (`/site-pages` adds error-page detail in a second click) | | 15 | Invite a teammate? | 1 | `/teams` | PASS | | 16 | Free plan limits? | 1 | `/limits` | PASS | | 17 | Connect GitHub for auto-deploy? | 1 | `/git` | PASS | @@ -47,7 +72,7 @@ detail. | 19 | Something broke — where do I get help? | 1 | `/troubleshooting` | **FAIL — real gap, not fixed** | | 20 | Can I resell this under my own brand? | 1 | `/platforms` | PASS | -**Final score: 19/20 pass at ≤3 clicks, 1 unresolved failure.** Q11 +**Candidate score: 19/20 pass at ≤3 clicks, 1 unresolved failure.** Q11 failed on the first pass and was fixed before this result was recorded. ### Failure 1 (Q11) — fixed From e3e32c275de715885f361dc4a7457c74cd421812 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Mon, 5 Oct 2026 12:36:20 -0400 Subject: [PATCH 14/20] Evaluate authored prose and revise weak page leads --- DOCS_TEST_SUITE.md | 3 ++ DOCS_WRITING_QA.md | 40 ++++++++++++++++++++++ content/(concepts)/spaces.mdx | 2 +- content/(dynamic)/logs.mdx | 2 +- content/(dynamic)/wordpress.mdx | 4 +-- content/(publish)/frameworks.mdx | 4 ++- content/(publish)/git.mdx | 4 ++- scripts/compare-writing.mjs | 57 ++++++++++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 DOCS_WRITING_QA.md create mode 100644 scripts/compare-writing.mjs diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md index 24f1edad..b1f24258 100644 --- a/DOCS_TEST_SUITE.md +++ b/DOCS_TEST_SUITE.md @@ -5,6 +5,9 @@ existed before this suite; `evals.yaml` already covers a third, related thing (an AI assistant's factual-retrieval accuracy answering from the docs) and isn't duplicated here. +These suites do not grade prose quality. The before/after writing audit is in +[DOCS_WRITING_QA.md](DOCS_WRITING_QA.md). + - **Part 1 — can a real user find the answer, stated clearly, in 3 clicks or less?** Tests navigation and clarity, not content accuracy. - **Part 2 — is the exact command/code shown actually correct?** Tests diff --git a/DOCS_WRITING_QA.md b/DOCS_WRITING_QA.md new file mode 100644 index 00000000..a18ea664 --- /dev/null +++ b/DOCS_WRITING_QA.md @@ -0,0 +1,40 @@ +# Authored-docs writing audit + +The click-path suite in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md) measures whether a reader can find an answer. The command suite checks facts against the generated reference. Neither measures whether the authored prose is better. This point-in-time audit checks the writing changes against `main` as of October 5, 2026. + +## Method + +Run `node scripts/compare-writing.mjs origin/main` from the repository root before this branch merges. It selects existing authored MDX pages changed from that baseline, extracts each page's first prose paragraph after frontmatter, and counts words. A templated opening starts with “After this page” or “By the end of this page.” The 30-word threshold is a review signal, not a readability guarantee. + +I also read all 65 changed opening paragraphs side by side with their page titles. For each, I checked whether the first paragraph gives a practical fact or action or repeats a generic promise about the page. That review found five leads starting with a narrow implementation detail; they were revised to lead with the page's main job. I checked that each displaced detail remains elsewhere on its page. + +## Results + +| Signal | Current `main` | This branch | Scope | +|---|---:|---:|---| +| Repeated “After this page…” or “By the end of this page…” openings | 53 | 0 | 78 changed existing MDX pages | +| Opening paragraphs over 30 words | 43 | 13 | 65 changed opening paragraphs | +| Median opening words | 34 | 23 | 65 changed opening paragraphs | + +Of the 65 changed leads, 62 are shorter, one has the same word count, and two are longer. The candidate also adds a glossary and a no-install publishing route on the homepage. Those are separate navigation and comprehension aids; this word-count table does not score them. + +The edited leads now put several useful answers before the reader has to scan the page: + +| Page | Reader's question | Current `main` opening | Candidate opening | +|---|---|---|---| +| Versions | Does rollback rebuild the site? | Promises to explain rollback | Says rollback only repoints `live`; no rebuild | +| Crons | Is there a dashboard editor? | Promises to explain scheduling | Says schedules live in `sf.jsonc` and take effect on publish | +| Environment variables | Which value wins when team and Space both set a name? | Promises to explain scopes | Says the Space value wins | +| Access and sharing | What makes a Space private? | Promises to explain access | Says removing every grant makes it private | +| Caching | How do I force a fresh response? | Promises to explain cache behavior | Says republishing forces one | +| Traffic stats | Are crawlers included? | Promises to explain counts | Says crawler traffic is excluded | + +These rows are illustrative checks of what the opening now tells a reader, not an independent six-question success rate. The underlying facts and the full procedures remain on the pages. + +For example, Versions opened with “After this page you know what a version holds, how it reaches `ready`, how the `live` pointer moves, and how to roll back to any earlier version in seconds.” It now opens with “A rollback doesn't rebuild anything — it just repoints `live` at a version that already exists, which is why it takes seconds, not minutes.” The new sentence answers the likely rollback question; the page still explains version states below it. + +## Review findings and limits + +The manual review caught five candidate leads that were shorter but spent the first sentence on a less useful detail: slug validation on Spaces, polling mechanics on Logs, archive flags on Frameworks and builds, CLI naming on Publish from Git, and remote WP-CLI output on WordPress. Each now leads with the page's main task or mental model. The removed detail was checked elsewhere on the same page and retained or moved into the body. + +This audit establishes changes in structure and in which facts appear first. It does not show that real readers complete tasks faster or understand the docs better. That requires reader testing. Vale still reports the two existing spelling alerts (`GETs` and `TTYs`); its rules do not detect repeated sentence structures or judge whether a page leads with the right fact. diff --git a/content/(concepts)/spaces.mdx b/content/(concepts)/spaces.mdx index 802aa321..93fdd90f 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -A Space's slug has to pass a strict set of validation rules before the API accepts it — and it becomes part of every hostname that Space answers on. +A Space is one site with a stable hostname. Publishing creates versions inside it, and the Space decides which one visitors see. ## What a Space is diff --git a/content/(dynamic)/logs.mdx b/content/(dynamic)/logs.mdx index 40737cbc..8740ee54 100644 --- a/content/(dynamic)/logs.mdx +++ b/content/(dynamic)/logs.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -`sf logs --follow` doesn't open a websocket — it polls every two seconds and only prints what it hasn't shown you yet. +Use `sf logs` for requests, `sf logs runtime` for your code's output, and `sf builds logs` for build failures. ## Three streams diff --git a/content/(dynamic)/wordpress.mdx b/content/(dynamic)/wordpress.mdx index b5b4ffe6..f0e6a702 100644 --- a/content/(dynamic)/wordpress.mdx +++ b/content/(dynamic)/wordpress.mdx @@ -3,9 +3,9 @@ title: WordPress on every Space description: Every Space runs a WordPress behind its static files. Run WP-CLI against it, call its Abilities with a short-lived token, and open its database. --- -Run `sf wp` against a Space's WordPress and you won't see what the command printed — only whether it succeeded, unless you add `--local`. +Every Space includes a WordPress install behind its published files. Reach it through `sf wp` or its published Abilities. -Every Space is backed by a WordPress install. Your published files serve in front of it, 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](/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-runtime) apps run on it, [Storage](/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/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index dcf5eebc..fb35ed65 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -`--prebuilt` forces the upload path and `--build` forces the build path — but a `.tar.gz` archive always goes through a build, since archives are classified remotely. +Publish built output directly when you have it; Spacefast can build from source when you don't. ## Two paths @@ -24,6 +24,8 @@ sf publish --remote With no flags, `sf publish` builds a detected project, and publishes the directory as-is if there is nothing to build. +`--prebuilt` forces the upload path and `--build` forces the build path. A `.tar.gz` archive always goes through a build, since archives are classified remotely. + ## Which directory to publish Detection uses this table for both paths. The output directory column is what to hand `sf publish` when you build yourself. diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index e87586fe..2483c789 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -5,10 +5,12 @@ sidebar: order: 4 --- -The `sf git` and `sf source` command families name the same two connection types in different words — on purpose, not a typo. +Keep your source in Git and publish from either a Spacefast remote or a connected GitHub repository. Pushing to either starts a build. Two lanes. The Spacefast remote needs no external host and no app install. A GitHub connection keeps your repository where it is and reacts to its webhooks. +The `sf git` and `sf source` command families name the same two connection types in different words — on purpose, not a typo. + ## Source and deployment files `sf publish dist --prebuilt` uploads the files in `dist`, not the editable project that produced them. A version's Git commit metadata records where a build came from; it does not mean your source or Git history was uploaded. diff --git a/scripts/compare-writing.mjs b/scripts/compare-writing.mjs new file mode 100644 index 00000000..3e990aa8 --- /dev/null +++ b/scripts/compare-writing.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +// Compare the opening prose of existing authored pages against a Git baseline. +// This reports editing signals, not a reader-comprehension score. + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const baseline = process.argv[2] ?? "origin/main"; + +function git(...args) { + return execFileSync("git", args, { encoding: "utf8" }); +} + +function lead(source) { + const body = source.startsWith("---") ? source.split("---", 3)[2] : source; + if (!body) throw new Error("Could not find page body"); + + for (const block of body.trimStart().split(/\n\s*\n/)) { + const paragraph = block.trim(); + if (!paragraph || /^(#|<|:::|```|\||- |1\. )/.test(paragraph)) continue; + return paragraph.replace(/\s+/g, " "); + } + throw new Error("Could not find opening paragraph"); +} + +function words(source) { + return (source.match(/[\p{L}\p{N}_]+(?:['’-][\p{L}\p{N}_]+)*/gu) ?? []).length; +} + +const paths = git("diff", "--name-only", "--diff-filter=M", baseline, "--", "content") + .trim() + .split("\n") + .filter((path) => path.endsWith(".mdx")); + +const rows = paths.map((path) => { + const before = lead(git("show", `${baseline}:${path}`)); + const after = lead(readFileSync(path, "utf8")); + return { path, before, after, beforeWords: words(before), afterWords: words(after) }; +}); + +const changed = rows.filter(({ before, after }) => before !== after); +const median = (numbers) => { + const sorted = numbers.toSorted((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2; +}; +const count = (predicate, list = changed) => list.filter(predicate).length; +const templated = (source) => /^(after this page|by the end of this page)/i.test(source); + +console.log(`Baseline: ${baseline}`); +console.log(`Existing authored MDX pages changed: ${rows.length}`); +console.log(`Opening paragraphs changed: ${changed.length}`); +console.log(`Templated openings: ${count((row) => templated(row.before), rows)} → ${count((row) => templated(row.after), rows)}`); +console.log(`Opening paragraphs over 30 words: ${count((row) => row.beforeWords > 30)} → ${count((row) => row.afterWords > 30)}`); +console.log(`Median opening words: ${median(changed.map((row) => row.beforeWords))} → ${median(changed.map((row) => row.afterWords))}`); +console.log(`Shorter / same length / longer: ${count((row) => row.afterWords < row.beforeWords)} / ${count((row) => row.afterWords === row.beforeWords)} / ${count((row) => row.afterWords > row.beforeWords)}`); From 1d9dfcc6f675435fcb49ae2635e1cd1c2fd13615 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Mon, 5 Oct 2026 13:36:56 -0400 Subject: [PATCH 15/20] Add executable docs experience regression tests --- .github/workflows/ci.yml | 9 +- AGENTS.md | 1 + DOCS_QA_COMPARISON.md | 173 +++++++++++++++++++++++++++++ DOCS_TEST_SUITE.md | 182 ++++--------------------------- DOCS_WRITING_QA.md | 2 +- package.json | 1 + scripts/docs-experience.test.mjs | 133 ++++++++++++++++++++++ 7 files changed, 334 insertions(+), 167 deletions(-) create mode 100644 DOCS_QA_COMPARISON.md create mode 100644 scripts/docs-experience.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87cc3c73..fc00aa0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,9 +43,6 @@ jobs: | sudo tar -xz -C /usr/local/bin vale vale --version - - name: Verify prose style - run: bun run verify:prose - - name: Check types and templates run: bun run check @@ -55,8 +52,14 @@ jobs: - name: Build static site run: bun run build + - name: Test built docs experience and helpers + run: bun run test:docs + - name: Audit built site run: bun run audit - name: Verify routes and artifacts run: bun run verify:routes + + - name: Verify prose style + run: bun run verify:prose diff --git a/AGENTS.md b/AGENTS.md index e28927ac..762823d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,7 @@ bun run verify:generated bun run check bun run validate bun run build +bun run test:docs bun run audit bun run verify:public-safety bun run verify:prose diff --git a/DOCS_QA_COMPARISON.md b/DOCS_QA_COMPARISON.md new file mode 100644 index 00000000..aec945cf --- /dev/null +++ b/DOCS_QA_COMPARISON.md @@ -0,0 +1,173 @@ +# Point-in-time docs QA comparison + +This report records two manual 20-question comparisons made before the PR. +The results are evidence for this edit, not an executable regression suite. +The runnable checks are documented in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). +`evals.yaml` separately covers an AI assistant's factual retrieval from the +docs. + +These comparisons do not grade prose quality. The before/after writing audit is in +[DOCS_WRITING_QA.md](DOCS_WRITING_QA.md). + +- **Part 1 — can a real user find the answer, stated clearly, in 3 clicks or + less?** Tests navigation and clarity, not content accuracy. +- **Part 2 — is the exact command/code shown actually correct?** Tests + content accuracy against the frozen, producer-owned reference + (`generated/cli/`, `generated/openapi/`), not navigation. + +Both are point-in-time observations, not a standing guarantee or a CI gate. +See "Running this again" below for the manual method. + +## Current main versus this branch (October 5, 2026) + +Both sites were built from the same current `main` base and tested in a browser +from their homepages. The candidate adds one authored route (Glossary); the +generated CLI and API reference trees are identical on both branches. + +| Test | Current `main` | This branch | Change | +|---|---:|---:|---| +| Part 1: clear answer in at most 3 clicks | 18/20 | 19/20 | +1 question | +| Part 2: command/API accuracy against generated reference | 19/20 | 19/20 | No change | + +Part 1 improvement comes from Q11: the Database page now tells readers how +to enable Zero before using its database (one click on both sites; the old +answer was incomplete). Q1 already passed on `main`, but the no-install Drop +path moved from Publishing → Dashboard (two actions) to the homepage (zero). +Q19 still fails on both sites because “contact support” has no linked or named +support channel. Q14 can be answered on Customization in one click; Site pages +adds detail about error-page layout in a second click. + +Part 2 Q8 is one failed question with two issues on both branches: the +generated CLI example uses a preset outside its own enum, and the authored +API-key page omits `partner_admin` while giving conflicting default-preset +guidance. The checks compare documentation with the generated reference; they +do not execute a live publish or API request. + +## Part 1: Manual user click-path comparison (20 questions) + +Methodology: every test starts fresh from the homepage +(`/docs/`), using only real navigation a user would use +(nav bar, sidebar, tabs, in-page links/cards). A "click" is one navigation action. +"Stated clearly" means the answer is plain and near the top of where you +land — not something inferred from paragraphs of surrounding technical +detail. + +| # | Question | Clicks | Landed on | Verdict | +|---|---|---|---|---| +| 1 | Publish without installing anything? | 0 | Homepage | PASS | +| 2 | How much does it cost? | 1 | `/billing` | PASS | +| 3 | Add a custom domain? | 1 | `/domains` | PASS | +| 4 | Undo a bad publish? | 1 | `/versions` | PASS | +| 5 | Publish without an account — what happens? | 1 | `/anonymous-and-claim` | PASS | +| 6 | Let one specific person see my site? | 1 | `/access` | PASS | +| 7 | Put a password on my site? | 1 | `/access` | PASS | +| 8 | What exactly is a "Space"? | 1 | `/spaces` | PASS | +| 9 | Connect Claude to publish for me? | 1 | `/agents` | PASS | +| 10 | Does it work with Next.js? | 1 | `/recipes/next` | PASS | +| 11 | Add a database to my site? | 1 | `/database` | PASS (initial failure fixed) | +| 12 | My build failed — what do I do? | 1 | `/troubleshooting` | PASS | +| 13 | See my site's traffic? | 1 | `/stats` | PASS | +| 14 | Use my own logo on error pages? | 1 | `/customization` | PASS (`/site-pages` adds error-page detail in a second click) | +| 15 | Invite a teammate? | 1 | `/teams` | PASS | +| 16 | Free plan limits? | 1 | `/limits` | PASS | +| 17 | Connect GitHub for auto-deploy? | 1 | `/git` | PASS | +| 18 | Schedule something hourly? | 1 | `/crons` | PASS | +| 19 | Something broke — where do I get help? | 1 | `/troubleshooting` | **FAIL — real gap, not fixed** | +| 20 | Can I resell this under my own brand? | 1 | `/platforms` | PASS | + +**Candidate score: 19/20 pass at ≤3 clicks, 1 unresolved failure.** Q11 +failed on the first pass and was fixed before this result was recorded. + +### Failure 1 (Q11) — fixed + +The single most predictable click for "add a database" — the page literally +titled **Database** — silently assumed Zero was already running and never +explained how to turn it on. The real instructions live on a differently +named page (**Dynamic sites with Zero**) that the question wouldn't point +you to. Fixed: added a note at the top of `content/(dynamic)/database.mdx` +("Don't have a database yet?") naming the exact `sf.jsonc` key and `sf init` +flag, cross-linking to Zero. + +### Failure 2 (Q19) — flagged, not fixed + +There is no discoverable support/contact/community surface anywhere in the +site — no footer, no "Help" nav item, no status page. "Contact support" +appears exactly 3 times in the whole corpus +(`troubleshooting.mdx`, `domains.mdx` ×2) and **never once says how** — no +email, form, or link. This is not a documentation bug I can fix with facts +I have: inventing a plausible-looking support email or link would violate +the same "document only shipped, verifiable behavior" rule this whole +project has followed, and would be actively worse than the current honest +gap. **This needs real input — an actual support channel — before anyone +can write the fix.** + +## Part 2: Manual command-accuracy comparison (20 questions) + +Methodology: every answer cross-checked against `generated/cli/index.md` and +`generated/openapi/api.json` — the frozen, producer-owned ground truth — not +executed against a live backend (none available in this environment). +"Works" means the command, flag, or endpoint shown is real, current, and +internally consistent with the generated reference. + +| # | Question | Result | +|---|---|---| +| 1 | Install command (npm)? | PASS | +| 2 | Publish the current directory? | PASS | +| 3 | Force upload vs. force build? | PASS | +| 4 | List all versions? | PASS | +| 5 | Roll back to a version? | PASS | +| 6 | Add a domain as primary? | PASS | +| 7 | Check domain/DNS verification? | PASS | +| 8 | Create an API key with a preset? | **FAIL — 2 bugs found** | +| 9 | curl to publish via HTTP API? | PASS | +| 10 | Bearer-token header? | PASS | +| 11 | Set an environment variable? | PASS | +| 12 | View build logs? | PASS | +| 13 | Connect a GitHub repository? | PASS | +| 14 | Create a team? | PASS | +| 15 | CLI exit code for auth failure? | PASS | +| 16 | MCP server URL + transport? | PASS | +| 17 | Run a cron on demand? | PASS | +| 18 | Open a SQL console? | PASS | +| 19 | Password-protect a path? | PASS | +| 20 | Webhook signature header + algorithm? | PASS | + +**Score: 19/20 questions pass.** Q8 fails because of 2 real bugs in that +one question. + +### Bug 1 — in the frozen generated reference (not fixed here) + +`generated/cli/index.md` documents `sf api-keys create --preset` with a +7-value enum (`ci_deploy`, `space_publisher`, `space_admin`, +`domain_manager`, `team_admin`, `billing_viewer`, `partner_admin`) — then its +own usage example runs `--preset full_access`, a value that isn't in that +list. Anyone who copies the example gets a validation error. This exact text +is also baked into `content/cli/reference.md` (the materialized build +overlay). Per `AGENTS.md`, generated content is fixed at its source in the +product monorepo and re-exported, never hand-edited here — **this needs a +fix in the monorepo's CLI source**, flagged, not touched, in this repo. + +### Bug 2 — in hand-authored content (not fixed here, flagging for a decision) + +`content/(account)/api-keys.mdx`'s preset table lists only 6 of the 7 real +presets (missing `partner_admin` entirely), and contradicts itself on the +default: line 34 says `sf api-keys create` defaults to `space_publisher` +(matching the generated reference); line 60 says `space_admin` is "the +default when a request names no preset." Unlike Bug 1, this one *is* +editable here — it's authored content, not generated — but fixing it needs +a judgment call on what the second "default" claim actually means (CLI vs. +a different code path), which this comparison can't resolve on its own. +Left for a deliberate follow-up rather than guessing. + +## Running this again + +- **Part 1** needs a human or an agent with browser access, the dev server + running (`bun run dev`), and ~15–20 minutes. No automation exists for + this yet — it's a manual QA script, not a CI check. +- **Part 2** needs `grep`/`Read` access to `generated/cli/index.md` and + `generated/openapi/api.json`, cross-checked against whatever commands the + content pages show. Also manual today. A future version of this could + become a script (extract every fenced `sf ...` command from `content/**`, + parse `generated/cli/index.md`'s own usage/example blocks, diff them) — + out of scope for this pass, but the two real bugs found by hand suggest + it would pay for itself. diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md index b1f24258..4659d829 100644 --- a/DOCS_TEST_SUITE.md +++ b/DOCS_TEST_SUITE.md @@ -1,173 +1,29 @@ -# Spacefast docs test suite +# Docs regression tests -Two 20-question suites, each testing something the other can't. Neither -existed before this suite; `evals.yaml` already covers a third, related -thing (an AI assistant's factual-retrieval accuracy answering from the docs) -and isn't duplicated here. +Run the executable suite after building the site: -These suites do not grade prose quality. The before/after writing audit is in -[DOCS_WRITING_QA.md](DOCS_WRITING_QA.md). +```bash +bun run build +bun run test:docs +``` -- **Part 1 — can a real user find the answer, stated clearly, in 3 clicks or - less?** Tests navigation and clarity, not content accuracy. -- **Part 2 — is the exact command/code shown actually correct?** Tests - content accuracy against the frozen, producer-owned reference - (`generated/cli/`, `generated/openapi/`), not navigation. +CI runs `test:docs` after the production build. It uses Bun's test runner and exits nonzero on a failed assertion. The experience checks read the built Markdown in `dist/`, so a passing source edit alone cannot satisfy them. Existing unit tests for the docs corpus, LLM index, and composed-site audit run in the same command. -Both are point-in-time results, not a standing guarantee. Re-run by hand -when the content changes meaningfully — there's no CI gate for either yet -(see "Running this again," below). +## Reader-facing contracts -## Current main versus this branch (October 5, 2026) +`scripts/docs-experience.test.mjs` checks that: -Both sites were built from the same current `main` base and tested in a browser -from their homepages. The candidate adds one authored route (Glossary); the -generated CLI and API reference trees are identical on both branches. +- Authored pages open with a subject or action instead of the repeated “After this page…” promise. +- The homepage and Quickstart offer dashboard Drop before CLI installation, and the linked Publishing page describes the Drop flow. +- The homepage links to a glossary that defines its recurring product terms. +- Database explains how to enable Zero before it teaches queries. +- Troubleshooting gives readers a first diagnostic step before listing error codes. +- The opening paragraphs on Versions, Crons, Environment variables, Access, Caching, and Traffic stats answer specific reader questions. Each test names the question and the evidence expected near the top of the built page. -| Test | Current `main` | This branch | Change | -|---|---:|---:|---| -| Part 1: clear answer in at most 3 clicks | 18/20 | 19/20 | +1 question | -| Part 2: command/API accuracy against generated reference | 19/20 | 19/20 | No change | +These are regression contracts for the changed entry paths and first-screen explanations. They make a future edit fail if it hides those answers again. They do not grade tone, prove that shorter text is clearer, or measure whether a person can complete a real task. Those claims need editorial review and reader testing. -Part 1 improvement comes from Q11: the Database page now tells readers how -to enable Zero before using its database (one click on both sites; the old -answer was incomplete). Q1 already passed on `main`, but the no-install Drop -path moved from Publishing → Dashboard (two actions) to the homepage (zero). -Q19 still fails on both sites because “contact support” has no linked or named -support channel. Q14 can be answered on Customization in one click; Site pages -adds detail about error-page layout in a second click. +## Other gates -Part 2 Q8 is one failed question with two issues on both branches: the -generated CLI example uses a preset outside its own enum, and the authored -API-key page omits `partner_admin` while giving conflicting default-preset -guidance. The checks compare documentation with the generated reference; they -do not execute a live publish or API request. +The CI build also runs `verify:generated`, command-example verification, type checking, strict link validation, the composed-site audit, public-safety verification, Vale, and route verification. The existing `evals.yaml` asks an agent factual-retrieval questions from the built docs; it is a separate evaluation and is not part of `test:docs`. -## Part 1: User click-path tests (20 questions) - -Methodology: every test starts fresh from the homepage -(`/docs/`), using only real navigation a user would use -(nav bar, sidebar, tabs, in-page links/cards). A "click" is one navigation action. -"Stated clearly" means the answer is plain and near the top of where you -land — not something inferred from paragraphs of surrounding technical -detail. - -| # | Question | Clicks | Landed on | Verdict | -|---|---|---|---|---| -| 1 | Publish without installing anything? | 0 | Homepage | PASS | -| 2 | How much does it cost? | 1 | `/billing` | PASS | -| 3 | Add a custom domain? | 1 | `/domains` | PASS | -| 4 | Undo a bad publish? | 1 | `/versions` | PASS | -| 5 | Publish without an account — what happens? | 1 | `/anonymous-and-claim` | PASS | -| 6 | Let one specific person see my site? | 1 | `/access` | PASS | -| 7 | Put a password on my site? | 1 | `/access` | PASS | -| 8 | What exactly is a "Space"? | 1 | `/spaces` | PASS | -| 9 | Connect Claude to publish for me? | 1 | `/agents` | PASS | -| 10 | Does it work with Next.js? | 1 | `/recipes/next` | PASS | -| 11 | Add a database to my site? | 1 | `/database` | PASS (initial failure fixed) | -| 12 | My build failed — what do I do? | 1 | `/troubleshooting` | PASS | -| 13 | See my site's traffic? | 1 | `/stats` | PASS | -| 14 | Use my own logo on error pages? | 1 | `/customization` | PASS (`/site-pages` adds error-page detail in a second click) | -| 15 | Invite a teammate? | 1 | `/teams` | PASS | -| 16 | Free plan limits? | 1 | `/limits` | PASS | -| 17 | Connect GitHub for auto-deploy? | 1 | `/git` | PASS | -| 18 | Schedule something hourly? | 1 | `/crons` | PASS | -| 19 | Something broke — where do I get help? | 1 | `/troubleshooting` | **FAIL — real gap, not fixed** | -| 20 | Can I resell this under my own brand? | 1 | `/platforms` | PASS | - -**Candidate score: 19/20 pass at ≤3 clicks, 1 unresolved failure.** Q11 -failed on the first pass and was fixed before this result was recorded. - -### Failure 1 (Q11) — fixed - -The single most predictable click for "add a database" — the page literally -titled **Database** — silently assumed Zero was already running and never -explained how to turn it on. The real instructions live on a differently -named page (**Dynamic sites with Zero**) that the question wouldn't point -you to. Fixed: added a note at the top of `content/(dynamic)/database.mdx` -("Don't have a database yet?") naming the exact `sf.jsonc` key and `sf init` -flag, cross-linking to Zero. - -### Failure 2 (Q19) — flagged, not fixed - -There is no discoverable support/contact/community surface anywhere in the -site — no footer, no "Help" nav item, no status page. "Contact support" -appears exactly 3 times in the whole corpus -(`troubleshooting.mdx`, `domains.mdx` ×2) and **never once says how** — no -email, form, or link. This is not a documentation bug I can fix with facts -I have: inventing a plausible-looking support email or link would violate -the same "document only shipped, verifiable behavior" rule this whole -project has followed, and would be actively worse than the current honest -gap. **This needs real input — an actual support channel — before anyone -can write the fix.** - -## Part 2: Agent/command-accuracy tests (20 questions) - -Methodology: every answer cross-checked against `generated/cli/index.md` and -`generated/openapi/api.json` — the frozen, producer-owned ground truth — not -executed against a live backend (none available in this environment). -"Works" means the command, flag, or endpoint shown is real, current, and -internally consistent with the generated reference. - -| # | Question | Result | -|---|---|---| -| 1 | Install command (npm)? | PASS | -| 2 | Publish the current directory? | PASS | -| 3 | Force upload vs. force build? | PASS | -| 4 | List all versions? | PASS | -| 5 | Roll back to a version? | PASS | -| 6 | Add a domain as primary? | PASS | -| 7 | Check domain/DNS verification? | PASS | -| 8 | Create an API key with a preset? | **FAIL — 2 bugs found** | -| 9 | curl to publish via HTTP API? | PASS | -| 10 | Bearer-token header? | PASS | -| 11 | Set an environment variable? | PASS | -| 12 | View build logs? | PASS | -| 13 | Connect a GitHub repository? | PASS | -| 14 | Create a team? | PASS | -| 15 | CLI exit code for auth failure? | PASS | -| 16 | MCP server URL + transport? | PASS | -| 17 | Run a cron on demand? | PASS | -| 18 | Open a SQL console? | PASS | -| 19 | Password-protect a path? | PASS | -| 20 | Webhook signature header + algorithm? | PASS | - -**Score: 19/20 questions pass.** Q8 fails because of 2 real bugs in that -one question. - -### Bug 1 — in the frozen generated reference (not fixed here) - -`generated/cli/index.md` documents `sf api-keys create --preset` with a -7-value enum (`ci_deploy`, `space_publisher`, `space_admin`, -`domain_manager`, `team_admin`, `billing_viewer`, `partner_admin`) — then its -own usage example runs `--preset full_access`, a value that isn't in that -list. Anyone who copies the example gets a validation error. This exact text -is also baked into `content/cli/reference.md` (the materialized build -overlay). Per `AGENTS.md`, generated content is fixed at its source in the -product monorepo and re-exported, never hand-edited here — **this needs a -fix in the monorepo's CLI source**, flagged, not touched, in this repo. - -### Bug 2 — in hand-authored content (not fixed here, flagging for a decision) - -`content/(account)/api-keys.mdx`'s preset table lists only 6 of the 7 real -presets (missing `partner_admin` entirely), and contradicts itself on the -default: line 34 says `sf api-keys create` defaults to `space_publisher` -(matching the generated reference); line 60 says `space_admin` is "the -default when a request names no preset." Unlike Bug 1, this one *is* -editable here — it's authored content, not generated — but fixing it needs -a judgment call on what the second "default" claim actually means (CLI vs. -a different code path), which this test suite can't resolve on its own. -Left for a deliberate follow-up rather than guessing. - -## Running this again - -- **Part 1** needs a human or an agent with browser access, the dev server - running (`bun run dev`), and ~15–20 minutes. No automation exists for - this yet — it's a manual QA script, not a CI check. -- **Part 2** needs `grep`/`Read` access to `generated/cli/index.md` and - `generated/openapi/api.json`, cross-checked against whatever commands the - content pages show. Also manual today. A future version of this could - become a script (extract every fenced `sf ...` command from `content/**`, - parse `generated/cli/index.md`'s own usage/example blocks, diff them) — - out of scope for this pass, but the two real bugs found by hand suggest - it would pay for itself. +Known content gaps remain documented in the [point-in-time QA comparison](DOCS_QA_COMPARISON.md): no confirmed support channel and conflicting API-key preset guidance. That comparison records manual observations, not test-suite results. The [writing audit](DOCS_WRITING_QA.md) records before/after editorial signals and its limits. diff --git a/DOCS_WRITING_QA.md b/DOCS_WRITING_QA.md index a18ea664..de7b480a 100644 --- a/DOCS_WRITING_QA.md +++ b/DOCS_WRITING_QA.md @@ -1,6 +1,6 @@ # Authored-docs writing audit -The click-path suite in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md) measures whether a reader can find an answer. The command suite checks facts against the generated reference. Neither measures whether the authored prose is better. This point-in-time audit checks the writing changes against `main` as of October 5, 2026. +The manual click-path and command comparisons in [DOCS_QA_COMPARISON.md](DOCS_QA_COMPARISON.md) measure answer findability and factual accuracy. Neither measures whether the authored prose is better. This point-in-time audit checks the writing changes against `main` as of October 5, 2026. Repeatable checks for the current docs are in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). ## Method diff --git a/package.json b/package.json index 605231da..6bad91ef 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "verify:public-safety": "node scripts/verify-public-safety.mjs", "verify:commands": "node scripts/verify-command-examples.mjs", "verify:routes": "node scripts/verify-routes.mjs", + "test:docs": "bun test scripts/*.test.mjs", "test:corpus": "node --test scripts/build-docs-corpus.test.mjs", "test:llms": "node --test scripts/build-llms-index.test.mjs", "doctor": "blume doctor" diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs new file mode 100644 index 00000000..7b492c6d --- /dev/null +++ b/scripts/docs-experience.test.mjs @@ -0,0 +1,133 @@ +import assert from "node:assert/strict"; +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const content = path.join(root, "content"); +const dist = path.join(root, "dist"); + +async function builtPage(route) { + const name = route === "/" ? "index" : route.slice(1); + const source = await readFile(path.join(dist, `${name}.md`), "utf8"); + return source.replace(/^---\n[\s\S]*?\n---\n/u, "").trimStart(); +} + +function opening(page) { + const paragraph = page.split(/\n\s*\n/u).find((block) => { + const text = block.trim(); + return text && !/^(#|<|:::|```|\||- |\d+\. )/u.test(text); + }); + assert.ok(paragraph, "page should have an opening paragraph"); + return paragraph.replace(/\s+/gu, " "); +} + +async function* authoredPages(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const location = path.join(directory, entry.name); + if (entry.isDirectory()) yield* authoredPages(location); + else if (entry.name.endsWith(".mdx")) yield location; + } +} + +test("every authored page opens with its subject instead of a page promise", async () => { + const offenders = []; + let count = 0; + for await (const file of authoredPages(content)) { + const source = await readFile(file, "utf8"); + const body = source.replace(/^---\n[\s\S]*?\n---\n/u, "").trimStart(); + const lead = opening(body); + count += 1; + if (/^(after this page|by the end of this page)\b/iu.test(lead)) { + offenders.push(path.relative(root, file)); + } + } + assert.ok(count > 50, "the authored corpus should be present"); + assert.deepEqual(offenders, [], "generic page promises hide the useful first fact"); +}); + +test("a browser-first reader sees Drop before the CLI install on the homepage", async () => { + const home = await builtPage("/"); + const publish = await builtPage("/publish"); + const drop = home.indexOf("no install, no account required"); + const install = home.indexOf("npm install -g spacefast"); + assert.ok(drop >= 0 && install > drop, "show Drop before the install command"); + assert.match(home, /\[Drop\]\(\/docs\/publish#publish\)/u); + assert.match(publish.replaceAll("**", ""), /Drop takes a folder/iu); +}); + +test("Quickstart gives a no-install route before the CLI steps", async () => { + const quickstart = await builtPage("/quickstart"); + const drop = quickstart.indexOf("Don't want to install anything?"); + const install = quickstart.indexOf("Install the CLI"); + assert.ok(drop >= 0 && install > drop, "offer Drop before CLI instructions"); + assert.match(quickstart.slice(drop, install), /\[Drop\]\(\/docs\/publish#publish\)/u); +}); + +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"]) { + assert.match(glossary, new RegExp(`^## ${term}(?:\\b| \\()`, "mu"), `${term} needs a definition`); + } +}); + +test("Database gives the Zero prerequisite before query instructions", async () => { + const database = await builtPage("/database"); + 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), /kind: "zero"/u); + assert.match(database.slice(prerequisite, query), /sf init --runtime zero/u); +}); + +test("Troubleshooting gives a first diagnostic step before the error catalog", async () => { + const troubleshooting = await builtPage("/troubleshooting"); + const overview = troubleshooting.indexOf("Overview"); + const errors = troubleshooting.indexOf("## Publishing"); + assert.ok(overview >= 0 && errors > overview, "start with the Space Overview"); + assert.match(troubleshooting.slice(0, errors), /last publish and its status/iu); +}); + +const firstFactCases = [ + { + route: "/versions", + question: "Does rollback rebuild?", + evidence: [/rollback/iu, /doesn't rebuild|does not rebuild/iu, /live/iu], + }, + { + route: "/crons", + question: "Where is a schedule set, and when does it take effect?", + evidence: [/no dashboard editor/iu, /sf\.jsonc/u, /publish/iu], + }, + { + route: "/environment-variables", + question: "Which variable wins when Space and team names collide?", + evidence: [/Space-level value/iu, /wins/iu, /team/iu], + }, + { + route: "/access", + question: "What makes a Space private?", + evidence: [/removing every one/iu, /private/iu, /grant/iu], + }, + { + route: "/caching", + question: "How do I force a fresh response?", + evidence: [/republish/iu, /fresh response/iu], + }, + { + route: "/stats", + question: "Does traffic count crawlers?", + evidence: [/crawler traffic/iu, /left out|excluded/iu], + }, +]; + +for (const { route, question, evidence } of firstFactCases) { + test(`${route} answers “${question}” in its opening paragraph`, async () => { + const lead = opening(await builtPage(route)); + for (const pattern of evidence) assert.match(lead, pattern); + }); +} From 0829e4a1d83f0830211aa299c804b333a9d92a48 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Mon, 5 Oct 2026 16:30:55 -0400 Subject: [PATCH 16/20] Document docs evaluation and team guidance --- DOCS_QA_COMPARISON.md | 273 ++++++++++++++++++++------- DOCS_TEST_SUITE.md | 2 +- DOCS_WRITING_QA.md | 4 +- content/(concepts)/teams.mdx | 6 + content/(dynamic)/zero-runtime.mdx | 2 +- content/cli/index.mdx | 2 +- content/index.mdx | 5 + content/quickstart.mdx | 5 + evals.yaml | 12 +- package.json | 2 +- scripts/audit-composed-site.test.mjs | 5 +- scripts/docs-experience.test.mjs | 27 +-- 12 files changed, 245 insertions(+), 100 deletions(-) diff --git a/DOCS_QA_COMPARISON.md b/DOCS_QA_COMPARISON.md index aec945cf..c2cfc22b 100644 --- a/DOCS_QA_COMPARISON.md +++ b/DOCS_QA_COMPARISON.md @@ -1,22 +1,44 @@ -# Point-in-time docs QA comparison +# Old and new docs evaluation -This report records two manual 20-question comparisons made before the PR. -The results are evidence for this edit, not an executable regression suite. -The runnable checks are documented in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). -`evals.yaml` separately covers an AI assistant's factual retrieval from the -docs. +This report compares the built `main` docs with this branch using the same +questions and checks on both versions. It includes two 20-question reviewer +comparisons, the 23-question docs-only agent eval, and the 12 targeted +regression checks. The runnable checks are documented in +[DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). -These comparisons do not grade prose quality. The before/after writing audit is in +**Conclusion:** The evidence establishes a small improvement in findability and +one repaired task prerequisite. It does not establish a broad improvement in +reader comprehension or task completion. The 20 questions were reviewed by an +evaluator, not tested with real users. The 12 new regression checks were written +for this change, so their pass-rate difference cannot stand in for an +independent outcome measure. Do not use this report to claim that the rewrite +as a whole is better for readers. The before/after writing audit is in [DOCS_WRITING_QA.md](DOCS_WRITING_QA.md). -- **Part 1 — can a real user find the answer, stated clearly, in 3 clicks or - less?** Tests navigation and clarity, not content accuracy. +- **Part 1 — can a reviewer find the answer, stated clearly, in 3 clicks or + less?** Tests navigation and answer presence, not reader comprehension or + content accuracy. - **Part 2 — is the exact command/code shown actually correct?** Tests content accuracy against the frozen, producer-owned reference (`generated/cli/`, `generated/openapi/`), not navigation. -Both are point-in-time observations, not a standing guarantee or a CI gate. -See "Running this again" below for the manual method. +The manual results are point-in-time observations, not a CI gate. The agent +results are single runs and can vary between runs. The 20 user questions do not +cover most of the 78 edited authored pages, so they cannot validate the rewrite +as a whole. See "Running this again" below for the method. + +To support a broader claim, ask readers unfamiliar with both versions to do +the same representative tasks on each site, with site order balanced between +readers. Record task completion without assistance, wrong turns, and time to +the correct answer. Include tasks from pages whose openings changed, not only +the Database and publishing paths. Keep the task list and scoring rules fixed +before testing; report per-task results and failures alongside any aggregate. + +The later team-benefit edits on the homepage, Quickstart, and Teams page were +not part of the 20-question comparison. Q15 checks whether an evaluator can +find invitation instructions; it does not test whether readers understand why +to use a team. Those edits have build and route verification but no measured +reader outcome yet. ## Current main versus this branch (October 5, 2026) @@ -26,8 +48,10 @@ generated CLI and API reference trees are identical on both branches. | Test | Current `main` | This branch | Change | |---|---:|---:|---| -| Part 1: clear answer in at most 3 clicks | 18/20 | 19/20 | +1 question | +| Part 1: reviewer finds clear answer in at most 3 clicks | 18/20 | 19/20 | +1 question | | Part 2: command/API accuracy against generated reference | 19/20 | 19/20 | No change | +| Targeted regression checks | 0/12 | 12/12 | Checks written for this change | +| Corrected docs-only agent retrieval | 23/23 | 23/23 | No factual retrieval regression | Part 1 improvement comes from Q11: the Database page now tells readers how to enable Zero before using its database (one click on both sites; the old @@ -43,37 +67,44 @@ API-key page omits `partner_admin` while giving conflicting default-preset guidance. The checks compare documentation with the generated reference; they do not execute a live publish or API request. -## Part 1: Manual user click-path comparison (20 questions) +The same 26-test executable suite was run on both built sites. The 14 existing +corpus, index, and audit unit tests pass on each. The 12 new experience checks +fail on `main` and pass on this branch. They were written for the changed +reader journeys, so their before/after result shows that the intended edits +landed and are protected against regression. It is not independent evidence +that the edits improve the reader experience. -Methodology: every test starts fresh from the homepage -(`/docs/`), using only real navigation a user would use +## Part 1: Reviewer click-path comparison (20 user questions) + +Methodology: an evaluator starts every test fresh from the homepage +(`/docs/`), using navigation a reader could use (nav bar, sidebar, tabs, in-page links/cards). A "click" is one navigation action. "Stated clearly" means the answer is plain and near the top of where you land — not something inferred from paragraphs of surrounding technical detail. -| # | Question | Clicks | Landed on | Verdict | +| # | Question | `main` | This branch | Destination / change | |---|---|---|---|---| -| 1 | Publish without installing anything? | 0 | Homepage | PASS | -| 2 | How much does it cost? | 1 | `/billing` | PASS | -| 3 | Add a custom domain? | 1 | `/domains` | PASS | -| 4 | Undo a bad publish? | 1 | `/versions` | PASS | -| 5 | Publish without an account — what happens? | 1 | `/anonymous-and-claim` | PASS | -| 6 | Let one specific person see my site? | 1 | `/access` | PASS | -| 7 | Put a password on my site? | 1 | `/access` | PASS | -| 8 | What exactly is a "Space"? | 1 | `/spaces` | PASS | -| 9 | Connect Claude to publish for me? | 1 | `/agents` | PASS | -| 10 | Does it work with Next.js? | 1 | `/recipes/next` | PASS | -| 11 | Add a database to my site? | 1 | `/database` | PASS (initial failure fixed) | -| 12 | My build failed — what do I do? | 1 | `/troubleshooting` | PASS | -| 13 | See my site's traffic? | 1 | `/stats` | PASS | -| 14 | Use my own logo on error pages? | 1 | `/customization` | PASS (`/site-pages` adds error-page detail in a second click) | -| 15 | Invite a teammate? | 1 | `/teams` | PASS | -| 16 | Free plan limits? | 1 | `/limits` | PASS | -| 17 | Connect GitHub for auto-deploy? | 1 | `/git` | PASS | -| 18 | Schedule something hourly? | 1 | `/crons` | PASS | -| 19 | Something broke — where do I get help? | 1 | `/troubleshooting` | **FAIL — real gap, not fixed** | -| 20 | Can I resell this under my own brand? | 1 | `/platforms` | PASS | +| 1 | Publish without installing anything? | Pass, 2 actions | Pass, 0 actions | Homepage now states the dashboard Drop path before CLI install. | +| 2 | How much does it cost? | Pass, 1 | Pass, 1 | `/billing` | +| 3 | Add a custom domain? | Pass, 1 | Pass, 1 | `/domains`; the new lead states the capability directly. | +| 4 | Undo a bad publish? | Pass, 1 | Pass, 1 | `/versions`; the new lead says rollback does not rebuild. | +| 5 | Publish without an account — what happens? | Pass, 1 | Pass, 1 | `/anonymous-and-claim` | +| 6 | Let one specific person see my site? | Pass, 1 | Pass, 1 | `/access` | +| 7 | Put a password on my site? | Pass, 1 | Pass, 1 | `/access` | +| 8 | What exactly is a "Space"? | Pass, 1 | Pass, 1 | `/spaces`; the new lead defines it. | +| 9 | Connect Claude to publish for me? | Pass, 1 | Pass, 1 | `/agents` | +| 10 | Does it work with Next.js? | Pass, 1 | Pass, 1 | `/recipes/next` | +| 11 | Add a database to my site? | Fail, 1 | Pass, 1 | `/database` now gives the Zero setup prerequisite before query instructions. | +| 12 | My build failed — what do I do? | Pass, 1 | Pass, 1 | `/troubleshooting` now starts with the Space Overview diagnostic step. | +| 13 | See my site's traffic? | Pass, 1 | Pass, 1 | `/stats`; the new lead also states that crawler traffic is excluded. | +| 14 | Use my own logo on error pages? | Pass, 1 | Pass, 1 | `/customization`; `/site-pages` adds layout detail in a second action. | +| 15 | Invite a teammate? | Pass, 1 | Pass, 1 | `/teams` | +| 16 | Free plan limits? | Pass, 1 | Pass, 1 | `/limits` | +| 17 | Connect GitHub for auto-deploy? | Pass, 1 | Pass, 1 | `/git`; the new lead names the GitHub route. | +| 18 | Schedule something hourly? | Pass, 1 | Pass, 1 | `/crons`; the new lead names `sf.jsonc` and publish timing. | +| 19 | Something broke — where do I get help? | Fail, 1 | Fail, 1 | `/troubleshooting` still gives no support channel. | +| 20 | Can I resell this under my own brand? | Pass, 1 | Pass, 1 | `/platforms` | **Candidate score: 19/20 pass at ≤3 clicks, 1 unresolved failure.** Q11 failed on the first pass and was fixed before this result was recorded. @@ -109,28 +140,28 @@ executed against a live backend (none available in this environment). "Works" means the command, flag, or endpoint shown is real, current, and internally consistent with the generated reference. -| # | Question | Result | -|---|---|---| -| 1 | Install command (npm)? | PASS | -| 2 | Publish the current directory? | PASS | -| 3 | Force upload vs. force build? | PASS | -| 4 | List all versions? | PASS | -| 5 | Roll back to a version? | PASS | -| 6 | Add a domain as primary? | PASS | -| 7 | Check domain/DNS verification? | PASS | -| 8 | Create an API key with a preset? | **FAIL — 2 bugs found** | -| 9 | curl to publish via HTTP API? | PASS | -| 10 | Bearer-token header? | PASS | -| 11 | Set an environment variable? | PASS | -| 12 | View build logs? | PASS | -| 13 | Connect a GitHub repository? | PASS | -| 14 | Create a team? | PASS | -| 15 | CLI exit code for auth failure? | PASS | -| 16 | MCP server URL + transport? | PASS | -| 17 | Run a cron on demand? | PASS | -| 18 | Open a SQL console? | PASS | -| 19 | Password-protect a path? | PASS | -| 20 | Webhook signature header + algorithm? | PASS | +| # | Question | `main` | This branch | +|---|---|---|---| +| 1 | Install command (npm)? | Pass | Pass | +| 2 | Publish the current directory? | Pass | Pass | +| 3 | Force upload vs. force build? | Pass | Pass | +| 4 | List all versions? | Pass | Pass | +| 5 | Roll back to a version? | Pass | Pass | +| 6 | Add a domain as primary? | Pass | Pass | +| 7 | Check domain/DNS verification? | Pass | Pass | +| 8 | Create an API key with a preset? | Fail | Fail | +| 9 | curl to publish via HTTP API? | Pass | Pass | +| 10 | Bearer-token header? | Pass | Pass | +| 11 | Set an environment variable? | Pass | Pass | +| 12 | View build logs? | Pass | Pass | +| 13 | Connect a GitHub repository? | Pass | Pass | +| 14 | Create a team? | Pass | Pass | +| 15 | CLI exit code for auth failure? | Pass | Pass | +| 16 | MCP server URL + transport? | Pass | Pass | +| 17 | Run a cron on demand? | Pass | Pass | +| 18 | Open a SQL console? | Pass | Pass | +| 19 | Password-protect a path? | Pass | Pass | +| 20 | Webhook signature header + algorithm? | Pass | Pass | **Score: 19/20 questions pass.** Q8 fails because of 2 real bugs in that one question. @@ -159,15 +190,121 @@ a judgment call on what the second "default" claim actually means (CLI vs. a different code path), which this comparison can't resolve on its own. Left for a deliberate follow-up rather than guessing. +## Part 3: Built-docs reading-experience checks (12 tests) + +The same `scripts/docs-experience.test.mjs` file ran against each separately +built site. All 12 fail on `main` and pass on this branch. The old build's +existing 14 corpus, index, and audit unit tests pass, as do all 14 on this +branch, making the complete suite **14/26 versus 26/26**. This suite was +designed to guard the paths changed in this PR; its baseline failure rate +should not be generalized to the quality of every old docs page. + +| Check against the authored or built page | `main` | This branch | +|---|---|---| +| Authored pages open with their subject, not a templated page promise | Fail | Pass | +| Homepage offers no-install Drop before the CLI install | Fail | Pass | +| Quickstart offers no-install Drop before CLI steps | Fail | Pass | +| Homepage links unfamiliar terms to glossary definitions | Fail | Pass | +| Database gives the Zero prerequisite before query instructions | Fail | Pass | +| Troubleshooting gives a first diagnostic step before the error catalog | Fail | Pass | +| Versions lead says whether rollback rebuilds | Fail | Pass | +| Crons lead says where and when a schedule takes effect | Fail | Pass | +| Environment variables lead says which scope wins | Fail | Pass | +| Access lead says what makes a Space private | Fail | Pass | +| Caching lead says how to force a fresh response | Fail | Pass | +| Traffic stats lead says whether crawlers count | Fail | Pass | + +The writing comparison in [DOCS_WRITING_QA.md](DOCS_WRITING_QA.md) covers the +larger edit: 65 changed opening paragraphs across 78 edited existing pages, +with 53 templated openings reduced to zero and median lead length moving from +34 to 23 words. Those are structural and editorial signals, not a reader +comprehension score. + +## Part 4: Docs-only agent evaluation (23 questions) + +Blume gives Codex only the built docs through its search, page, and navigation +tools. For each question, a separate judge checks the answer against the +expected facts. Both builds use the same corrected `evals.yaml`, the same +Blume version, and the same agent CLI. Each result is one run, so a difference +needs a repeat before we attribute it to the writing. + +The first pass exposed four defects in the eval questions or grading key. We +corrected them before the final side-by-side run: + +- **Rate limits:** The question asked only for the credential limit while the + key also required the unauthenticated IP limit. It now asks for both. +- **Plus price:** The key said `$4.99`, but both built Billing pages say the + upcoming Plus base price is `$15` per team per month. The question now + names those upcoming terms. +- **Free file limit:** The key assigned the anonymous `50 MiB` cap to the + claimed Free plan. The docs say `1 GiB` for a claimed Free plan and + `50 MiB` for anonymous publishing. The question now asks for both. +- **Anonymous claim:** The original question conflated the serving deadline + with the later recovery period. It now asks separately when an anonymous + Space stops serving and how long the key remains valid for claiming. + +The original, uncorrected pass is excluded from the headline comparison. It +gave a false failure on both builds for the first three items. It also gave +one old-only failure for anonymous claiming that passed when rerun unchanged +on both builds. That result did not establish a docs improvement. + +The corrected full run passed **23/23 on `main` and 23/23 on this branch**, +with no errors or skips. This establishes factual retrieval on these 23 +questions in this run. It does not grade prose quality or real agent task +completion. The full agent run used the earlier builds made with Bun 1.4.2. +Both sites were then rebuilt with the repository's pinned Bun 1.3.11. Two +Vale-only wording edits changed the Zero and CLI pages; their affected agent +questions (18 and 22) were rerun against the final build and both passed. + +| # | Agent question | `main` | This branch | +|---|---|---|---| +| 1 | Minimum Node.js version for the `sf` CLI? | Pass | Pass | +| 2 | New Space hostname and single-version URL? | Pass | Pass | +| 3 | `sf login` code validity and credential lifetime? | Pass | Pass | +| 4 | Anonymous serving deadline and later claim period? | Pass | Pass | +| 5 | DNS records to connect `example.com`? | Pass | Pass | +| 6 | Does `sf domains rm` delete the domain? | Pass | Pass | +| 7 | Authentication-failure CLI exit code? | Pass | Pass | +| 8 | HTML cache header and cache busting? | Pass | Pass | +| 9 | List-page sizes and pagination? | Pass | Pass | +| 10 | Authenticated and unauthenticated API request limits? | Pass | Pass | +| 11 | `Idempotency-Key` retention? | Pass | Pass | +| 12 | Hosted Spacefast MCP server URL? | Pass | Pass | +| 13 | MCP execute sandbox limits? | Pass | Pass | +| 14 | Team roles? | Pass | Pass | +| 15 | Upcoming Plus base price? | Pass | Pass | +| 16 | Claimed Free and anonymous single-file limits? | Pass | Pass | +| 17 | Project config filename? | Pass | Pass | +| 18 | Turn on the Zero runtime? | Pass | Pass | +| 19 | `_redirects` versus `sf.jsonc` rule precedence? | Pass | Pass | +| 20 | Does `sf publish` upload `node_modules` and `.git`? | Pass | Pass | +| 21 | Spacefast API-key prefix? | Pass | Pass | +| 22 | CLI behavior with `CI=1`? | Pass | Pass | +| 23 | Does rollback rebuild? | Pass | Pass | + ## Running this again -- **Part 1** needs a human or an agent with browser access, the dev server - running (`bun run dev`), and ~15–20 minutes. No automation exists for - this yet — it's a manual QA script, not a CI check. -- **Part 2** needs `grep`/`Read` access to `generated/cli/index.md` and - `generated/openapi/api.json`, cross-checked against whatever commands the - content pages show. Also manual today. A future version of this could - become a script (extract every fenced `sf ...` command from `content/**`, - parse `generated/cli/index.md`'s own usage/example blocks, diff them) — - out of scope for this pass, but the two real bugs found by hand suggest - it would pay for itself. +- Build current `main` and this branch in separate clean checkouts with + Bun 1.3.11, Node 24 or newer, and `bun run build` in each. The comparison + here used Blume 2.0.3 and Codex CLI 0.160.0 on October 5, 2026. +- **Part 1:** Start each browser check at `/docs/`. Follow only rendered + navigation, tabs, and page links; count each navigation action. Read the + destination answer, not just its title. The 20-question click-path pass is + manual. We also checked the rebuilt homepage and Database page in a browser + and inspected all 20 built destination pages side by side. +- **Part 2:** Cross-check the 20 command/API answers against the same + `generated/cli/index.md` and `generated/openapi/api.json` in each build. + `bun run verify:commands` checks examples more broadly, but this 20-question + comparison is still a manual cross-check rather than a live API test. +- **Part 3:** Run the candidate's `scripts/docs-experience.test.mjs` against + each built checkout. For the old checkout, copy that test and the Node-based + `scripts/audit-composed-site.test.mjs` into a temporary copy, then run + `node --test scripts/*.test.mjs`. On this branch, `bun run test:docs` runs + the same Node test command. The old checkout should fail the 12 new + experience checks while passing the 14 existing unit checks. +- **Part 4:** Run `./node_modules/.bin/blume eval --agent=codex + --file=evals.yaml --json` in this branch, and point the old checkout's + `--file` argument at the **same** corrected `evals.yaml`. Blume runs one + reader and one judge per question. Keep failures, errors, and skips separate; + repeat a differing result before claiming an improvement. The agent's model + was not pinned, so these single-run scores are not deterministic. diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md index 4659d829..81b9455e 100644 --- a/DOCS_TEST_SUITE.md +++ b/DOCS_TEST_SUITE.md @@ -7,7 +7,7 @@ bun run build bun run test:docs ``` -CI runs `test:docs` after the production build. It uses Bun's test runner and exits nonzero on a failed assertion. The experience checks read the built Markdown in `dist/`, so a passing source edit alone cannot satisfy them. Existing unit tests for the docs corpus, LLM index, and composed-site audit run in the same command. +CI runs `test:docs` after the production build. It uses Node's test runner and exits nonzero on a failed assertion. The experience checks read the built Markdown in `dist/`, so a passing source edit alone cannot satisfy them. Existing unit tests for the docs corpus, LLM index, and composed-site audit run in the same command. ## Reader-facing contracts diff --git a/DOCS_WRITING_QA.md b/DOCS_WRITING_QA.md index de7b480a..d18621b0 100644 --- a/DOCS_WRITING_QA.md +++ b/DOCS_WRITING_QA.md @@ -1,6 +1,6 @@ # Authored-docs writing audit -The manual click-path and command comparisons in [DOCS_QA_COMPARISON.md](DOCS_QA_COMPARISON.md) measure answer findability and factual accuracy. Neither measures whether the authored prose is better. This point-in-time audit checks the writing changes against `main` as of October 5, 2026. Repeatable checks for the current docs are in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). +The reviewer click-path and command comparisons in [DOCS_QA_COMPARISON.md](DOCS_QA_COMPARISON.md) measure answer findability and factual accuracy. Neither measures whether the authored prose is better for readers. This point-in-time audit checks the writing changes against `main` as of October 5, 2026. Repeatable checks for the current docs are in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). ## Method @@ -37,4 +37,4 @@ For example, Versions opened with “After this page you know what a version hol The manual review caught five candidate leads that were shorter but spent the first sentence on a less useful detail: slug validation on Spaces, polling mechanics on Logs, archive flags on Frameworks and builds, CLI naming on Publish from Git, and remote WP-CLI output on WordPress. Each now leads with the page's main task or mental model. The removed detail was checked elsewhere on the same page and retained or moved into the body. -This audit establishes changes in structure and in which facts appear first. It does not show that real readers complete tasks faster or understand the docs better. That requires reader testing. Vale still reports the two existing spelling alerts (`GETs` and `TTYs`); its rules do not detect repeated sentence structures or judge whether a page leads with the right fact. +This audit establishes changes in structure and in which facts appear first. It does not show that real readers complete tasks faster or understand the docs better. Shorter openings could also lose useful context for some readers. That requires reader testing. Vale now passes after two existing technical plurals (`GETs` and `TTYs`) were written as plain explanations; its rules do not detect repeated sentence structures or judge whether a page leads with the right fact. diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index b69c676c..696a5fa1 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -7,6 +7,12 @@ sidebar: A team owns everything — Spaces, domains, API keys, billing, members — and the role you hold there caps what you can do with any of it. +## Why work in a team + +- **Publish together.** Members can create Spaces, publish new versions, and roll back. The site stays with the team when a member leaves. +- **Keep access in one place.** Invite someone to the team to give them access to team-owned work at their role. Remove them to take that access away across the team's Spaces. Set the default access for each new Space to `team`, `private`, or `public`. +- **Separate routine work from administration.** Members can publish, while owners and admins manage invitations, domains, and billing. The [role table](#roles) shows the full boundary. + ## What a team is A team is the thing that owns work. Spaces, custom domains, API keys, billing, and members all hang off it. Every claimed Space belongs to exactly one team, even when you are its only member. There is no separate personal container to reason about. diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 1e3d490e..15d5449b 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -50,7 +50,7 @@ sf publish | 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 GETs against your own paths, declared in `sf.jsonc`. | [Crons](/crons) | +| Crons | Scheduled `GET` requests to your own paths, declared in `sf.jsonc`. | [Crons](/crons) | | Storage | Runtime object storage for visitor uploads. | [Storage](/storage) | | Logs | What your handlers wrote, plus every request the edge served. | [Logs](/logs) | diff --git a/content/cli/index.mdx b/content/cli/index.mdx index bc8eb3eb..6e98f3b2 100644 --- a/content/cli/index.mdx +++ b/content/cli/index.mdx @@ -105,7 +105,7 @@ Commands that act on a Space add `--space`, `-o, --team`, and `--claim-token`. C ## Interactive and non-interactive -The CLI prompts only when stdin and stdout are both TTYs, `--json` is absent, and neither `CI` nor `SPACEFAST_NON_INTERACTIVE` is set. Otherwise it never prompts. +The CLI prompts only when stdin and stdout are both connected to a terminal, `--json` is absent, and neither `CI` nor `SPACEFAST_NON_INTERACTIVE` is set. Otherwise it never prompts. Destructive actions need an answer either way. Pass `--yes` (or set `SPACEFAST_YES=1`) to proceed without a prompt. Without it, a non-interactive run fails with `confirmation_required` and exit 2. Declining a prompt exits 130. diff --git a/content/index.mdx b/content/index.mdx index 0991633e..3e420188 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -43,6 +43,8 @@ A **Space** is one site with a stable hostname. Every publish creates a new **ve 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 opt into the Zero runtime with one line of config. +When more than one person works on a site, its team owns the Space, domains, and API keys. Members can publish and roll back without passing around one person's login, while 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. + Install, sign in, publish, and attach a domain in five minutes. @@ -59,6 +61,9 @@ Static output is the default path. Point `sf publish` at a build directory and i DNS records, verification, TLS, and the primary domain. + + Invite collaborators, share Spaces, and control who can change what. + Connect Claude Code, Cursor, Codex, or any MCP client. diff --git a/content/quickstart.mdx b/content/quickstart.mdx index ab868325..6ef53fbc 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -95,7 +95,12 @@ Don't want to install anything? Open the dashboard and drag a folder, a `.zip`, ## Next +Working with other people? Claim your Space to a team, then [invite members](/teams#invite-someone). Team members can publish and roll back; owners and admins can manage domains and invitations. You can set whether new Spaces start open to the team, private, or public in [team access settings](/teams#default-access-for-new-spaces). + + + Invite members, choose roles, and set access for new Spaces. + Install the Spacefast plugin for Claude Code, Cursor, or Codex. diff --git a/evals.yaml b/evals.yaml index db41262f..a554ef01 100644 --- a/evals.yaml +++ b/evals.yaml @@ -13,7 +13,7 @@ questions: expected: ["30 minutes", "30 days", "7 days without use"] routes: /authentication - id: anonymous-lifetime - question: If I publish without an account, how long do I have to claim the Space? + question: After publishing without an account, when does the Space stop serving, and how long after that can its key still be used to claim it? expected: ["33 hours and 20 minutes", "7 days"] routes: /anonymous-and-claim - id: domain-dns-records @@ -37,7 +37,7 @@ questions: expected: ["default 20", "maximum 100", "pass nextCursor as cursor"] routes: /api/pagination - id: rate-limit - question: How many API requests per minute can one credential make? + question: What are the API request limits per minute for an authenticated credential and an unauthenticated client IP? expected: ["600 per minute per credential", "300 per minute per IP"] routes: /api/rate-limits - id: idempotency-retention @@ -57,12 +57,12 @@ questions: expected: ["owner", "admin", "member"] routes: /teams - id: plans - question: What does the Plus plan cost per month? - expected: ["$4.99"] + question: Under the documented upcoming plan terms, what is the Plus base price per team per month before extra seats or usage? + expected: ["$15 per team per month"] routes: /billing - id: free-file-limit - question: What is the largest single file I can publish on the Free plan? - expected: ["50 MiB"] + question: What is the largest single file on a claimed Free plan, and how does the cap differ for an anonymous publish? + expected: ["1 GiB on the claimed Free plan", "50 MiB for an anonymous publish"] routes: /limits - id: config-filename question: What is the project config file called? diff --git a/package.json b/package.json index 6bad91ef..4c42fe3d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "verify:public-safety": "node scripts/verify-public-safety.mjs", "verify:commands": "node scripts/verify-command-examples.mjs", "verify:routes": "node scripts/verify-routes.mjs", - "test:docs": "bun test scripts/*.test.mjs", + "test:docs": "node --test scripts/*.test.mjs", "test:corpus": "node --test scripts/build-docs-corpus.test.mjs", "test:llms": "node --test scripts/build-llms-index.test.mjs", "doctor": "blume doctor" diff --git a/scripts/audit-composed-site.test.mjs b/scripts/audit-composed-site.test.mjs index a92e7477..ca02a5b1 100644 --- a/scripts/audit-composed-site.test.mjs +++ b/scripts/audit-composed-site.test.mjs @@ -1,4 +1,5 @@ -import { expect, test } from "bun:test"; +import assert from "node:assert/strict"; +import test from "node:test"; import { unexpectedAuditErrors } from "./audit-composed-site.mjs"; @@ -50,7 +51,7 @@ test("allows only exact Website-owned composition dependencies", () => { }, ]; - expect(unexpectedAuditErrors(diagnostics, ["/cookie-banner.js", "/help"])).toEqual([ + assert.deepEqual(unexpectedAuditErrors(diagnostics, ["/cookie-banner.js", "/help"]), [ diagnostics[2], diagnostics[6], ]); diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index 7b492c6d..6fe5e284 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -1,11 +1,10 @@ import assert from "node:assert/strict"; -import { readFile, readdir } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; const root = fileURLToPath(new URL("..", import.meta.url)); -const content = path.join(root, "content"); const dist = path.join(root, "dist"); async function builtPage(route) { @@ -23,27 +22,19 @@ function opening(page) { return paragraph.replace(/\s+/gu, " "); } -async function* authoredPages(directory) { - for (const entry of await readdir(directory, { withFileTypes: true })) { - const location = path.join(directory, entry.name); - if (entry.isDirectory()) yield* authoredPages(location); - else if (entry.name.endsWith(".mdx")) yield location; - } -} - test("every authored page opens with its subject instead of a page promise", async () => { const offenders = []; - let count = 0; - for await (const file of authoredPages(content)) { - const source = await readFile(file, "utf8"); - const body = source.replace(/^---\n[\s\S]*?\n---\n/u, "").trimStart(); - const lead = opening(body); - count += 1; + const manifest = JSON.parse(await readFile(path.join(root, ".blume", "blume.manifest.json"), "utf8")); + const authored = manifest.routes.filter((route) => + route.source?.name === "filesystem" && route.entryId?.endsWith(".mdx"), + ); + for (const route of authored) { + const lead = opening(await builtPage(route.path)); if (/^(after this page|by the end of this page)\b/iu.test(lead)) { - offenders.push(path.relative(root, file)); + offenders.push(route.path); } } - assert.ok(count > 50, "the authored corpus should be present"); + assert.ok(authored.length > 50, "the authored corpus should be present"); assert.deepEqual(offenders, [], "generic page promises hide the useful first fact"); }); From 95dae2da086840b34f86f173155b4030eb1eca24 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Mon, 5 Oct 2026 16:56:46 -0400 Subject: [PATCH 17/20] Correct glossary scope and unpublished live state --- content/(reference)/glossary.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx index 6b9147ee..1236b2de 100644 --- a/content/(reference)/glossary.mdx +++ b/content/(reference)/glossary.mdx @@ -1,6 +1,6 @@ --- title: Glossary -description: Every Spacefast-specific term used across these docs, defined once, in one place +description: Core Spacefast terms used across these docs, defined in one place sidebar: order: 3 --- @@ -49,7 +49,7 @@ The thing that owns your Spaces, domains, API keys, and billing. Every claimed S ## Version -An immutable snapshot created by every publish, with its own permanent URL that never changes. The [live](#live-the-live-channel) pointer always points at exactly one version. See [Versions and the live channel](/versions). +An immutable snapshot created when a publish produces a new version. A ready version gets its own permanent URL that never changes. The [live](#live-the-live-channel) pointer names the version visitors see; it is empty before the first publish. See [Versions and the live channel](/versions). ## Zero From c5dcff2541d8201065a25209efc2dd3852ca941f Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Tue, 6 Oct 2026 10:35:43 -0400 Subject: [PATCH 18/20] Correct doc qualifiers and onboarding prerequisites from review --- content/(concepts)/teams.mdx | 4 ++++ content/(dynamic)/database.mdx | 2 +- content/(dynamic)/zero-runtime.mdx | 4 ++-- content/(publish)/anonymous-and-claim.mdx | 2 +- content/(serve)/caching.mdx | 6 +++--- content/(serve)/routing.mdx | 2 +- content/(serve)/stats.mdx | 4 ++-- content/agents/mcp-server.mdx | 4 ++-- content/index.mdx | 4 ++-- content/quickstart.mdx | 2 +- scripts/docs-experience.test.mjs | 11 +++++++++-- 11 files changed, 28 insertions(+), 17 deletions(-) diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index 696a5fa1..30cd858b 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -9,6 +9,8 @@ A team owns everything — Spaces, domains, API keys, billing, members — and t ## Why work in a team +Free and Go allow only the owner. Adding another member needs [Plus](/billing#seats). + - **Publish together.** Members can create Spaces, publish new versions, and roll back. The site stays with the team when a member leaves. - **Keep access in one place.** Invite someone to the team to give them access to team-owned work at their role. Remove them to take that access away across the team's Spaces. Set the default access for each new Space to `team`, `private`, or `public`. - **Separate routine work from administration.** Members can publish, while owners and admins manage invitations, domains, and billing. The [role table](#roles) shows the full boundary. @@ -55,6 +57,8 @@ Owner and admin differ in exactly one place. An admin can hand out `admin` and ` ## Invite someone +Your team needs [Plus](/billing#seats) to add another member. Free and Go include one editor, the owner. + Go to the team and open **Members**. The page has **Members** and **Pending** tabs. diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index b64481ce..041e7a05 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -8,7 +8,7 @@ sidebar: 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. :::note[Don't have a database yet?] -This page covers the database you get once [Zero](/zero-runtime) is running. Turn Zero on first — declare `kind: "zero"` in `sf.jsonc`, or run `sf init --runtime zero` to scaffold a new project with it. +This page covers the database you get once [Zero](/zero-runtime) is running. 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. ::: ## What it is diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 15d5449b..26a97ff0 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -5,13 +5,13 @@ sidebar: order: 0 --- -Every account gets Zero for free, but it only turns on when your `sf.jsonc` explicitly declares `kind: "zero"`. +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. ## 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 on for every account. There is nothing to enable. +There is no account-level switch to enable. The other runtime is [Functions](/functions), a worker for npm-heavy code and framework output like OpenNext. One version declares one runtime, not both. diff --git a/content/(publish)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index 23f923aa..2f5bf0dd 100644 --- a/content/(publish)/anonymous-and-claim.mdx +++ b/content/(publish)/anonymous-and-claim.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -There's exactly one copy of the key that lets you keep publishing — lose the folder you published from, and the Space runs out its clock with no way to get it back. +Keep the space key or claim link from your anonymous publish. Losing the publishing folder is recoverable if you saved either; losing every copy of the key leaves you unable to claim the Space before it expires. ## Publish with no login diff --git a/content/(serve)/caching.mdx b/content/(serve)/caching.mdx index 2963caf7..8eeeb8b5 100644 --- a/content/(serve)/caching.mdx +++ b/content/(serve)/caching.mdx @@ -1,14 +1,14 @@ --- title: Caching -description: The two cache policies a published Space sends, which files get which, and what a publish does to copies already out there +description: Default cache policies for public static files, header overrides, responses that are never cached, and what publishing refreshes sidebar: { order: 4 } --- -Every file in a published Space gets one of exactly two `Cache-Control` headers, and republishing is how you force a fresh response. +Republish to request a fresh response from the edge. Public static files use two default cache policies, with exceptions for protected responses, conditional routing, and header overrides. ## Two policies -A published Space sends one of two headers on its static files. There is nothing in between. +Public static files use these defaults unless a header override or a [no-cache rule](#responses-that-are-never-cached) applies. | Policy | `Cache-Control` | | --- | --- | diff --git a/content/(serve)/routing.mdx b/content/(serve)/routing.mdx index 4ca37efc..3e2344cc 100644 --- a/content/(serve)/routing.mdx +++ b/content/(serve)/routing.mdx @@ -4,7 +4,7 @@ description: The _redirects and _headers files, the routing rules in sf.jsonc, h sidebar: { order: 3 } --- -You can redirect, rewrite, proxy, and set response headers on a published Space, and when two rules overlap the first match always wins. +You can redirect, rewrite, proxy, and set response headers on a published Space. Redirect rules run in order, while matching header blocks accumulate. Rewrites and 404 rules can yield to real files. Nothing here is evaluated per request. Spacefast compiles your rules at publish time into a table the edge reads, so a rule that behaves unexpectedly is almost always a compile-time problem. `sf routing inspect` shows you the compiled result before you publish. diff --git a/content/(serve)/stats.mdx b/content/(serve)/stats.mdx index a3661cfc..7b2ae562 100644 --- a/content/(serve)/stats.mdx +++ b/content/(serve)/stats.mdx @@ -3,7 +3,7 @@ title: Traffic stats description: Requests, views, unique visitors, and top paths for a Space, where the numbers come from, and how to read them from the CLI and the API --- -Requests, views, and unique visitors each count something different on a Space's stats page, and crawler traffic is deliberately left out. +Views exclude crawlers and failed requests. Requests count every HTTP request, and unique visitors use a separate daily count without those filters. ## Where to look @@ -29,7 +29,7 @@ Buckets are labelled in UTC, not your timezone, because that is the boundary the ## What counts as a view -A **request** is any HTTP request the Space answered. A **view** is a successful request from something that is not a crawler: status under 400, assets included. So `views` is always lower than `requests`, and the gap is redirects, 404s, and bots. +A **request** is any HTTP request the Space answered. A **view** is a request from something that is not a crawler with a status under 400, assets included. So `views` cannot exceed `requests`, and the gap comes from crawlers and responses with a status of 400 or higher. **Unique visitors** come from a separate daily measurement. It ignores the crawler and status filters, and it merges every hostname mapped to the Space into one count. Daily is the finest resolution available, and days are never summed into a window total. That's why the page lists them one by one instead of showing a single number. diff --git a/content/agents/mcp-server.mdx b/content/agents/mcp-server.mdx index 7721d67e..1928d849 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -3,7 +3,7 @@ title: MCP server description: Connect to the Spacefast MCP server over hosted HTTP or local stdio, and use its five tools, execute sandbox, and approval model --- -Your client never actually holds a Spacefast API token — each authenticated request mints a short-lived delegation token instead, so a leaked MCP session can't be replayed against the wider API. +The hosted MCP server uses OAuth and accepts inline files. The local server uses your CLI login or `SPACEFAST_TOKEN` and can publish files from your workspace. :::note[Already connected through Claude Desktop or a one-click plugin?] You don't need this page. It's the technical reference for wiring up the MCP server by hand — see [Claude Desktop](/agents/claude-desktop) or [Claude Code](/agents/claude-code) for the point-and-click setup instead. @@ -58,7 +58,7 @@ Each tool declares the scopes it needs, and clients can read them from `tools/li A scope shortfall applies to the requested operation. A credential that can read analytics may still lack access to domains. -That delegation token lives five minutes and carries the source credential's policy verbatim. +The hosted server mints a delegation token for each authenticated request instead of handing your client a Spacefast API token. That delegation token lives five minutes and carries the source credential's policy verbatim, so a leaked MCP session can't be replayed against the wider API. Hosted requests are rate limited to 600 per minute per credential and 300 per minute per IP. diff --git a/content/index.mdx b/content/index.mdx index 3e420188..775123f9 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -41,9 +41,9 @@ Publish this folder to Spacefast and give me the live URL. A **Space** is one site with a stable hostname. Every publish creates a new **version**, an immutable snapshot with its own URL that never changes. The `live` channel points at one version. Promoting or rolling back moves that pointer. Nothing gets rebuilt. (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 opt into the Zero runtime with one line of config. +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. -When more than one person works on a site, its team owns the Space, domains, and API keys. Members can publish and roll back without passing around one person's login, while 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. +Every claimed Space belongs to a team, 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 6ef53fbc..1fc29a15 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -95,7 +95,7 @@ Don't want to install anything? Open the dashboard and drag a folder, a `.zip`, ## Next -Working with other people? Claim your Space to a team, then [invite members](/teams#invite-someone). Team members can publish and roll back; owners and admins can manage domains and invitations. You can set whether new Spaces start open to the team, private, or public in [team access settings](/teams#default-access-for-new-spaces). +Working with other people? If your Space is still anonymous, [claim it first](/anonymous-and-claim#claim-it). Signed-in publishes already belong to a team. Free and Go allow only the owner; adding another member needs [Plus](/billing#seats). Then [invite members](/teams#invite-someone) to publish and roll back together. Owners and admins can manage domains and invitations. Choose whether new Spaces start open to the team, private, or public in [team access settings](/teams#default-access-for-new-spaces). diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index 6fe5e284..0204cc75 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -72,6 +72,9 @@ test("Database gives the Zero prerequisite before query instructions", async () 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); }); @@ -111,8 +114,12 @@ const firstFactCases = [ }, { route: "/stats", - question: "Does traffic count crawlers?", - evidence: [/crawler traffic/iu, /left out|excluded/iu], + question: "Which traffic counts exclude crawlers?", + evidence: [ + /views exclude crawlers and failed requests/iu, + /requests count every HTTP request/iu, + /unique visitors .* without those filters/iu, + ], }, ]; From 5cd49ebedc2c65f014c90694dda2e29c06d01881 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Tue, 6 Oct 2026 11:24:21 -0400 Subject: [PATCH 19/20] Preserve conditions and exceptions across the docs rewrite --- AGENTS.md | 4 +++ DOCS_TEST_SUITE.md | 6 ++-- DOCS_WRITING_QA.md | 47 ++++++++++++++++++++++++++++-- STYLE_GUIDE.md | 47 +++++++++++++++++++++++++----- content/(account)/api-keys.mdx | 2 +- content/(account)/billing.mdx | 2 +- content/(concepts)/spaces.mdx | 7 +++-- content/(concepts)/teams.mdx | 4 +-- content/(concepts)/versions.mdx | 6 ++-- content/(dynamic)/crons.mdx | 2 +- content/(dynamic)/database.mdx | 2 +- content/(dynamic)/functions.mdx | 2 +- content/(dynamic)/storage.mdx | 2 +- content/(dynamic)/zero-runtime.mdx | 2 +- content/(publish)/ci.mdx | 4 +-- content/(publish)/frameworks.mdx | 2 +- content/(publish)/git.mdx | 4 +-- content/(reference)/glossary.mdx | 12 ++++---- content/(serve)/access.mdx | 4 +-- content/(serve)/urls.mdx | 4 +-- content/agents/claude-code.mdx | 2 +- content/agents/cursor.mdx | 2 +- content/agents/other-clients.mdx | 4 +-- content/agents/permissions.mdx | 2 +- content/agents/sf-setup.mdx | 4 +-- content/agents/skills.mdx | 2 +- content/api/idempotency.mdx | 2 +- content/api/index.mdx | 10 +++---- content/api/pagination.mdx | 20 +++++++------ content/api/rate-limits.mdx | 2 +- content/cli/agent-commands.mdx | 4 +-- content/cli/agents.mdx | 4 +-- content/cli/db.mdx | 2 +- content/cli/env.mdx | 2 +- content/cli/git.mdx | 2 +- content/cli/project.mdx | 4 +-- content/cli/publish.mdx | 4 +-- content/cli/storage.mdx | 2 +- content/cli/teams.mdx | 2 +- content/cli/versions.mdx | 4 +-- content/cli/zero.mdx | 4 +-- content/index.mdx | 8 ++--- content/troubleshooting.mdx | 4 +-- scripts/docs-experience.test.mjs | 10 +++---- 44 files changed, 178 insertions(+), 93 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 762823d5..5b098208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,10 @@ Assume every commit and every line of history will be public. - Be concise; lead with importance. See [STYLE_GUIDE.md](STYLE_GUIDE.md) for the full rationale, worked examples, and the complete list of banned words, wordiness swaps, and vocabulary rules Vale enforces. +- Preserve prerequisites, scope, and exceptions when rewriting. Check leads + against the procedure and public reference, then reconcile sibling guides, + glossary entries, and built-output tests. Shorter wording is not evidence + of factual accuracy. - Do not add navigation to a section until that section has a real page or generated source. - Authored navigation comes from `content/**` and its `meta.ts` files. diff --git a/DOCS_TEST_SUITE.md b/DOCS_TEST_SUITE.md index 81b9455e..10777933 100644 --- a/DOCS_TEST_SUITE.md +++ b/DOCS_TEST_SUITE.md @@ -16,11 +16,13 @@ CI runs `test:docs` after the production build. It uses Node's test runner and e - Authored pages open with a subject or action instead of the repeated “After this page…” promise. - The homepage and Quickstart offer dashboard Drop before CLI installation, and the linked Publishing page describes the Drop flow. - The homepage links to a glossary that defines its recurring product terms. -- Database explains how to enable Zero before it teaches queries. +- Database links the full Zero runtime declaration and names its required server file before teaching queries. - Troubleshooting gives readers a first diagnostic step before listing error codes. - The opening paragraphs on Versions, Crons, Environment variables, Access, Caching, and Traffic stats answer specific reader questions. Each test names the question and the evidence expected near the top of the built page. -These are regression contracts for the changed entry paths and first-screen explanations. They make a future edit fail if it hides those answers again. They do not grade tone, prove that shorter text is clearer, or measure whether a person can complete a real task. Those claims need editorial review and reader testing. +These are regression contracts for the changed entry paths and first-screen explanations. They make a future edit fail if it hides those answers again. They do not prove product behavior, grade tone, prove that shorter text is clearer, or measure whether a person can complete a real task. Check each expected answer against the public contract and the page's exceptions before encoding it in a test. + +The October 6 review corrected assertions that reinforced overbroad access and traffic claims. The checks now distinguish views from requests and unique visitors, retained ready versions from other versions, and removal of one public grant from removal of all access. Cache refresh is a request, not an unconditional freshness guarantee. ## Other gates diff --git a/DOCS_WRITING_QA.md b/DOCS_WRITING_QA.md index d18621b0..6878a13f 100644 --- a/DOCS_WRITING_QA.md +++ b/DOCS_WRITING_QA.md @@ -1,6 +1,6 @@ # Authored-docs writing audit -The reviewer click-path and command comparisons in [DOCS_QA_COMPARISON.md](DOCS_QA_COMPARISON.md) measure answer findability and factual accuracy. Neither measures whether the authored prose is better for readers. This point-in-time audit checks the writing changes against `main` as of October 5, 2026. Repeatable checks for the current docs are in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). +The October 5 audit below measured structure and wording, but missed factual overstatements. Its candidate examples are historical, not verified guidance. The [October 6 semantic review](#semantic-review-october-6-2026) records the corrections and their evidence. Repeatable checks for the current docs are in [DOCS_TEST_SUITE.md](DOCS_TEST_SUITE.md). ## Method @@ -29,7 +29,7 @@ The edited leads now put several useful answers before the reader has to scan th | Caching | How do I force a fresh response? | Promises to explain cache behavior | Says republishing forces one | | Traffic stats | Are crawlers included? | Promises to explain counts | Says crawler traffic is excluded | -These rows are illustrative checks of what the opening now tells a reader, not an independent six-question success rate. The underlying facts and the full procedures remain on the pages. +These rows record what the candidate openings claimed at the time, not an independent six-question success rate. Review later found that several claims dropped necessary qualifications, including the caching and traffic examples. For example, Versions opened with “After this page you know what a version holds, how it reaches `ready`, how the `live` pointer moves, and how to roll back to any earlier version in seconds.” It now opens with “A rollback doesn't rebuild anything — it just repoints `live` at a version that already exists, which is why it takes seconds, not minutes.” The new sentence answers the likely rollback question; the page still explains version states below it. @@ -38,3 +38,46 @@ For example, Versions opened with “After this page you know what a version hol The manual review caught five candidate leads that were shorter but spent the first sentence on a less useful detail: slug validation on Spaces, polling mechanics on Logs, archive flags on Frameworks and builds, CLI naming on Publish from Git, and remote WP-CLI output on WordPress. Each now leads with the page's main task or mental model. The removed detail was checked elsewhere on the same page and retained or moved into the body. This audit establishes changes in structure and in which facts appear first. It does not show that real readers complete tasks faster or understand the docs better. Shorter openings could also lose useful context for some readers. That requires reader testing. Vale now passes after two existing technical plurals (`GETs` and `TTYs`) were written as plain explanations; its rules do not detect repeated sentence structures or judge whether a page leads with the right fact. + +## Semantic review, October 6, 2026 + +Reviewed every changed MDX hunk in the PR: 78 existing pages plus the new glossary. Compared each rewrite with its original wording, then checked broader claims against the relevant procedures, exceptions, sibling guides, and producer-owned public reference snapshot. This was a documentation consistency audit, not a live product test or a reread of every unchanged paragraph. + +The first review fixed eight findings covering stats, caching, routing, anonymous key recovery, Zero pricing, hosted MCP authentication, team plan limits, and Zero setup. The follow-up applied the same reasoning across the full rewrite and corrected related claims at other entry points. + +| Claim family | Correction | Evidence checked | +| --- | --- | --- | +| Retry safety | Name the key, matching request scope, 24-hour replay window, and unstored outcomes that execute again | `content/api/idempotency.mdx`, Send a key / What is not stored | +| Pagination | Limit the common cursor model to endpoints that use it; retain endpoint defaults, ordering, offset paging, and unpaginated lists | `generated/openapi/api.json`: `searchDocs`, `listSpaceStorageObjects`, `listSpaceDomains` | +| Publish and rollback | Preserve no-op publishes, ready/retained targets, manual promotion, and preview behavior | Public reference: `createSpaceVersion`, `promoteSpaceVersion`; `content/(concepts)/versions.mdx`; `content/(publish)/ci.mdx` | +| URL lifetime | Separate a stable version URL from retained files; distinguish domain attachment from slug rename | `content/(concepts)/spaces.mdx`, Renaming; `content/cli/versions.mdx`, sf versions rm | +| Access | Revoking one matching grant does not revoke other grants or the team's permissions | `content/(serve)/access.mdx`, scoped grants; `content/(concepts)/teams.mdx`, role and default-access tables | +| Runtime setup | Scope auto-detection to Functions layouts; preserve the Zero declaration and the Functions database alternative | `content/(dynamic)/functions.mdx`, Where the code lives / Declare it; `content/cli/db.mdx` | +| Variables and logs | A queued re-finalize can apply variables; logs have retention, ingestion delay, and static-runtime limits | `content/(dynamic)/environment-variables.mdx`; `content/(dynamic)/logs.mdx`; `listSpaceRuntimeLogs` | +| Archives and Git | Preserve prebuilt archives, branch auto-deploy controls, and the GitHub App prerequisite | `generated/cli/index.md`, sf publish `--prebuilt`; `content/(publish)/git.mdx`; `content/cli/git.mdx` | +| CLI helpers | Linking selects a Space rather than removing all publish options; apply and continuation helpers mutate state; continuation needs claim approval | `content/cli/project.mdx`; `content/cli/agent-commands.mdx`; `content/(publish)/anonymous-and-claim.mdx` | +| WP-CLI and storage | Remote WP-CLI returns no printed output, even for read commands; object IDs do not replace read keys | `content/(dynamic)/wordpress.mdx`, Local versus remote; `content/(dynamic)/storage.mdx`, returned URL | +| Agent reach | Keep supported-client detection, skill installation, permission ceilings, human-only actions, and team-automation revocation exceptions | `content/cli/agents.mdx`; `content/agents/skills.mdx`; `content/agents/permissions.mdx` | +| Ownership and billing | Distinguish self-serve teams from partner customer ownership; billing reads differ from plan changes; key rotation depends on switching consumers first | `createSpace` public reference; `content/platforms/partner-api/customers.mdx`; `content/(account)/billing.mdx`; `content/(account)/api-keys.mdx` | +| Cache and schedule application | Repeat public-cache exceptions in troubleshooting; crons follow the live version | `content/(serve)/caching.mdx`; `listSpaceCrons` public reference | +| Sentence splitting | Limit missing generated CSS to the dynamic class instead of declaring the whole app unstyled | `content/(dynamic)/zero-runtime.mdx`, Styling | + +Coverage by original PR section: + +| Section | Pages compared | +| --- | --- | +| Account | 3: api-keys, authentication, billing | +| Concepts | 3: spaces, teams, versions | +| Dynamic | 8: crons, database, environment-variables, functions, logs, storage, wordpress, zero-runtime | +| Publishing | 8: anonymous-and-claim, ci, frameworks, git, publish, recipes/html, recipes/next, wordpress-data-sources | +| Reference | 3: config-file, glossary, limits | +| Serving | 8: access, caching, customization, domains, routing, site-pages, stats, urls | +| Agents | 9: claude-code, claude-desktop, codex, cursor, mcp-server, other-clients, permissions, sf-setup, skills | +| API | 9: authentication, errors, idempotency, index, operations, pagination, rate-limits, sdk, webhooks | +| CLI | 20: agent-commands, agents, api-keys, api, builds, db, domains, env, git, index, login, project, publish, share, source, spaces, storage, teams, versions, zero | +| Entry pages | 3: index, quickstart, troubleshooting | +| Platforms | 5: partner-api/configuration, customers, go-live, index, tokens | + +The style guide and contributor instructions now require this meaning check. Existing built-output assertions were updated where they reinforced a misleading claim. No new regex suite is presented as independent proof of product behavior. Generated references remain producer-owned and unchanged. The previously recorded support-contact and API-key preset gaps remain outside these corrections. + +Verification with Bun 1.3.11 and Node 24 passed: frozen dependency install, generated-reference and command-example checks, type check, strict link validation, production build, all 26 docs tests, composed-site audit, public-safety check, Vale, route verification, and `git diff --check`. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 4b19d70d..dd2fa3b5 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -49,8 +49,38 @@ In practice: definition of "wordy." A 22-word sentence that chains four clauses is still a problem this rule won't catch; use judgment, not just the word count. -- **Cut filler and hedging.** If a sentence works with a phrase removed, - remove it. +- **Cut filler, preserve conditions.** Remove a phrase only if the shorter + sentence remains true for the same readers and situations. A plan limit, + required flag, credential type, runtime, or exception is not filler. + +## Preserve meaning when shortening + +Read a rewritten lead beside the full procedure and its exceptions. Check +the original wording and the public reference before treating the shorter +sentence as equivalent. A fact moved out of a subsection must keep that +subsection's scope: a hosted MCP guarantee does not describe local stdio. + +- Keep prerequisites next to the action: a ready version for rollback, + an idempotency key for replay, or the plan required to add members. +- Keep distinct states distinct: available versus free, ready versus live, + a permanent URL versus retained files, and configuration saved versus applied. +- Check words such as "every," "always," "never," "only," and "any" against + counterexamples. Name the supported case instead of broadening a claim. +- After splitting a sentence, make sure its condition still governs every + sentence that depends on it. Repeat the condition when necessary. +- Check sibling guides, CLI pages, descriptions, glossary entries, and tests + for the same claim. A correction is incomplete if another entry point + teaches the old rule. + +For example, "Every list uses cursors" drops offset-based and unpaginated +endpoints. "Cursor-paginated endpoints return `pagination.nextCursor`; check +the endpoint's limits and ordering" preserves the useful rule and its scope. + +Tests of built docs can check that a prerequisite or exception stays visible. +They do not prove product behavior. A regex matching confident wording is +not evidence that the wording is true, and shorter copy is not a pass criterion. + +## Concision examples Worked example, from the page-opener cleanup: @@ -70,9 +100,12 @@ Another: > recognize every other credential Spacefast hands you by its prefix." (33 > words, 4 clauses) > -> After: "You can mint a scoped API key with exactly the permissions it -> needs, then rotate it without downtime." (18 words — the credential-prefix -> table further down the page already covers the dropped clause.) +> After: "Team owners and admins can mint scoped API keys. To rotate without +> interrupting an integration, create a replacement, switch the integration +> to it, verify a request, and only then revoke the old key." + +This version keeps the role prerequisite and the order that makes rotation +safe. The credential-prefix table can stay below; the rotation condition cannot. (These two "after" versions also show the fix from the next section — no "after this page" framing. The intermediate step, where the sentence was @@ -96,8 +129,8 @@ Open with the fact itself, not a sentence announcing that a fact is coming: > Templated: "After this page you can mint a scoped API key and rotate it > without downtime." > -> Direct: "You can mint a scoped API key with exactly the permissions it -> needs, then rotate it without downtime." +> Direct: "To rotate a key without interrupting an integration, switch to +> its replacement and verify a request before revoking the old key." > Templated: "After this page you know whether to publish your own build > output or let Spacefast build it." diff --git a/content/(account)/api-keys.mdx b/content/(account)/api-keys.mdx index e7fa65ef..1a2fa46b 100644 --- a/content/(account)/api-keys.mdx +++ b/content/(account)/api-keys.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -You can mint a scoped API key with exactly the permissions it needs, then rotate it without taking anything down. +Team owners and admins can mint scoped API keys. To rotate without interrupting an integration, create a replacement, switch the integration to it, verify a request, and only then revoke the old key. ## Create an API key diff --git a/content/(account)/billing.mdx b/content/(account)/billing.mdx index ff670f5d..acf8d429 100644 --- a/content/(account)/billing.mdx +++ b/content/(account)/billing.mdx @@ -86,6 +86,6 @@ Reading is never blocked in either state. Settle the payment from the billing po ## Billing is dashboard-only -Plans, checkout, and cancellation live in the dashboard. There is no `sf` command and no API endpoint for changing a plan. API keys and agent credentials cannot reach billing at all. +Plans, checkout, and cancellation live in the dashboard. There is no `sf` command and no API endpoint for changing a plan. API keys and agent credentials cannot make those changes. Reading is a different story. An agent can pull the resolved plan and limits from `GET /v1/teams/{teamId}/entitlements`, current counters from `GET /v1/teams/{teamId}/usage`, and the limits as they are enforced at publish time from `GET /v1/teams/{teamId}/plan-policy`. See [Usage](/usage) for what those numbers mean. diff --git a/content/(concepts)/spaces.mdx b/content/(concepts)/spaces.mdx index 93fdd90f..35ab02d2 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -9,7 +9,7 @@ A Space is one site with a stable hostname. Publishing creates versions inside i ## What a Space is -A Space is one site. It owns a slug, a hostname, an owner, and one pointer that says which version visitors get. Everything you publish lands in a Space as a new immutable version, and the Space decides which of them is live. +A Space is one site. It owns a slug, a hostname, an owner, and one pointer that says which version visitors get. A publish can create a new immutable version or report no changes, and the Space decides which version is live. Ids are `spc_` plus 32 hex characters: @@ -54,14 +54,15 @@ Check before you commit with [`getSlugAvailability`](/api/reference/spaces/getsl ## The default hostname -Every Space answers at `https://.view.fast/` from the moment it exists, and keeps that hostname for life. Adding a custom domain never retires it. Version and branch hostnames, the live URL rule, and what a rename does to each are on [URLs and hostnames](/urls). +A Space gets a default `view.fast` hostname. Adding a custom domain leaves it in place; renaming the slug moves the default hostname and redirects the old one. Version and branch hostnames, the live URL rule, and what a rename does to each are on [URLs and hostnames](/urls). ## Who owns a Space -Exactly one owner, one of two kinds: +The owner depends on how the Space was created: - **A team.** The normal case. Team members reach the Space by their role. Slugs are unique per team rather than globally, so two teams can both own `docs`. - **A space key.** A Space published without an account is owned by its `sfc_` key until someone claims it. See [Publish without an account](/anonymous-and-claim). +- **An external principal.** A customer in a [Partner API integration](/platforms/partner-api/customers) can own Spaces without a self-serve team. To move a Space to another team, run `sf spaces transfer --space docs`, or open **Space settings → General → Transfer this space → Transfer space…**. If you are an owner or admin of both teams, the move is instant. The Space keeps serving at its current address. Otherwise the target team has 7 days to accept, and nothing changes until they do. Roles and membership are on [Teams](/teams). diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index 30cd858b..e2b2b89c 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -5,7 +5,7 @@ sidebar: order: 3 --- -A team owns everything — Spaces, domains, API keys, billing, members — and the role you hold there caps what you can do with any of it. +A team owns its Spaces, domains, API keys, and billing. Your role in that team caps what you can do with them. ## Why work in a team @@ -17,7 +17,7 @@ Free and Go allow only the owner. Adding another member needs [Plus](/billing#se ## What a team is -A team is the thing that owns work. Spaces, custom domains, API keys, billing, and members all hang off it. Every claimed Space belongs to exactly one team, even when you are its only member. There is no separate personal container to reason about. +A team is the thing that owns work in a self-serve account. Spaces, custom domains, API keys, billing, and members all hang off it. A Space you claim belongs to the team you select, even when you are its only member. [Partner API customers](/platforms/partner-api/customers) use external principals instead. A team has a name and a slug. The slug is the first segment of its dashboard URLs. You can change it in **Team settings → General**, where a live check tells you whether the new one is free. diff --git a/content/(concepts)/versions.mdx b/content/(concepts)/versions.mdx index 252b5208..e6e081ab 100644 --- a/content/(concepts)/versions.mdx +++ b/content/(concepts)/versions.mdx @@ -1,15 +1,15 @@ --- title: Versions and the live channel -description: Every publish is an immutable version, the live channel is a pointer at one of them, and rolling back moves the pointer without rebuilding anything +description: Immutable versions, the live channel, promotion policies, and rollback to a retained ready version without rebuilding sidebar: order: 2 --- -A rollback doesn't rebuild anything — it just repoints `live` at a version that already exists, which is why it takes seconds, not minutes. +A rollback doesn't rebuild anything. It repoints `live` at a retained, ready version; deleted or expired versions cannot be rollback targets. ## A version is a snapshot -Each publish creates a version. A version is the exact set of files plus the config it finalized with, frozen. Nothing rewrites a version after it is ready. Publishing again does not modify the old one, it makes a new one. +A publish that changes the files creates a version. A version is the exact set of files plus the config it finalized with, frozen. Nothing rewrites a version after it is ready. A publish with no changes can return `noop_publish` without creating a version. Ids are `ver_` plus 32 hex characters. Once a version reaches `ready` it also gets a number, and the CLI and dashboard call it by that number. diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index bde3533f..ee25e156 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -5,7 +5,7 @@ sidebar: order: 4 --- -There's no dashboard editor for crons — you declare schedules entirely in `sf.jsonc`, and they only take effect when you publish. +There's no dashboard editor for crons. Declare schedules in `sf.jsonc`, then publish and make that version live to apply them. A preview does not replace the live version's schedules. ## How a run happens diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index 041e7a05..c7b2a608 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -8,7 +8,7 @@ sidebar: 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. :::note[Don't have a database yet?] -This page covers the database you get once [Zero](/zero-runtime) is running. 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. +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. ::: ## What it is diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index 494ce1b2..d9df2709 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -Detection decides your runtime: Spacefast scans your project tree and picks the first layout it recognizes, so most projects need no `runtime` block at all. +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 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. diff --git a/content/(dynamic)/storage.mdx b/content/(dynamic)/storage.mdx index ef6c019e..5c11dce9 100644 --- a/content/(dynamic)/storage.mdx +++ b/content/(dynamic)/storage.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -Storage holds the files your running app uploads at runtime — separate from what you publish — and each object is addressed by nothing but a 32-character id. +Storage holds files uploaded by a running app, separate from published version files. Each object has a 32-character hexadecimal id; its returned read URL also carries a read key. ## What it is diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 26a97ff0..6baf3041 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -259,7 +259,7 @@ Call them from the client with `useAction("invite")`. `scope: "/"` covers the wh 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. -Class names must be static strings. The compiler finds them by scanning source text, so `bg-${tone}-500` produces no CSS at all. The capsule ships unstyled. Branch to whole literals instead. +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. 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. diff --git a/content/(publish)/ci.mdx b/content/(publish)/ci.mdx index 3c2c523d..616b8218 100644 --- a/content/(publish)/ci.mdx +++ b/content/(publish)/ci.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -A CI pipeline can publish automatically on every push to main, and ship pull requests as preview versions that never touch live traffic. +A CI pipeline can publish pushes to your production branch. Use `--target preview` for pull requests to leave the `live` channel unchanged; an ordinary publish can move it. ## Set it up @@ -87,7 +87,7 @@ publish: -The CLI needs Node.js 20.3 or newer. `sf publish` waits until the version is ready and live before it exits, so the job fails when the publish fails. +The CLI needs Node.js 20.3 or newer. `sf publish` waits by default, but a successful command can leave a version ready without making it live. Check the receipt's activation outcome, especially for previews or a manual promotion policy. ## What `CI` changes diff --git a/content/(publish)/frameworks.mdx b/content/(publish)/frameworks.mdx index fb35ed65..1921a764 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -24,7 +24,7 @@ sf publish --remote With no flags, `sf publish` builds a detected project, and publishes the directory as-is if there is nothing to build. -`--prebuilt` forces the upload path and `--build` forces the build path. A `.tar.gz` archive always goes through a build, since archives are classified remotely. +`--prebuilt` publishes a built directory or archive without installing dependencies or running a build. `--build` forces build mode for directories. Without `--prebuilt`, archives go through remote detection. ## Which directory to publish diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 2483c789..226821f4 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -1,11 +1,11 @@ --- title: Publish from Git -description: Push to a Spacefast remote, or connect a GitHub repository and let every push build and publish itself +description: Push to a Spacefast remote or connect GitHub, choose which branches build, and control which versions go live sidebar: order: 4 --- -Keep your source in Git and publish from either a Spacefast remote or a connected GitHub repository. Pushing to either starts a build. +Keep your source in Git and publish from a Spacefast remote or a connected GitHub repository. Connected repository pushes queue builds only when auto-deploy is enabled for that branch; deleting a branch does not build it. Two lanes. The Spacefast remote needs no external host and no app install. A GitHub connection keeps your repository where it is and reacts to its webhooks. diff --git a/content/(reference)/glossary.mdx b/content/(reference)/glossary.mdx index 1236b2de..3443db32 100644 --- a/content/(reference)/glossary.mdx +++ b/content/(reference)/glossary.mdx @@ -13,7 +13,7 @@ A named operation with a schema that a Space's WordPress publishes for agents to ## Build -What happens when you hand Spacefast source code instead of a built output directory. It installs your dependencies, runs the build in a sandbox, and publishes the result as a version. See [Frameworks and builds](/frameworks). +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 @@ -37,20 +37,20 @@ One rule in a Space's access list: it names an audience (public, team, one perso ## Live (the live channel) -The pointer that decides which version a Space actually serves right now. Publishing, promoting, and rolling back all just move this pointer — nothing gets rebuilt. See [Versions and the live channel](/versions). +The pointer that selects the version served at the live URL. Promoting or rolling back moves it to a retained, ready version without rebuilding. Publishing can build a new version; preview publishes and manual promotion policies leave `live` unchanged. See [Versions and the live channel](/versions). ## Space -One published site with a stable hostname. Everything else in these docs — versions, domains, access, functions — belongs to a Space. See [Spaces](/spaces). +A container for a site's versions and access settings. It can exist before anything is published. Custom domains belong to a team and are assigned to a Space. See [Spaces](/spaces). ## Team -The thing that owns your Spaces, domains, API keys, and billing. Every claimed Space belongs to exactly one team, even if you're its only member. See [Teams and members](/teams). +The owner of Spaces, domains, API keys, and billing in a self-serve account, even when you are its only member. Partner API integrations can use external principals for customer ownership instead. See [Teams and members](/teams). ## Version -An immutable snapshot created when a publish produces a new version. A ready version gets its own permanent URL that never changes. The [live](#live-the-live-channel) pointer names the version visitors see; it is empty before the first publish. See [Versions and the live channel](/versions). +An immutable snapshot created when a publish produces a new version. A ready version gets its own URL, which serves the snapshot while the version is retained. The [live](#live-the-live-channel) pointer names the version visitors see; it is empty before the first publish. See [Versions and the live channel](/versions). ## Zero -The runtime for a Space that needs a database, live queries, or scheduled jobs. One TypeScript app compiles into a [capsule](#capsule) with a MySQL database next to your code. The other dynamic option, alongside [Functions](#functions). See [Dynamic sites with Zero](/zero-runtime). +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). diff --git a/content/(serve)/access.mdx b/content/(serve)/access.mdx index 71ab91ce..cce97d12 100644 --- a/content/(serve)/access.mdx +++ b/content/(serve)/access.mdx @@ -4,11 +4,11 @@ description: Make a Space public, hand out scoped share links or a password, inv sidebar: { order: 5 } --- -A Space has no public-or-private flag — access is a list of additive grants, and removing every one of them is what makes it private. +Visitor access comes from additive grants. Removing a public grant closes that route to the public only if no other public grant matches; links, people, and team access can remain. ## Access is a list, not a switch -Access is a set of additive **grants**, each one naming an audience, a set of capabilities, the paths it covers, and which published target it applies to. A visitor is admitted when at least one grant matches, and nothing admits anyone once every grant is gone. +Access is a set of additive **grants**, each one naming an audience, a set of capabilities, the paths it covers, and which published target it applies to. A matching grant admits a visitor. Revoking one grant does not revoke other matching grants or remove the owning team's permissions. That model is why a Space can be public at `/docs/**` and closed everywhere else, and why revoking a share link takes effect on the next request rather than at the next publish. diff --git a/content/(serve)/urls.mdx b/content/(serve)/urls.mdx index 2efd2aca..2addd793 100644 --- a/content/(serve)/urls.mdx +++ b/content/(serve)/urls.mdx @@ -4,7 +4,7 @@ description: Every hostname a Space answers on, which one counts as the live URL sidebar: { order: 1 } --- -Adding a custom domain never retires a Space's default `view.fast` hostname — it keeps serving for the life of the Space. +Adding a custom domain leaves the Space's default `view.fast` hostname in place. A later slug rename moves that default address and redirects the old one. ## The default hostname @@ -26,7 +26,7 @@ Every version that reaches ready gets a permanent hostname of its own: https://v{number}--{label}.view.fast/ ``` -Version 7 of `my-site` is `https://v7--my-site.view.fast/`. The URL is computed once when the version turns ready, and stored on the version row. So it survives a slug rename, and keeps serving that exact build no matter what is live now. +Version 7 of `my-site` is `https://v7--my-site.view.fast/`. The URL is computed once when the version turns ready, and stored on the version row. It survives a slug rename and serves that build while the version is retained. [Deleting or expiring a version](/versions#how-many-versions-are-kept) removes its files. The API and the CLI report it as `immutableUrl`, with an `immutableUrlStatus` of `pending` until the hostname is routable. diff --git a/content/agents/claude-code.mdx b/content/agents/claude-code.mdx index 4e0f80ec..eed3bd12 100644 --- a/content/agents/claude-code.mdx +++ b/content/agents/claude-code.mdx @@ -3,7 +3,7 @@ title: Claude Code description: Install the Spacefast plugin in Claude Code, what it adds to your session, and the prompts that work --- -The Spacefast plugin lets Claude Code publish your project and manage your account without leaving the terminal. +The Spacefast plugin lets Claude Code publish your project and manage resources within its approved permissions. Team membership and billing changes still need you in the dashboard. ## Install diff --git a/content/agents/cursor.mdx b/content/agents/cursor.mdx index c56aeaaa..0089366b 100644 --- a/content/agents/cursor.mdx +++ b/content/agents/cursor.mdx @@ -3,7 +3,7 @@ title: Cursor description: Install the Spacefast plugin in Cursor, or add the MCP server to mcp.json by hand --- -Cursor can publish the project you have open and reach the rest of the Spacefast API through one sandboxed tool. +The Spacefast plugin lets Cursor publish your project and call API operations through `execute`, within the permissions you approved. Some actions remain reserved for a signed-in person. ## Install diff --git a/content/agents/other-clients.mdx b/content/agents/other-clients.mdx index 764d5ae8..7d26422e 100644 --- a/content/agents/other-clients.mdx +++ b/content/agents/other-clients.mdx @@ -3,7 +3,7 @@ title: Any MCP client description: Connect any MCP client to Spacefast over hosted HTTP or local stdio, and what sf mcp proxy does --- -Every MCP client names its config key differently — `mcp` for OpenCode, `servers` for VS Code, `context_servers` for Zed — so `sf setup agent` writes the right one for you. +MCP configuration varies by client: `mcp` for OpenCode, `servers` for VS Code, and `context_servers` for Zed. `sf setup agent` writes configuration for the clients it supports. ## Pick a transport @@ -59,7 +59,7 @@ Every MCP client names its config key differently — `mcp` for OpenCode, `serve -Amp goes further still, putting the server map at the top level with no root key at all. `sf setup agent` knows every dialect, so let it write the file when you can. +Amp puts the server map at the top level with no root key. Use `sf setup agent` for a [supported client](/cli/agents#sf-mcp-install); otherwise adapt the transport example to your client's configuration format. Ask the agent to find a documentation page through `execute`. It should search for the documentation operation, describe it, and call it before attempting a publish. diff --git a/content/agents/permissions.mdx b/content/agents/permissions.mdx index 9f8fb77f..f55cac2a 100644 --- a/content/agents/permissions.mdx +++ b/content/agents/permissions.mdx @@ -3,7 +3,7 @@ title: What agents are allowed to do description: How an agent gets access to your Spacefast account, what it can never do without you, and how approvals, handoffs, and revocation work --- -You decide exactly how much access an agent holds, and can cut it off in one click whenever you're done. +You approve an agent's scopes and teams, with your team role as the ceiling. Revoking its connection removes that access; the account-wide stop does not revoke team-owned automation. ## How an agent gets access diff --git a/content/agents/sf-setup.mdx b/content/agents/sf-setup.mdx index 744d1d4e..a8b88782 100644 --- a/content/agents/sf-setup.mdx +++ b/content/agents/sf-setup.mdx @@ -1,9 +1,9 @@ --- title: sf setup agent -description: Configure MCP and install the Spacefast skill for every agent on your machine with one CLI command +description: Configure MCP and install the Spacefast skill for supported agent clients with one CLI command --- -One command points every agent client on your machine at Spacefast, and the same command runs non-interactively in a script. +`sf setup agent` configures supported clients it detects or you select. Add `-y` to accept the defaults without prompts. If detection finds no client, it installs the universal skill without writing MCP configuration. ## The command diff --git a/content/agents/skills.mdx b/content/agents/skills.mdx index cfb16ea6..0c8bd3ac 100644 --- a/content/agents/skills.mdx +++ b/content/agents/skills.mdx @@ -3,7 +3,7 @@ title: Skills description: The Spacefast agent skill, what it tells your agent to do, and how to install or remove it with the sf CLI --- -The `spacefast` skill ships with every agent lane and routes publish, inspect, and manage requests to the right tool. +The Spacefast plugins and `sf skills` install guidance for publishing, inspection, and management. The format and tool choices depend on the client; adding only an MCP connection does not install the skill. ## One skill, called `spacefast` diff --git a/content/api/idempotency.mdx b/content/api/idempotency.mdx index 7bb64dfe..329a12ae 100644 --- a/content/api/idempotency.mdx +++ b/content/api/idempotency.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -A dropped connection on a publish should not create two versions, so retrying any `POST` gets one of two answers: the original result replayed, or a conflict saying the first attempt is still running. +Send a valid `Idempotency-Key` when retrying a `POST`, and reuse the same body, credential, and path. Stored results replay for 24 hours. In-flight requests return a conflict, and outcomes that are not stored can execute again. ## Send a key diff --git a/content/api/index.mdx b/content/api/index.mdx index 52f2792a..ab67ce98 100644 --- a/content/api/index.mdx +++ b/content/api/index.mdx @@ -1,11 +1,11 @@ --- title: API overview -description: Base URL, response envelopes, content types, and a one-minute curl walkthrough from credential to live URL +description: Base URL, response envelopes, content types, and a curl walkthrough from credential to published version sidebar: order: 1 --- -Everything the dashboard and the `sf` CLI do runs through this same API — authenticate a request, read any success or failure body, and publish a folder to a live URL with four curl calls. +Use the Spacefast API to publish files, inspect Spaces, and manage resources within your credential's permissions. Publishing can return an operation that needs repeated polling before you know the result. ## Base URL and versioning @@ -50,7 +50,7 @@ Success is always an object with a `data` member. Three shapes cover the whole A | Shape | When | Example operation | | --- | --- | --- | | `{ data }` | one resource, or a mutation with no background work | `GET /v1/spaces/{spaceId}` | -| `{ data, pagination }` | any list | `GET /v1/spaces/{spaceId}/versions` | +| `{ data, pagination }` | a cursor-paginated list | `GET /v1/spaces/{spaceId}/versions` | | `{ data, operation }` | a mutation that can outlive the request | `POST /v1/spaces/{spaceId}/versions/{versionId}/promote` | `operation` is `null` when the work finished inside the request. See [Operations](/api/operations). @@ -65,7 +65,7 @@ Send `application/json` on every request body. `POST /v1/publish` is the excepti Every response carries `X-Request-Id`. It equals the `requestId` in a problem document, and it is the value to quote when you ask for help. -## One minute, four calls +## Publish with curl @@ -133,7 +133,7 @@ Stop when `data.status` is `succeeded`, `failed`, or `canceled`. Then read `spac Problem documents, the stable `code`, and every code the API can return. - One cursor scheme for every list. + Cursor paging, endpoint defaults, and other list shapes. Per-credential and per-IP budgets, and what a 429 carries. diff --git a/content/api/pagination.mdx b/content/api/pagination.mdx index 0fa3516a..a6fe1365 100644 --- a/content/api/pagination.mdx +++ b/content/api/pagination.mdx @@ -1,26 +1,26 @@ --- title: Pagination -description: One cursor scheme for every list in the Spacefast API, with defaults, limits, and the loop that walks a whole collection +description: Walk cursor-paginated collections, check endpoint-specific limits, and handle lists that use another paging scheme sidebar: order: 4 --- -Every list in this API pages the same way, so you can walk a collection to the end without writing a special case for any endpoint. +Cursor-paginated collections return `pagination.nextCursor` for the next request. Check each endpoint's defaults and ordering: documentation search uses offsets, and some lists return their full result without pagination. ## Request -Two optional query parameters. +Cursor-paginated endpoints take two query parameters. These are the common defaults; check the endpoint's reference for exceptions. | Parameter | Type | Default | What it does | | --- | --- | --- | --- | | `limit` | integer, 1 to 100 | 20 | How many items to return. | | `cursor` | string, up to 4096 characters | none | The `nextCursor` from the previous page. | -Both are optional. The generated spec marks `limit` as required because the schema supplies its own default. A request that omits it still gets 20. +Both can be omitted. The generated spec marks a defaulted `limit` as required, but the server applies the endpoint's default when it is absent. For example, [storage objects](/api/reference/spaces/listspacestorageobjects) default to 50 and accept cursors up to 2048 characters. ## Response -A list body adds a `pagination` object next to `data`. +A cursor-paginated list adds a `pagination` object next to `data`. ```json { @@ -37,11 +37,11 @@ A list body adds a `pagination` object next to `data`. | `nextCursor` | string or null | Pass it back as `cursor` for the next page. `null` when there is nothing after this page. | | `hasMore` | boolean | Whether another page exists. | -Lists are newest first, ordered by creation time and then by id, so a stable sort holds across pages even when two rows share a timestamp. +Ordering belongs to the endpoint. Versions are newest first, while storage objects are in ID order. Keep that order when combining pages. ## Cursors are opaque -A cursor is a base64url-encoded keyset. Do not parse it, build one, or store it as a durable bookmark. Pass back exactly what you got. +A cursor is an opaque continuation value. Do not parse it, build one, or store it as a durable bookmark. Pass back exactly what you got. ## The loop @@ -85,9 +85,11 @@ async function allVersions(spaceId: string) { The `sf` CLI does the same walk for you with `sf api GET --paginate`, which streams every page as JSON Lines and fails when a cursor repeats. -## The one exception +## Other list shapes -`GET /v1/docs/search` pages by offset rather than by cursor. It takes `limit` (up to 25) and `offset`, and answers with `nextOffset` and `hasMore`. Every other list on the public API uses the cursor scheme above. +`GET /v1/docs/search` pages by offset rather than by cursor. It takes `limit` (up to 25) and `offset`, and answers with `nextOffset` and `hasMore`. + +Some lists do not paginate. For example, [Space domains](/api/reference/domains/listspacedomains) return the full set in one response. Other endpoints use their own continuation fields. Follow the response shape documented for the endpoint instead of assuming every list carries `pagination`. ## Filters compose with paging diff --git a/content/api/rate-limits.mdx b/content/api/rate-limits.mdx index 91475c87..aeace674 100644 --- a/content/api/rate-limits.mdx +++ b/content/api/rate-limits.mdx @@ -5,7 +5,7 @@ sidebar: order: 5 --- -Two layers of limits sit in front of the API: a general per-minute request budget, and hourly budgets on the expensive actions. Read the headers on any response to back off correctly when one trips. +Two layers of limits sit in front of the API: a general per-minute request budget, and hourly budgets on expensive actions. Use the rate-limit headers when present; they can be absent when accounting is unavailable. ## The general budget diff --git a/content/cli/agent-commands.mdx b/content/cli/agent-commands.mdx index 85ac217b..3c9990b2 100644 --- a/content/cli/agent-commands.mdx +++ b/content/cli/agent-commands.mdx @@ -5,7 +5,7 @@ sidebar: order: 27 --- -The commands on this page answer questions and finish jobs rather than change what is published — look up any Space you can reach, or pull a private route straight from the terminal. +Use `sf inspect` and `sf fetch` to read a Space you can access. Other helpers change state: `sf apply` makes saved settings live, and `sf continue` updates your saved credential after an approved claim handoff. Every command here also takes the [global flags](/cli#global-flags). @@ -155,7 +155,7 @@ sf continue [--claim-token ] sf continue ``` -This is the last step of the anonymous flow. You published without an account, someone claimed the Space, and the space key in `.spacefast/state.json` stopped being the right credential. `sf continue` trades it for a durable key bound to the claimed Space, in place, so the same directory keeps publishing. +This is the last step of an anonymous flow where the person claiming the Space chose [Claim & keep access](/anonymous-and-claim#keeping-your-agent-publishing). `sf continue` trades the saved space key for a durable key bound to the claimed Space, so the same directory keeps publishing. Without that approval, reconnect with a new credential. Run it from the directory you published from. diff --git a/content/cli/agents.mdx b/content/cli/agents.mdx index 566359f7..9b356b05 100644 --- a/content/cli/agents.mdx +++ b/content/cli/agents.mdx @@ -1,11 +1,11 @@ --- title: mcp, setup, skills, agents -description: Configure any coding agent for Spacefast, run the MCP server or its authenticated proxy, and manage the bundled agent skill +description: Configure supported coding agents for Spacefast, run the MCP server or its authenticated proxy, and manage the bundled agent skill sidebar: order: 26 --- -One command points Claude Code, Cursor, Codex, or any other MCP client at Spacefast. +`sf setup agent` configures detected or selected supported clients, including Claude Code, Cursor, and Codex. Other MCP clients may need manual configuration. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`). `sf mcp` and `sf mcp proxy` do not support `--json`, because stdout carries the MCP protocol. diff --git a/content/cli/db.mdx b/content/cli/db.mdx index a816354f..b82a52e8 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -5,7 +5,7 @@ sidebar: order: 22 --- -A Space's database can be inspected, migrated, and dumped without ever touching a connection string — there isn't one. You can also fire a declared cron job on demand instead of waiting for its schedule. +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. 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/env.mdx b/content/cli/env.mdx index 58847695..20e40b78 100644 --- a/content/cli/env.mdx +++ b/content/cli/env.mdx @@ -5,7 +5,7 @@ sidebar: order: 21 --- -A Space's variables can be set without ever putting secrets in your shell history, and a write only takes effect when the next version finalizes. +Read secret values from stdin to keep them out of shell history. A variable write takes effect when a version next finalizes, through a new publish or the queued re-finalize of the live version. Every command here takes the global flags (`--api-url`, `--profile`, `--token`, `-y`/`--yes`, `--json`). All except `sf env export-template` also take the space selection flags (`--space`, `-o`/`--team`, `--claim-token`). See [the CLI overview](/cli). diff --git a/content/cli/git.mdx b/content/cli/git.mdx index b0f530a1..2144a9ce 100644 --- a/content/cli/git.mdx +++ b/content/cli/git.mdx @@ -5,7 +5,7 @@ sidebar: order: 24 --- -You can connect a GitHub repository to a Space in one command, then trigger a remote build from it right away. +Connect a GitHub repository to a Space with `sf git connect`, then request a build. The GitHub App installation must grant that repository to your team. 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 af87fa39..2b1df5b8 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -5,9 +5,9 @@ sidebar: order: 9 --- -Scaffolding a project and linking a directory to a Space means `sf publish` needs no flags afterward. +Scaffold a project with `sf init`, then link it to a Space with `sf link`. The saved link selects the Space on later publishes; build and publish options still depend on your project. -Two files carry the link. `.spacefast/space.json` is the committable one: which Space, which team, which API base URL. `.spacefast/state.json` is the local cache. It holds credentials, so it's never committed; the CLI adds it to `.gitignore` for you. +Two files carry the link. `.spacefast/space.json` is the committable one: which Space, which team, which API base URL. `.spacefast/state.json` is the local cache. It holds credentials, so never commit it. The CLI adds it to `.gitignore` for you. Every command here also takes the [global flags](/cli#global-flags). diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index cdc788fa..5a21fffa 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -One command, `sf publish`, ships any folder, project, or archive to a Space. `sf dev` is documented at the end because it runs a project locally, not a copy of what publish serves. +Use `sf publish` for a file, directory, supported project, or `.zip`/`.tar.gz` archive. Build detection and validation decide what can publish. `sf dev` is documented at the end because it runs a project locally, not a copy of what publish serves. ## sf publish @@ -15,7 +15,7 @@ Publish files or built projects to Spacefast. `sf deploy` is an exact alias. sf publish [DIR] ``` -A publish creates one immutable version. If the live channel promotes automatically (the default), that version goes live. See [Publishing](/publish) for the model and [Versions](/versions) for what happens after. +A publish can create an immutable version or report no changes. A production publish goes live when the channel promotes automatically (the default); preview publishes and manual policies leave it ready for a separate promotion. See [Publishing](/publish) for the model and [Versions](/versions) for what happens after. ### Arguments diff --git a/content/cli/storage.mdx b/content/cli/storage.mdx index 14b17b40..fd201d29 100644 --- a/content/cli/storage.mdx +++ b/content/cli/storage.mdx @@ -5,7 +5,7 @@ sidebar: order: 23 --- -You can see what your app has stored in runtime storage, and read every log line your Space produces. +List your app's stored objects and read access or runtime logs within your plan's retention window. Runtime lines take time to appear, and static Spaces have no runtime logs. 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/teams.mdx b/content/cli/teams.mdx index d4d78cd4..895d0fc8 100644 --- a/content/cli/teams.mdx +++ b/content/cli/teams.mdx @@ -5,7 +5,7 @@ sidebar: order: 6 --- -Creating a team, inviting members, and setting the access new Spaces start with are all one `sf teams` subcommand away. +Use `sf teams` to create or select a team and set defaults for new Spaces. Adding a member requires [Plus](/billing#seats); API keys and agent credentials cannot manage invitations or membership, so use the dashboard for those actions. A team owns Spaces, domains, and billing. Every member holds one team role: `owner`, `admin`, or `member`. See [Teams](/teams) for the model, and [`sf share`](/cli/share) for per-Space access, which is a separate system. diff --git a/content/cli/versions.mdx b/content/cli/versions.mdx index d664a58f..aa99d461 100644 --- a/content/cli/versions.mdx +++ b/content/cli/versions.mdx @@ -5,9 +5,9 @@ sidebar: order: 4 --- -Live traffic can be moved to any version a Space has published, and rolled back the same way after a bad one. +Move live traffic to a retained, ready version with `sf promote` or `sf rollback`. Deleted, expired, and unfinished versions cannot serve as rollback targets. -Every publish creates one immutable version. A channel is a named pointer at one version. Only `live` exists today. Promoting and rolling back move that pointer; they never rebuild anything. See [Versions](/versions) for the model. +A publish can create an immutable version or report no changes. A channel is a named pointer at one version. Only `live` exists today. Promoting and rolling back move that pointer; they never rebuild anything. See [Versions](/versions) for the model. ## sf versions ls diff --git a/content/cli/zero.mdx b/content/cli/zero.mdx index 70c7af6e..5281ee4e 100644 --- a/content/cli/zero.mdx +++ b/content/cli/zero.mdx @@ -5,7 +5,7 @@ sidebar: order: 25 --- -A Space's capsule exposes a catalog of Zero Abilities that agents can list and call, and any WP-CLI command can run against a Space directly. +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. 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). @@ -138,7 +138,7 @@ sf zero import emdash ../my-emdash-site --out . Run WP-CLI against a Space's WordPress, or against a local one with `--local`. -Everything after the flags is passed to `wp` unchanged, so `sf wp plugin list --format=csv` runs exactly that. With `--local`, WP-CLI's own output and exit code pass through untouched. Against a Space, the command runs remotely and reports whether it succeeded and how long it took. The platform returns no stdout, so read state back with a follow-up command such as `sf wp option get`. `--json` wraps only Spacefast's own errors, never `wp`'s output. See [WordPress on every Space](/wordpress). Use `--` to pass a flag `sf` would otherwise claim. +Everything after the flags is passed to `wp` unchanged, so `sf wp plugin list --format=csv` runs exactly that. With `--local`, WP-CLI's own output and exit code pass through untouched. Against a Space, the command runs remotely and reports whether it succeeded and how long it took. The platform returns no stdout, including for read commands such as `sf wp option get`. `--json` wraps only Spacefast's own errors, never `wp`'s output. See [WordPress on every Space](/wordpress). Use `--` to pass a flag `sf` would otherwise claim. ```text sf wp [--space ] [--local] [--path ] [--mode full|limited] -- diff --git a/content/index.mdx b/content/index.mdx index 775123f9..802c35cc 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -5,7 +5,7 @@ sidebar: order: 0 --- -Spacefast hosts the sites people build with AI agents. You hand it a folder, it hands back a URL. Everything you can do in the dashboard you can also do from the `sf` CLI, the HTTP API, or an MCP server. That means the agent that built the site can publish it too. +Spacefast hosts the sites people build with AI agents. You hand it a folder, it hands back a URL. Publish from the dashboard, the `sf` CLI, the HTTP API, or an MCP server. An agent with publishing permission can ship the site it built. Prefer a browser to a terminal? Drag a folder onto the dashboard instead — no install, no account required. See [Drop](/publish#publish). @@ -39,11 +39,11 @@ Publish this folder to Spacefast and give me the live URL. ## How it fits together -A **Space** is one site with a stable hostname. Every publish creates a new **version**, an immutable snapshot with its own URL that never changes. The `live` channel points at one version. Promoting or rolling back moves that pointer. Nothing gets rebuilt. (New terms piling up? See the [glossary](/glossary).) +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. -Every claimed Space belongs to a team, 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. +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 @@ Every claimed Space belongs to a team, even when you are its only member. On [Pl Drag a folder onto the dashboard. No install, no account required. - Space, version, live, capsule, Ability, grant — every term, defined once. + Space, version, live, capsule, Ability, grant — core terms in one place. What gets uploaded, how versions work, and how to roll back. diff --git a/content/troubleshooting.mdx b/content/troubleshooting.mdx index 6f13dd03..a604f35c 100644 --- a/content/troubleshooting.mdx +++ b/content/troubleshooting.mdx @@ -53,9 +53,9 @@ The codes name the stage. `build_failed` is your command exiting non-zero. `buil ### The site still shows the old version -Shared caches hold a copy for ten minutes. Non-immutable files are sent with `public, s-maxage=600, max-age=0, must-revalidate`. Your own browser revalidates on every load, but a shared copy can be up to ten minutes stale. +Ordinary public static files default to `public, s-maxage=600, max-age=0, must-revalidate`. Your browser revalidates on every load, but a shared copy can be up to ten minutes stale. Protected responses, conditional routing, and header overrides follow the exceptions on [Caching](/caching). -A publish purges every hostname the Space serves, which normally makes that moot. When the purge does not confirm, you get a `runtime_purge_failed` diagnostic and the runtime retries. The ten-minute lifetime is the backstop. Check which version answered you: +Activating a new version requests a purge of the Space's serving hostnames; immutable version hostnames are not purged. When the purge does not confirm, you get a `runtime_purge_failed` diagnostic and the runtime retries. The default ten-minute shared lifetime is the backstop. Check which version answered you: ```bash curl -sI https://my-site.view.fast/ | grep -i x-spacefast-version diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index 0204cc75..1d14f695 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -89,8 +89,8 @@ test("Troubleshooting gives a first diagnostic step before the error catalog", a const firstFactCases = [ { route: "/versions", - question: "Does rollback rebuild?", - evidence: [/rollback/iu, /doesn't rebuild|does not rebuild/iu, /live/iu], + question: "Does rollback rebuild, and which versions can it use?", + evidence: [/rollback/iu, /doesn't rebuild|does not rebuild/iu, /live/iu, /retained, ready version/iu, /deleted or expired versions cannot/iu], }, { route: "/crons", @@ -104,12 +104,12 @@ const firstFactCases = [ }, { route: "/access", - question: "What makes a Space private?", - evidence: [/removing every one/iu, /private/iu, /grant/iu], + question: "Does removing one public grant remove all access?", + evidence: [/removing a public grant/iu, /no other public grant matches/iu, /links, people, and team access can remain/iu], }, { route: "/caching", - question: "How do I force a fresh response?", + question: "How do I request a cache refresh?", evidence: [/republish/iu, /fresh response/iu], }, { From b43b34ccb86db17ce55d4c76a1409528e08c7332 Mon Sep 17 00:00:00 2001 From: Alexander Aidun Date: Tue, 6 Oct 2026 13:26:58 -0400 Subject: [PATCH 20/20] Reconcile team CLI restrictions and scoped eval expectations --- DOCS_WRITING_QA.md | 10 +++++ content/(concepts)/teams.mdx | 4 +- content/cli/teams.mdx | 81 ++++-------------------------------- evals.yaml | 8 ++-- 4 files changed, 25 insertions(+), 78 deletions(-) diff --git a/DOCS_WRITING_QA.md b/DOCS_WRITING_QA.md index 6878a13f..6c2a3638 100644 --- a/DOCS_WRITING_QA.md +++ b/DOCS_WRITING_QA.md @@ -81,3 +81,13 @@ Coverage by original PR section: The style guide and contributor instructions now require this meaning check. Existing built-output assertions were updated where they reinforced a misleading claim. No new regex suite is presented as independent proof of product behavior. Generated references remain producer-owned and unchanged. The previously recorded support-contact and API-key preset gaps remain outside these corrections. Verification with Bun 1.3.11 and Node 24 passed: frozen dependency install, generated-reference and command-example checks, type check, strict link validation, production build, all 26 docs tests, composed-site audit, public-safety check, Vale, route verification, and `git diff --check`. + +## Review follow-through, October 6, 2026 + +The next review caught two places the semantic pass had not reconciled: the `sf teams` lead contradicted its command examples, and the pagination eval still assumed a universal default. The team CLI guide now names the documented credential restriction beside all five affected commands, including invitation acceptance, and replaces success examples with dashboard instructions. The Teams guide now recommends the CLI only for listing invitations and states the credential restriction beside its role table. + +These corrections follow the authored authentication and permissions contract in `/authentication`, `/api-keys`, and `/agents/permissions`. The generated CLI snapshot lists command syntax but does not establish that a CLI credential can execute the action. No team membership was changed to test authorization, and the producer-owned snapshot was not edited. + +The pagination eval now expects endpoint-specific defaults, offset paging, and unpaginated lists. Checking the adjacent eval expectations also found an overly broad cache-purge answer; that case now names public static HTML, best-effort purge, and immutable version-hostname exceptions. These are corrected evaluation expectations, not a claim that the agent-based eval has run. + +Verification passed with Bun 1.3.11 and Node 24: the full repository check sequence, all 26 docs tests, and `git diff --check`. All 23 eval cases parsed and passed a structural check; no agent-based eval result is claimed. diff --git a/content/(concepts)/teams.mdx b/content/(concepts)/teams.mdx index e2b2b89c..bc968fa0 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -53,6 +53,8 @@ Three roles, and the one you hold in a team caps everything you do there, whethe | Grant the owner role | Yes | No | No | | Transfer ownership | Yes | No | No | +Invitation and membership changes require a signed-in person in the dashboard. CLI and API credentials cannot perform them, even for an owner or admin. See [credential limits](/api-keys#what-an-agent-grant-cannot-do). + Owner and admin differ in exactly one place. An admin can hand out `admin` and `member`, but only an owner can make someone else an owner. ## Invite someone @@ -71,7 +73,7 @@ Your team needs [Plus](/billing#seats) to add another member. Free and Go includ -From a terminal, `sf teams invitations add teammate@example.com --role member`, plus `ls`, `resend`, and `cancel`. +From a terminal, `sf teams invitations ls` lists invitations. Use the dashboard to send, resend, cancel, or accept one. Things that catch people out: diff --git a/content/cli/teams.mdx b/content/cli/teams.mdx index 895d0fc8..1ca1ebca 100644 --- a/content/cli/teams.mdx +++ b/content/cli/teams.mdx @@ -1,11 +1,11 @@ --- title: sf teams -description: Create teams, switch the default one, invite and remove members, and set the access preset new Spaces get +description: Create and select teams, list members and invitations, set defaults for new Spaces, and check which actions require the dashboard sidebar: order: 6 --- -Use `sf teams` to create or select a team and set defaults for new Spaces. Adding a member requires [Plus](/billing#seats); API keys and agent credentials cannot manage invitations or membership, so use the dashboard for those actions. +Use `sf teams` to create or select a team and set defaults for new Spaces. Adding a member requires [Plus](/billing#seats); CLI credentials can list members and invitations, but invitation and membership changes require the dashboard. [`sf login`](/authentication#log-the-cli-in) stores an API key, not a browser session. See [credential limits](/api-keys#what-an-agent-grant-cannot-do). A team owns Spaces, domains, and billing. Every member holds one team role: `owner`, `admin`, or `member`. See [Teams](/teams) for the model, and [`sf share`](/cli/share) for per-Space access, which is a separate system. @@ -108,7 +108,7 @@ Accept a team invitation. sf teams accept INVITATION ``` -Accepts an invitation and joins the team as the current login. +Accept the invitation through its join link in the dashboard. With CLI credentials, this request is refused with `403 authorization_level_not_allowed`. Once you accept in the dashboard, credentials approved for all teams can see the new team. ### Arguments @@ -116,21 +116,6 @@ Accepts an invitation and joins the team as the current login. | --- | --- | --- | | `` | required | Invitation ID from the invite link | -### Example - -```bash -sf teams accept inv_123 -``` - -### Output - -```text -Joined team acme as member. -Team: team_… -``` - -`--json` returns `{ team }` with `teamId`, `teamSlug`, `userId`, and `role`. - ## sf teams defaults Manage future-space defaults. @@ -201,6 +186,8 @@ Remove a team member. Aliases: `sf teams members remove`, `sf teams members dele sf teams members rm MEMBER ``` +Remove the member from the dashboard under **Members**. With CLI credentials, this request is refused with `403 authorization_level_not_allowed`. + ### Arguments | Argument | Default | What it is | @@ -213,16 +200,6 @@ sf teams members rm MEMBER | --- | --- | --- | | `-o, --team=` | default team | Team slug, ID, or name. Env `SPACEFAST_TEAM` | -### Example - -```bash -sf teams members rm jane@example.com -``` - -### Output - -`Removed team member jane@example.com.` `--json` returns `{ removed: true, member }`. - ## sf teams invitations ls List team invitations. Alias: `sf teams invitations list`. @@ -257,7 +234,7 @@ Create a team invitation. Alias: `sf teams invitations create`. sf teams invitations add EMAIL ``` -Invites a person to the team. They join with the role you set once they accept. +Send the invitation from the dashboard under **Members → Invite member**. With CLI credentials, this request is refused with `403 authorization_level_not_allowed`. ### Arguments @@ -272,21 +249,6 @@ Invites a person to the team. They join with the role you set once they accept. | `--role=owner\|admin\|member` | `member` | Role to grant when the invitation is accepted | | `-o, --team=` | default team | Team slug, ID, or name. Env `SPACEFAST_TEAM` | -### Example - -```bash -sf teams invitations add jane@example.com --role member -``` - -### Output - -```text -Invited jane@example.com as member. -Invitation: inv_… -``` - -`--json` returns `{ invitation }`. - ## sf teams invitations resend Resend a team invitation. @@ -295,7 +257,7 @@ Resend a team invitation. sf teams invitations resend INVITATION ``` -Resends a pending invitation. +Resend the invitation from the dashboard under **Members → Pending**. With CLI credentials, this request is refused with `403 authorization_level_not_allowed`. ### Arguments @@ -303,16 +265,6 @@ Resends a pending invitation. | --- | --- | --- | | `` | required | Invitation ID | -### Example - -```bash -sf teams invitations resend inv_123 -``` - -### Output - -`Resent invitation to jane@example.com.` `--json` returns `{ invitation }`. - ## sf teams invitations cancel Cancel a team invitation. @@ -321,27 +273,10 @@ Cancel a team invitation. sf teams invitations cancel INVITATION ``` -Cancels a pending invitation. The invitee can no longer accept it. This one always confirms: agent mode cannot auto-approve it, so scripts must pass `--yes`. +Cancel the invitation from the dashboard under **Members → Pending**. With CLI credentials, this request is refused with `403 authorization_level_not_allowed`. Passing `--yes` only skips the CLI confirmation; it does not grant permission. ### Arguments | Argument | Default | What it is | | --- | --- | --- | | `` | required | Invitation ID | - -### Example - -```bash -sf teams invitations cancel inv_123 --yes -``` - -### Output - -`Canceled invitation inv_123.` `--json` returns `{ canceled: true, invitation }`. - -### Errors - -| Code | Exit | What to do | -| --- | --- | --- | -| `confirmation_required` | 2 | Non-interactive run without `--yes`. Pass `--yes` | -| `cancelled` | 130 | You declined the prompt | diff --git a/evals.yaml b/evals.yaml index a554ef01..b9a0afeb 100644 --- a/evals.yaml +++ b/evals.yaml @@ -29,12 +29,12 @@ questions: expected: ["10"] routes: /cli - id: html-cache-header - question: What Cache-Control header do HTML pages get, and how do I bust it? - expected: ["s-maxage=600", "publishing purges the whole host"] + question: For public static HTML without header overrides or no-cache rules, what is the default Cache-Control policy, and what are the limits of a publish purge? + expected: ["s-maxage=600", "max-age=0", "whole-host purge when a new version is activated", "best-effort", "ten-minute shared lifetime is the backstop", "immutable version hostnames are not purged"] routes: /caching - id: pagination - question: What are the default and maximum page sizes on list endpoints, and how do I get the next page? - expected: ["default 20", "maximum 100", "pass nextCursor as cursor"] + question: What are the common cursor-pagination defaults, how do storage objects differ, and do all list endpoints use cursors? + expected: ["common default 20", "storage objects default 50", "maximum 100 for these cursor-paginated lists", "pass nextCursor as cursor", "documentation search uses offsets", "some lists are unpaginated"] routes: /api/pagination - id: rate-limit question: What are the API request limits per minute for an authenticated credential and an unauthenticated client IP?