Separate-origin App hosting
Hosted App frontends (React/Svelte/plain-with-source) must be served from a
different registrable domain (eTLD+1) than the control plane. If Apps were
served on a subdomain of the control-plane host (supacloud.run), they would
be same-site with the API and a deployed App’s JavaScript could ride the
operator’s session cookie against /api. Serving from supacloud.net makes
every request cross-site, so SameSite=Lax withholds the session cookie,
cookie-tossing is impossible, and the CORS allowlist naturally excludes the apps
origin. This is the same isolation model GitHub uses with githubusercontent.com.
The feature is also gated by SUPACLOUD_APPS_HOSTED_FRONTEND (default false).
The flag must not be turned on without a separate apps domain configured — the
server refuses to boot in that combination.
Prerequisites
Section titled “Prerequisites”- A registrable domain (eTLD+1) that is distinct from your control-plane
domain. For SupaCloud SaaS this is
supacloud.net; self-hosted operators should provision their own equivalent (e.g.myapp-content.example). - A wildcard TLS certificate for
*.yourapps.domainand DNS routing for all subdomains to the SupaCloud server. The bw-infra reference deployment uses a Traefik wildcard cert via DNS-01. - The runner image must have been rebuilt with
vite --base=./so artifacts host at any subdomain root. Until that image is rolled out, keep the flag off.
How it works
Section titled “How it works”Each hosted-frontend deployment is assigned a stable, opaque 80-bit hex
subdomain (stored in app_deployments.subdomain, migration 172). The
subdomain is assigned once and re-used across redeploys so the URL is stable.
When SUPACLOUD_APPS_BASE_DOMAIN is set, a host-dispatch middleware
(interfaces/http/apps_host.rs) is mounted as the outermost layer of the
server. Any request whose Host header matches the apps domain or one of its
subdomains is served as App content and never reaches /api, the SPA, or
the login page. Only GET and HEAD are accepted on the apps origin; all other
methods return 405. The path-based route (/a/{workspace}/{route}) refuses
hosted frontends when an apps domain is configured, so each hosted App is
reachable exclusively through its opaque subdomain.
The boot guard in secret_hydration.rs rejects an apps_base_domain value
that shares an eTLD+1 with the control plane (checked in both directions via
the psl public-suffix list), so a misconfigured domain fails loudly at
startup rather than silently collapsing the isolation boundary.
Configuration
Section titled “Configuration”-
Set the apps content domain.
Add
SUPACLOUD_APPS_BASE_DOMAINto your environment, pointing at your separate registrable domain:SUPACLOUD_APPS_BASE_DOMAIN=supacloud.netThe value must be a bare domain — no scheme, port, path, or spaces.
supacloud.netis correct;https://supacloud.netis rejected at boot. -
Enable hosted-frontend serving.
SUPACLOUD_APPS_HOSTED_FRONTEND=trueThe server refuses to boot with this flag set but no apps domain configured.
-
Route wildcard DNS to your server.
Every
*.yourapps.domainrequest must reach SupaCloud. In the reference bw-infra deployment, Traefik handles a wildcard*.supacloud.netcertificate (InternetBS DNS-01) and forwards all subdomains to the server. -
Restart the server.
The boot guard validates both variables. On a successful start the
/configAPI endpoint will include"apps_base_domain": "yourapps.domain".
Per-deployment bearer token
Section titled “Per-deployment bearer token”A hosted App’s backend calls (e.g. a workflow trigger submitted via a form)
should not embed a user session. Instead, mint a per-deployment App API
token (prefix scwa_, stored as a hash in app_deployments.api_token_hash,
migration 173):
POST /api/apps/{id}/deployment/tokenAuthorization: Bearer <owner-session>The response returns the raw token value once. Store it server-side in
your App’s backend — never embed it in a public frontend bundle. The token
authenticates as a workspace-scoped service identity and is CSRF-exempt
(a non-cookie credential; CORS preflight blocks cross-site attachment of a
custom Authorization header). Revoke it when no longer needed:
DELETE /api/apps/{id}/deployment/tokenAuthorization: Bearer <owner-session>The token is also automatically cleared on undeploy so a redeploy cycle cannot silently re-arm a stale credential.
Verifying the setup
Section titled “Verifying the setup”After restarting, deploy a React or Svelte App from the SupaCloud UI. The
deployment detail page will show the assigned subdomain URL
(https://<opaque>.yourapps.domain). Opening that URL should serve the built
index.html with:
- A
Content-Security-Policyheader (default-deny,frame-ancestorsincludes the control-plane origin so the live preview iframe works cross-origin). X-Content-Type-Options: nosniff.<base href="/">injected intoindex.html.
Requests to the control-plane origin (/api) from the apps origin are blocked
by SameSite=Lax on the session cookie and excluded by the CORS allowlist.
Related
Section titled “Related”- Environment variable reference
- ADR 0041 — full decision record for App hosting and the separate-origin architecture.