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.auth prompt — 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.