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+git+facets. (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());
Calls to other spaces' public *.unfolder.space hosts are auto-allowed by the
egress gateway, so a plain fetch works with no setup.
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 the
unfolder.authprompt — for an internal admin, Cloudflare Access or HTTP Basic Auth is usually enough; for multiple editors, better-auth. - 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.