Skip to content
Select themeSelect language

Why separate-origin App hosting

SupaCloud can serve hosted App frontends — plain, React, or Svelte source folders built by the Vite worker. This page explains the security boundary between those hosted frontends and the SupaCloud control plane, and why they must live on a separate registrable domain.

The SupaCloud control plane is a cookie-authenticated API. The session cookie is set with SameSite=Lax, host-only (no Domain attribute), HttpOnly, and Path=/. As long as a hosted App and the API share the same origin, a malicious (or compromised) App can call /api/* and the browser silently attaches the visiting operator’s session cookie. The App then executes with the full session privileges of whoever opened it.

This risk is present even with a strict default-deny CSP: script-src 'self' and connect-src 'self' still permit same-origin fetch calls to /api, and SameSite=Lax only withholds cookies on cross-site requests — not same-origin or same-site ones.

An obvious first answer is “put Apps on apps.supacloud.run.” It is not sufficient. A browser site equals the registrable domain (eTLD+1), not the full origin. app.supacloud.run and api.supacloud.run are the same site — supacloud.run — so SameSite=Lax still forwards the session cookie on requests from one to the other. In addition, a subdomain under the same eTLD+1 can set a Domain=supacloud.run cookie that is visible to all sibling subdomains (cookie-tossing / shadow-cookie attack). Neither SameSite nor host-only cookies close these paths; the only mechanism that does under a shared eTLD+1 is a CSP sandbox attribute that nullifies the App’s origin entirely — a brittle approach with its own failure modes.

The solution: a different registrable domain

Section titled “The solution: a different registrable domain”

SupaCloud serves hosted App frontends on supacloud.net, a domain with a different eTLD+1 from the control-plane domain (supacloud.run). This makes every hosted App cross-site to the control plane. With the existing cookie configuration, the session-riding closes automatically:

  • SameSite=Lax withholds the session cookie on cross-site requests from the app to /api.
  • Cross-eTLD+1 rules prohibit a Domain attribute that would span both domains, so cookie-tossing is structurally impossible.
  • The control plane’s exact-allowlist CORS policy naturally excludes the apps domain; no CORS relaxation is needed.

SUPACLOUD_APPS_BASE_DOMAIN — set to supacloud.net on the hosted deployment. This is the only configuration required on the SupaCloud side.

Boot guard (secret_hydration.rs) — SupaCloud refuses to start if the apps domain shares an eTLD+1 with the control-plane host. The psl crate compares registrable domains; equal, subset, or sibling-subdomain relationships are all rejected.

SUPACLOUD_APPS_HOSTED_FRONTEND=true additionally requires SUPACLOUD_APPS_BASE_DOMAIN to be set; the guard enforces this at boot so the hosted path can never be enabled without the isolation boundary in place.

Per-deployment opaque subdomain (migration 172) — each deployed hosted App receives a stable 80-bit hex subdomain (e.g. a3f8b2….supacloud.net), assigned once and stable across redeploys. Only hosted frontends (react, svelte, plain-with-source) receive a subdomain; form-renderer Apps are served only on the control-plane path and are unaffected by this change.

Host-dispatch middleware (interfaces/http/apps_host.rs) — mounted as the outermost layer of the axum router. A request whose Host matches supacloud.net or a subdomain of it is served as App content and never reaches the control plane — no /api routes, no SPA, no login page. Only GET and HEAD are accepted; all other methods are rejected with 405 Method Not Allowed.

Path route isolation (services/app_deployments/serve.rs) — when an apps domain is configured, the control-plane path route (/a/{workspace_slug}/{route_path}) refuses hosted frontends. A hosted App is reachable only on its separate-origin subdomain. The #113 form-renderer App (manifest-only plain App) is not a hosted frontend and continues to be served on the path route.

Per-App callback token (migration 173, prefix scwa_) — a server-side, owner-issued bearer token for App→/api callbacks. Because the App’s JS cannot carry the operator session cookie cross-origin, App backends that need to call the SupaCloud API use this token instead. It is minted via POST /api/apps/{id}/deployment/token, is workspace-scoped, and is revoked automatically on undeploy. The token is used server-side and is never embedded in the public frontend.

Control Closes Notes
Separate registrable domain (supacloud.net) Session-riding via SameSite=Lax Primary isolation
Cross-eTLD+1 Cookie-tossing / shadow-cookie attack Structural; no code required
Boot guard (eTLD+1 check) Misconfigured same-site apps domain Fail-closed at startup
Host-dispatch middleware Hosted App reaching /api routes GET/HEAD only
Path-route refusal Hosted App on control-plane origin Form-renderer exempt
Per-App callback token App→API identity without session Owner-issued, hash-only

For operational setup, see Configuring App hosting and the Environment variables reference. The full design rationale is in ADR 0041.