Multiple spaces, one user
One space = one site. The platform deliberately doesn't have a "sub-site"
or "sub-app" concept. If you want N sites, you create N spaces — each
with its own slug, its own public site at <your-space-slug>.unfolder.space, and
its own workspace and live facet. (Management lives on the platform, not
at the site host.)
Cross-space communication is over HTTP. A "main" space can call into a "portfolio" space exactly the way it would call any other origin:
const data = await fetch("https://portfolio.unfolder.space/api/works")
.then(r => r.json());
Cross-space calls go through the same default-deny egress gateway as any
other host — only the calling space's own host is auto-allowed. Approve the
target first (config.allowEgress({ host: "portfolio.unfolder.space" }), then
the owner activates it in settings) and the plain fetch above works; the S2S
pattern below already includes this step.
Pattern: split the admin out into its own space
A clean way to keep an authoring/admin surface off the public site is to make it
a separate space: the website space (acme.unfolder.space) stays open and
fast, and a sibling admin space (acme-admin.unfolder.space, or a custom
domain like admin.acme.com) holds the dashboard. Benefits:
- Blast radius — the admin's auth, dependencies, and bugs can't take down the public site; deploy and iterate on it independently.
- Clean gating — the entire admin space is private, so you gate it once at
the front door rather than threading auth through public routes. Scope its
login with one of the
unfolder.auth-*patterns — for an internal admin, HTTP Basic Auth (unfolder.auth-basic) is usually enough; for multiple editors,unfolder.auth-accounts(better-auth). (Auth runs in your facet — edge Cloudflare Access can't gate a space.) - Shared data over HTTP — the admin writes through the website space's own private API (or its facet SQLite via an endpoint), reached with the server-to-server pattern below so only the admin space can call it.
// in the admin space: push a change to the website space's private endpoint
await fetch("https://acme.unfolder.space/api/internal/publish", {
method: "POST",
headers: { "content-type": "application/json" }, // + the shared S2S token (below)
body: JSON.stringify({ slug: "post-1", title: "Hello" }),
});
(If a single space is enough, you can instead gate an /admin path inside one
space — but separate spaces give you the isolation above.)
Securing server-to-server (private endpoints)
A public endpoint is readable by anyone. To expose an endpoint only to your other space, share a secret between the two and check it server-side — declare the same secret in both spaces (the owner sets the value in settings, never the agent), and send it as a header from the caller:
- Caller space — declare the secret, allow the callee's host, and attach the
header yourself:
// in run_code on the caller config.declareSecret({ name: "S2S_TOKEN" }) // owner sets the value in settings config.allowEgress({ host: "portfolio.unfolder.space" }) // worker code: // fetch("https://portfolio.unfolder.space/api/works", // { headers: { "X-S2S-Token": env.S2S_TOKEN } }) - Callee space — declare the same-valued secret (owner pastes the same
value in its settings) and reject requests whose header doesn't match
env.S2S_TOKEN. Keep the endpoint path unguessable too if it's sensitive.
The token's value is owner-set (never the agent), so it stays out of git log
and the bundle — see secrets.
Shared headers & redirects
For shared headers or cross-space redirects, configure each space's
AssetConfig via config.setAssetConfig inside run_code (see
bundling). Redirect entries accept cross-host
targets like https://other.unfolder.space/path. There is no shared config
across spaces — set it on each space that needs it.