Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b902646
Add the Infrastructure overview and Move from pages
batuhan Nov 14, 2026
a7ec3ab
Tighten the Move from pages against the docs that own each fact
batuhan Nov 14, 2026
38f0cc6
Say Spacefast on the infra pages and name WP Cloud once, in the intro
batuhan Nov 14, 2026
12fb11a
Fold the open docs fixes that still match the product
batuhan Nov 14, 2026
3146197
Document traffic rules, comments, connectors, team access, and feedback
batuhan Nov 15, 2026
6188241
Call them Spaces, not sites
batuhan Nov 15, 2026
f5f44d7
Quote plan limits from the pricing data, and drop the last WP Cloud p…
batuhan Nov 15, 2026
b01920e
Fix what the pre-merge review found
batuhan Nov 15, 2026
181e940
Finish the review fixes and match dashboard label case
batuhan Nov 15, 2026
0dc6f34
Say crontabs run on a simple schedule for now, and that SPAs are infe…
batuhan Nov 15, 2026
e9367c8
Fix what CodeRabbit found on the move, feedback, limits, and connecto…
batuhan Nov 15, 2026
5df12bf
Say no connector account is the default once there are two
batuhan Nov 15, 2026
9a322a0
Say who can read a note sent with a team key
batuhan Nov 15, 2026
e7f0fd2
Fix the remaining CodeRabbit threads on move, share, domains, and age…
batuhan Nov 15, 2026
3264abf
Say agents can invite people, and correct the same-team domain claim
batuhan Nov 15, 2026
e49b2a6
Scope the domain claim in the error table to a hostname another Space…
batuhan Nov 15, 2026
9311064
Document `sf teams members invite`
batuhan Nov 15, 2026
a5cd247
Say what sf teams members invite prints when some invitations fail
batuhan Nov 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,18 @@ Spacefast.Hype = NO
Spacefast.Wordiness = NO
Spacefast.Condescension = NO
Spacefast.ApiKey = NO
Spacefast.Hosting = NO

