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 84942bae..5b098208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,13 @@ 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. +- 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. @@ -60,6 +67,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 @@ -68,6 +76,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/DOCS_QA_COMPARISON.md b/DOCS_QA_COMPARISON.md new file mode 100644 index 00000000..c2cfc22b --- /dev/null +++ b/DOCS_QA_COMPARISON.md @@ -0,0 +1,310 @@ +# Old and new docs evaluation + +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). + +**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 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. + +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) + +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: 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 +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. + +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. + +## 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 | `main` | This branch | Destination / change | +|---|---|---|---|---| +| 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. + +### 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 | `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. + +### 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. + +## 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 + +- 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 new file mode 100644 index 00000000..10777933 --- /dev/null +++ b/DOCS_TEST_SUITE.md @@ -0,0 +1,31 @@ +# Docs regression tests + +Run the executable suite after building the site: + +```bash +bun run build +bun run test:docs +``` + +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 + +`scripts/docs-experience.test.mjs` checks that: + +- 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 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 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 + +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`. + +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 new file mode 100644 index 00000000..6c2a3638 --- /dev/null +++ b/DOCS_WRITING_QA.md @@ -0,0 +1,93 @@ +# Authored-docs writing audit + +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 + +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 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. + +## 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. 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`. + +## 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/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 00000000..dd2fa3b5 --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,267 @@ +# 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, 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: + +> 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." (31 words, 4 clauses) +> +> 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." (33 +> words, 4 clauses) +> +> 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 +merely shorter but still templated, is history now; don't resurrect it.) + +## 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: "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." +> +> 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, +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. diff --git a/content/(account)/api-keys.mdx b/content/(account)/api-keys.mdx index c91d9264..1a2fa46b 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. +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 @@ -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 4d95890f..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, lock the account down with two-factor, see every signed-in device, and 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 @@ -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..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, 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 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 19dfa0dc..35ab02d2 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -5,11 +5,11 @@ 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. +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 -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,16 +54,17 @@ 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, 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). +- **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 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 +128,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 7f209620..bc968fa0 100644 --- a/content/(concepts)/teams.mdx +++ b/content/(concepts)/teams.mdx @@ -5,13 +5,21 @@ 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. +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 + +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. ## 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 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 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 | |---|---| @@ -45,10 +53,14 @@ 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 +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. @@ -61,7 +73,7 @@ Owner and admin differ in exactly one place. An admin can hand out `admin` and ` -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: @@ -98,7 +110,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 +120,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 fe468dd1..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 --- -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. +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. @@ -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 c3fad78f..ee25e156 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. +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 @@ -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 5ed87066..c7b2a608 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -5,13 +5,17 @@ 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. +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?] +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 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 +124,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 b7a2dab2..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 the dashboard, know when it reaches a running site, and pick the right lane for a server-only secret. +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 @@ -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 bf9dfa09..d9df2709 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. +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. @@ -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. @@ -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/(dynamic)/logs.mdx b/content/(dynamic)/logs.mdx index 83290a15..8740ee54 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. +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)/storage.mdx b/content/(dynamic)/storage.mdx index 87cdabd6..5c11dce9 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. +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 @@ -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 cefde388..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. --- -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. +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` @@ -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 faef910e..6baf3041 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -5,13 +5,13 @@ 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. +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, 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. +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. @@ -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) | @@ -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 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)/anonymous-and-claim.mdx b/content/(publish)/anonymous-and-claim.mdx index d4e77174..2f5bf0dd 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. +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 @@ -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. @@ -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 @@ -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/(publish)/ci.mdx b/content/(publish)/ci.mdx index 413afd92..616b8218 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. +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 bf495e76..1921a764 100644 --- a/content/(publish)/frameworks.mdx +++ b/content/(publish)/frameworks.mdx @@ -5,24 +5,26 @@ 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. +Publish built output directly when you have it; Spacefast can build from source when you don't. ## 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 ``` -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. + +`--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 @@ -207,7 +209,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 7471a337..226821f4 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -1,14 +1,16 @@ --- 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 --- -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. +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. +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. @@ -158,11 +160,11 @@ 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 -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)/publish.mdx b/content/(publish)/publish.mdx index 072d57da..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, 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. +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 @@ -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 94eba00b..e4e7a511 100644 --- a/content/(publish)/wordpress-data-sources.mdx +++ b/content/(publish)/wordpress-data-sources.mdx @@ -3,15 +3,13 @@ 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. +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. +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 -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/(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)/glossary.mdx b/content/(reference)/glossary.mdx new file mode 100644 index 00000000..3443db32 --- /dev/null +++ b/content/(reference)/glossary.mdx @@ -0,0 +1,56 @@ +--- +title: Glossary +description: Core Spacefast terms used across these docs, defined 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 + +A build turns source code into publishable output. You can build locally or let Spacefast build remotely and publish the result. See [Frameworks and builds](/frameworks). + +## Capsule + +The compiled form of a Zero app. `sf publish` turns your TypeScript server code into a capsule, which the platform installs on the Space. See [Dynamic sites with Zero](/zero-runtime). + +## Claim / claiming + +Attaching an anonymous Space (one published with no account) to a real account and team, so it stops counting down to its expiry deadline. See [Publish without an account](/anonymous-and-claim). + +## 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 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 + +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 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 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 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/(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/(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/(serve)/access.mdx b/content/(serve)/access.mdx index 5c696649..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 } --- -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. +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 -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 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. @@ -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 14856a4f..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 } --- -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. +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` | | --- | --- | @@ -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 f2deb439..fdd33ac3 100644 --- a/content/(serve)/customization.mdx +++ b/content/(serve)/customization.mdx @@ -3,13 +3,13 @@ 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. +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. ## 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)/domains.mdx b/content/(serve)/domains.mdx index f9bdb8ff..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, read every verification and certificate state the platform reports, make the domain the Space's primary address, and move it somewhere else later. +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. @@ -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/(serve)/routing.mdx b/content/(serve)/routing.mdx index 95c6768d..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 } --- -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. +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. @@ -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 98eff027..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 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. +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: @@ -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 7cba20bc..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 --- -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. +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,9 +29,9 @@ 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, 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 e8a571ba..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 } --- -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. +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 @@ -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 @@ -28,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 0b3a7f99..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 --- -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. +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 @@ -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/claude-desktop.mdx b/content/agents/claude-desktop.mdx index 57555fb5..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, either through a double-click extension that runs the server on your machine or through the hosted endpoint. +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 0ebf4196..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, check builds, domains, and traffic, and fall back to bundled curl scripts when MCP is unavailable. +Codex publishes your project to Spacefast through the plugin, and falls back to curl scripts when MCP isn't available. ## Install @@ -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/cursor.mdx b/content/agents/cursor.mdx index d1487d8c..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 --- -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. +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/mcp-server.mdx b/content/agents/mcp-server.mdx index 1e9682b1..1928d849 100644 --- a/content/agents/mcp-server.mdx +++ b/content/agents/mcp-server.mdx @@ -3,7 +3,11 @@ 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. +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. +::: ## Two runtimes, one tool set @@ -25,9 +29,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 +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. -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. +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/agents/other-clients.mdx b/content/agents/other-clients.mdx index cf4e83ae..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 --- -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. +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 @@ -22,7 +22,7 @@ After this page you can wire Spacefast into an MCP client that has no first-part } ``` - 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. @@ -59,7 +59,7 @@ After this page you can wire Spacefast into an MCP client that has no first-part -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 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 7a67d026..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 --- -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. +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 @@ -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/agents/sf-setup.mdx b/content/agents/sf-setup.mdx index 428c5af3..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 --- -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. +`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 aa281d61..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 --- -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. +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/authentication.mdx b/content/api/authentication.mdx index 2f56a632..b27b23a0 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, but a valid token can still get a 403 on some routes. ## The header @@ -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..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 @@ -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..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. 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. +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 @@ -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/index.mdx b/content/api/index.mdx index 7e7695de..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 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. +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/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 405b4a3f..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. After this page 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, but a request that omits it 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,15 +37,15 @@ 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 -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). @@ -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 cecf6752..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. 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 expensive actions. Use the rate-limit headers when present; they can be absent when accounting is unavailable. ## The general budget @@ -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/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 1e0769b3..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 @@ -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..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. 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. +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 8deedff5..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 --- -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`. +`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. @@ -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..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. 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. 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). @@ -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/builds.mdx b/content/cli/builds.mdx index bc303c8c..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 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. +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 86386611..b82a52e8 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. +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). @@ -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 be08cf2b..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, 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. +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. @@ -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 2c47cda5..20e40b78 100644 --- a/content/cli/env.mdx +++ b/content/cli/env.mdx @@ -5,13 +5,13 @@ 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. +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). ## 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/git.mdx b/content/cli/git.mdx index aacbc239..2144a9ce 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. +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/index.mdx b/content/cli/index.mdx index 54d93a7d..6e98f3b2 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 @@ -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/cli/login.mdx b/content/cli/login.mdx index a7ee5f95..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 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. +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 a8dd71a4..2b1df5b8 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -5,9 +5,9 @@ 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. +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 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 never commit it. 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 69d6b82a..5a21fffa 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. +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 @@ -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 e2c924d4..2c83c807 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. +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 @@ -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 b1e4664f..1eefb739 100644 --- a/content/cli/source.mdx +++ b/content/cli/source.mdx @@ -5,11 +5,11 @@ 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. +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). -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/spaces.mdx b/content/cli/spaces.mdx index 5d9d0a4c..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: 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. +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 111794ee..fd201d29 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. +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). @@ -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 f0f5cf17..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 --- -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. +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. @@ -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 @@ -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/content/cli/versions.mdx b/content/cli/versions.mdx index 432d7bfd..aa99d461 100644 --- a/content/cli/versions.mdx +++ b/content/cli/versions.mdx @@ -5,9 +5,9 @@ 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. +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 d12070f0..5281ee4e 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. +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). @@ -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, 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] -- @@ -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..802c35cc 100644 --- a/content/index.mdx +++ b/content/index.mdx @@ -5,7 +5,9 @@ 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. 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). ## Publish in one command @@ -37,20 +39,31 @@ 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** holds a site and its **versions**, immutable snapshots with their own URLs. A publish with no changes can reuse the current version. The `live` channel points at the version visitors see, or is empty before the first publish. Promoting or rolling back to a retained, ready version moves that pointer without rebuilding. Preview publishes and manual promotion policies leave `live` alone. (New terms piling up? See the [glossary](/glossary).) + +Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs declare a [Zero runtime](/zero-runtime#declare-it) and server entry. -Static output is the default path. Point `sf publish` at a build directory and it uploads only the files that changed. If you'd rather publish source, Spacefast detects the framework and builds it. Sites that need a database, server functions, or scheduled jobs opt into the Zero runtime with one line of config. +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. Install, sign in, publish, and attach a domain in five minutes. + + Drag a folder onto the dashboard. No install, no account required. + + + Space, version, live, capsule, Ability, grant — core terms in one place. + What gets uploaded, how versions work, and how to roll back. 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/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/quickstart.mdx b/content/quickstart.mdx index b8a64338..1fc29a15 100644 --- a/content/quickstart.mdx +++ b/content/quickstart.mdx @@ -5,12 +5,14 @@ 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. +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 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? 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. + @@ -93,7 +95,12 @@ You need Node.js 20.3 or newer and a folder of built static files. Any framework ## Next +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). + + + Invite members, choose roles, and set access for new Spaces. + Install the Spacefast plugin for Claude Code, Cursor, or Codex. diff --git a/content/troubleshooting.mdx b/content/troubleshooting.mdx index ffac2611..a604f35c 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 that's often enough. Come back here once you have a specific error code or symptom from the list below. +::: + ## Publishing ### The publish says nothing changed @@ -43,15 +47,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. +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, the runtime retries, and 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 @@ -63,7 +67,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 +91,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 +125,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 +139,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). diff --git a/evals.yaml b/evals.yaml index db41262f..b9a0afeb 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 @@ -29,15 +29,15 @@ 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: 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 605231da..4c42fe3d 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": "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/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)}`); diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs new file mode 100644 index 00000000..1d14f695 --- /dev/null +++ b/scripts/docs-experience.test.mjs @@ -0,0 +1,131 @@ +import assert from "node:assert/strict"; +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 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, " "); +} + +test("every authored page opens with its subject instead of a page promise", async () => { + const offenders = []; + 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(route.path); + } + } + assert.ok(authored.length > 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), /\[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); +}); + +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, 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", + 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: "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 request a cache refresh?", + evidence: [/republish/iu, /fresh response/iu], + }, + { + route: "/stats", + 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, + ], + }, +]; + +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); + }); +} 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