; Generated error pages are a verified snapshot synced from the monorepo, and
; the OpenAPI prose extraction reads generated/openapi. Both stay exempt from
; the "hosting" rule until the snapshot is refreshed with the monorepo fixes;
; then remove these carve-outs.
[content/(reference)/errors/**]
BasedOnStyles = Vale, Spacefast
Vale.Spelling = NO
Spacefast.Hosting = NO

[*openapi-prose.md]
BasedOnStyles = Vale, Spacefast
Vale.Spelling = NO
Spacefast.Hosting = NO
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ alerts is the bar.
rules, which mirror the vocabulary registry in the product monorepo
(`packages/common/src/vocabulary.ts`): "API key" never "access token". The
infra brand is "WP Cloud" (never dotted or hyphenated). Prefer "infra";
name WP Cloud only when the external reference is necessary — never the
euphemism "the hosting provider". Marketing may use the family line "the
name WP Cloud only when the external reference is necessary. Never use the
word "hosting" in prose: say publish, serve, live, infrastructure, or
runtime (`Hosting.yml`). Generated error pages, OpenAPI prose, and the
shipped changelog are exempt until the generated snapshot is refreshed.
Marketing may use the family line "the
same infra as Automattic, WP Cloud, Pressable, and WordPress.com".
- Unknown technical words go in `styles/Spacefast/spelling-exceptions.txt`
(sorted, case matters for proper nouns). Identifiers in prose — commands,
Expand Down
6 changes: 3 additions & 3 deletions STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ that is.
(`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.
the external reference is genuinely necessary. Never use the word
"hosting" for Spacefast or its features (`Hosting.yml`). Say publish,
serve, live, infrastructure, or runtime.
- **"API key," never "access token"** (`ApiKey.yml`) — "OAuth access
token" is the one exempted phrase, since that's the protocol's own term.

Expand Down
18 changes: 12 additions & 6 deletions blume.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { pagefind } from "blume/search";
import { loadPricing, pricingVariables } from "./scripts/pricing.mjs";

import { redirects } from "./redirects.ts";
import { pricingTableMarkdown } from "./scripts/pricing-next.mjs";
import { planVariables, pricingTableMarkdown } from "./scripts/pricing-next.mjs";

const deploymentBase = "/docs";
// Agent guidance for llms.txt, kept as Markdown so Vale lints it with the pages.
Expand Down Expand Up @@ -62,8 +62,14 @@ const icons = {

export default defineConfig({
title: "Spacefast Docs",
variables: pricingVariables(await loadPricing({ fixture: new URL("./content/_pricing/document.json", import.meta.url) })),
description: "Publish and host the sites your agents build.",
variables: {
...pricingVariables(await loadPricing({ fixture: new URL("./content/_pricing/document.json", import.meta.url) })),
// Plan values from scripts/pricing-next.mjs, the source Pricing & Limits renders.
...planVariables(),
},
description: "Publish what your agents build.",
// Two links (components/Logo.astro): the mark goes to the Website home at the
// origin root, outside the /docs base; "Docs" goes to the Docs home.
logo: { href: "/", image: { light: "/spacefast-wordmark.svg", alt: "Spacefast" }, text: "Docs" },
// The 2026-10 brand: neutral greys, one green, dark first like the Website.
// The green is a ground with ink on it, never type on white, so light mode
Expand Down Expand Up @@ -168,12 +174,12 @@ export default defineConfig({
},
agents: {
llmsTxt: { details: llmsDetails },
// The WP Cloud footer, written out as Markdown for .md pages, llms-full.txt
// The upstream reference footer, written out as Markdown for .md pages, llms-full.txt
// and MCP: without this the component ships as raw JSX.
markdownComponents: {
PricingTable: ({ props }) => pricingTableMarkdown(String(props.table)),
PricingTable: ({ props }) => pricingTableMarkdown(String(props.table), props.row ? String(props.row) : undefined),
WpCloudDocs: ({ props }) =>
`Spacefast is built on Automattic's [WP Cloud](https://wp.cloud/). Using the [Partner API](${deploymentBase}/platforms/partner-api)? See [${String(props.title)}](${String(props.href)}) on WP Cloud Docs.`,
`Using the [Partner API](${deploymentBase}/platforms/partner-api)? See [${String(props.title)}](${String(props.href)}) in the upstream infrastructure reference.`,
},
// The Website publishes Spacefast's agent skill at the site root; a second,
// generated Docs skill would compete with it.
Expand Down
3 changes: 2 additions & 1 deletion components.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { defineComponents } from "blume";

import Breadcrumbs from "./components/Breadcrumbs.astro";
import Logo from "./components/Logo.astro";
import Pagination from "./components/Pagination.astro";
import Sidebar from "./components/Sidebar.astro";
import PricingTable from "./components/PricingTable.astro";
import WpCloudDocs from "./components/WpCloudDocs.astro";

export default defineComponents({
layout: { Breadcrumbs, Pagination, Sidebar },
layout: { Breadcrumbs, Logo, Pagination, Sidebar },
mdx: { PricingTable, WpCloudDocs },
});
41 changes: 41 additions & 0 deletions components/Logo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
// The brand lockup: the Spacefast mark links to the Website home, "Docs" links
// to the Docs home. Docs are always served inside the Website artifact at
// /docs, so the configured root-relative `logo.href` ("/") is emitted as is,
// without the deployment base, and reaches the Website home on whatever origin
// serves the page. The Website is not part of the Docs' client router, so the
// mark's link does a plain page load instead of a router fetch first.
import { withBase } from "blume/components/islands/base-path.ts";
import type { BlumeLogo } from "blume/core/data.ts";

interface Props {
site: { title: string };
logo?: BlumeLogo | null;
locale?: string;
}

const { site, logo } = Astro.props;
const label = logo?.text ?? site.title;
const link = "inline-flex items-center transition-opacity hover:opacity-80";
// The config inlines the wordmark so it follows the theme. Fail the build
// rather than ship a header whose mark silently disappeared.
if (!logo?.svg) throw new Error("components/Logo.astro needs blume.config.ts `logo` to resolve to an inline SVG.");
---

<div class="flex min-w-0 items-center gap-2" data-sf-brand>
<a
aria-label={`${logo.alt || "Spacefast"} home`}
class:list={[link, "shrink-0 text-foreground"]}
data-astro-reload
href={logo.href}
>
<span aria-hidden="true" class="inline-flex h-5 items-center [&>svg]:h-5 [&>svg]:w-auto" set:html={logo.svg} />
</a>
{
label && (
<a class:list={[link, "min-w-0 font-semibold text-base text-foreground"]} href={withBase("/")}>
<span class="truncate">{label}</span>
</a>
)
}
</div>
8 changes: 5 additions & 3 deletions components/PricingTable.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import { pricingTable } from "../scripts/pricing-next.mjs";

interface Props {
table: "plans" | "team" | "space" | "features" | "examples" | "payments";
/** Show only these rows, joined with " | ", so another page can quote plan limits from the same data. */
row?: string;
}

const { table } = Astro.props;
const { columns, groups, highlight } = pricingTable(table);
const { table, row: rowLabel } = Astro.props;
const { columns, groups, highlight } = pricingTable(table, rowLabel);
const span = columns.length + 1;
---

<div class="sf-pricing not-prose" data-pricing-table={table} tabindex="0" role="region" aria-label={`${table} pricing table, scroll sideways for every plan`}>
<div class="sf-pricing not-prose" data-pricing-table={table} tabindex="0" role="region" aria-label={`${rowLabel ? rowLabel.replaceAll(" | ", ", ") : `${table} pricing table`}, scroll sideways for every plan`}>
<table style={`--sf-cols: ${columns.length}`}>
<thead>
<tr>
Expand Down
2 changes: 1 addition & 1 deletion components/WpCloudDocs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ const { title, href } = Astro.props;

<aside class="not-prose mt-10 border-border border-t pt-4 text-muted-foreground text-sm leading-6 [&_a]:underline [&_a]:decoration-current/50 hover:[&_a]:text-foreground" data-wp-cloud-docs>
<p>
Spacefast is built on Automattic's <a href="https://wp.cloud/">WP Cloud</a>. Using the <a href={contentHref("/platforms/partner-api")}>Partner API</a>? See <a href={href}>{title}</a> on WP Cloud Docs.
Using the <a href={contentHref("/platforms/partner-api")}>Partner API</a>? See <a href={href}>{title}</a> in the upstream infrastructure reference.
</p>
</aside>
4 changes: 2 additions & 2 deletions content/(account)/api-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ Keys created through the API can also be scoped to a single Space instead of the

Some actions are reserved for a signed-in person, at every permission level. No API key, agent grant, or OAuth token can do them:

- Invite a member, cancel or resend an invitation
- Cancel or resend an invitation
- Change a member's role
- Remove a member
- Accept a team invitation

A credential that tries gets a `403` with `authorization_level_not_allowed`. Do those in the dashboard. Once you accept an invitation there, the device credentials you already approved pick the new team up on their own. More detail in [what agents are allowed to do](/agents/permissions).
A credential that tries gets a `403` with `authorization_level_not_allowed`. Do those in the dashboard. Creating an invitation is the exception: a credential with the `teams:members.write` scope, such as Agent access, can invite people with [`createTeamInvitation`](/api/reference/teams/createteaminvitation). Once you accept an invitation in the dashboard, the device credentials you already approved pick the new team up on their own. More detail in [what agents are allowed to do](/agents/permissions).

## Rotate a key

Expand Down
76 changes: 76 additions & 0 deletions content/(account)/auto-enroll.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: Allowed email domains
description: Let people with a verified company email address find and join your team without an invitation
---

An allowed email domain offers your team to anyone whose verified email address is at that domain. Spacefast never adds anyone automatically. A person becomes a member only when they press **Join**.

Only owners and admins can add or remove domains. An OAuth token needs the `teams:members.write` scope, including to list them.

## Add a domain

1. Open the team and go to **Members**.
2. Press **Join Link**. The **Let Teammates Join** dialog opens.
3. Type the domain, for example `example.com`, in **Company email domain**.
4. Press **Add Domain**.

Over HTTP, send [`createTeamAutoEnrollEntry`](/api/reference/teams/createteamautoenrollentry):

```bash
curl "https://api.spacefast.com/v1/teams/$TEAM_ID/auto-enroll" \
-H "Authorization: Bearer $SPACEFAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value":"example.com"}'
```

The `201` response holds the entry: `id`, `teamId`, `kind` (always `domain`), `value`, and `createdAt`. Adding a domain the team already has returns the existing entry. [`listTeamAutoEnrollEntries`](/api/reference/teams/listteamautoenrollentries) returns all entries, newest first.

Adding a domain needs a credential that acts for a person. A team API key or OAuth app acts for the person who created it. A key with no person behind it gets `forbidden`.

## Which domains Spacefast accepts

A domain is accepted when all of these are true:

- **A current member already uses it.** At least one member of this team must have a verified email address at exactly that domain. A subdomain does not count.
- **It is a domain, not an address.** A value with `@` is refused. Invite that person by email instead.
- **It is a registrable domain.** IP addresses, single labels such as `localhost`, and public suffixes such as `co.uk` are refused.
- **It is not a shared mailbox provider.** These are refused: `gmail.com`, `googlemail.com`, `outlook.com`, `hotmail.com`, `live.com`, `yahoo.com`, `proton.me`, `protonmail.com`, `icloud.com`, `me.com`, `aol.com`, `mail.com`, `gmx.com`, and `wordpress.com`.

Spacefast stores the canonical form: lowercase ASCII, with international names converted to their `xn--` form. `Bücher.de` becomes `xn--bcher-kva.de`. Matching uses the same form, so either spelling works for people who join.

## What a person sees

A signed-in person with a verified address at the domain sees your team under **Open invites** on the **Teams** page. The row reads **You have an @example.com email**. Their team's Spaces page also shows a **Teams you can join** notice with **View Teams**.

Every verified address on the account counts, including secondary addresses. The account does not need to use that address to sign in.

Pressing **Join** makes them a member with the `member` role. Owners and admins can change the role later on **Members**. Joining needs a signed-in browser session. An API key or an agent cannot join for a person. The API operation is [`joinTeam`](/api/reference/teams/jointeam).

## Seats and billing

A person who joins through a domain takes a seat like any other member. Members on Pro: {{plan-team-members-pro}}.

Personal and Go allow one member, the owner. On those plans, **Join** fails with `team_member_quota_exceeded`, so nobody can join until the team moves to Pro. See [Pricing & Limits](/pricing).

## Remove a domain

In **Join Link**, press **Remove** next to the domain. Confirm with **Remove Domain**.

Over HTTP, send [`deleteTeamAutoEnrollEntry`](/api/reference/teams/deleteteamautoenrollentry) with the entry `id`. It answers `204`.

- **Existing members stay.** Removing a domain changes no membership and no role.
- **New people need an invitation.** The team stops appearing under their **Open invites**.

To take access away from someone who joined through a domain, remove them on **Members**. See [Teams and members](/teams#change-a-role-or-remove-someone).

A member you remove cannot rejoin through the domain. To bring them back, send an invitation. A member who leaves on their own can rejoin through the domain.

## Errors

| Code | Status | Meaning |
| --- | --- | --- |
| `invalid_domain_name` | 422 | The value is not a registrable domain. IP addresses and public suffixes land here |
| `validation_error` | 422 | `details.reason` says why. `email_not_supported`: the value is an email address. `domain_not_allowed`: the domain is a shared mailbox provider. `domain_not_represented`: no member has a verified address there |
| `not_found` | 404 | The entry does not exist or belongs to another team |
| `forbidden` | 403 | Your role cannot manage members, or the credential has no person behind it |
| `team_member_quota_exceeded` | 409 | A person pressed **Join** while the team was at its member limit |
4 changes: 2 additions & 2 deletions content/(account)/billing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ Use the returned entitlements for feature checks.

## When a payment fails

Your sites keep serving through a grace period, but you cannot change anything.
Your Spaces keep serving through a grace period, but you cannot change anything.

| Stage | How long | What visitors get | What you get |
| --- | --- | --- | --- |
| Past due | 7 days from the failed payment | Sites serve normally | Reads work. Every write returns `402` with code `tenant_past_due` |
| Past due | 7 days from the failed payment | Spaces serve normally | Reads work. Every write returns `402` with code `tenant_past_due` |
| Suspended | After the grace period ends | Every space serves `402` and the text `This space is paused.` | Reads work. Every write returns `403` with code `tenant_suspended` |

Settle the payment in the billing portal to restore access. API reads remain available in either state.
Expand Down
Loading
Loading