Playground production topology
How the playground's frontend and hosted API deploy, connect, and roll back in production — issue
Status: Accepted
Context
By the time this decision was needed, three of the playground's pieces already existed as independent deployables, each already the subject of its own decision:
Modeller.Api(issue #71,docs/architecture/decisions/hosted-workspace-api.mdx) — a container deployed as themodeller-apiVercel project, live athttps://modeller-next.vercel.app.apps/website(issue #69) — a static, zero-backend Next.js app deployed as themodeller.websiteVercel project: two flagship example pages with build-time-generated projection data, explicitly documented (apps/website/README.md) as having "no request-time .NET execution and no editing yet."apps/studioin playground mode (issues #72/#73) — a Next.js app with its ownNEXT_PUBLIC_MODELLER_STUDIO_MODE=playgroundbuild variant that callsModeller.Apidirectly from the browser, keeps its draft insessionStorage, and supports stateless share links and workspace download. Documented as a "sibling app" toapps/websitethat "deploys independently" — but nothing before this issue actually deployed it or decided how a visitor onmodeller.websitereaches it.
This issue closes that last gap: where does the interactive playground live relative to
modeller.website, and what does production hardening (TLS, environment separation, security
headers, monitoring, rollback, privacy) look like across all three deployables together.
Decision: a path under modeller.website, via Next.js Multi Zones
The interactive playground (apps/studio, playground mode) is reachable at
https://modeller.website/playground — the existing modeller.website Vercel project, not a
new public-facing domain. apps/studio still builds and deploys as its own Vercel project (kept
separate rather than merged into apps/website's codebase — the two apps have unrelated
dependency trees, build configs, and release cadences), but that deployment gets no domain of its
own that visitors ever see. Instead, apps/website proxies the path through to it using Next.js's
"Multi Zones"
pattern: a rewrites() rule forwards /playground and /playground/:path* to the studio
deployment's own (internal-only) Vercel URL, and apps/studio sets a matching basePath so its
own routes and /_next/* asset requests resolve correctly once mounted under that prefix.
This supersedes an earlier draft of this decision that proposed a studio.modeller.website
subdomain instead — reversed because the playground should present as part of the existing
modeller.website site, not a separate one. The basePath is only ever set via the studio Vercel
project's own environment variables (NEXT_PUBLIC_STUDIO_BASE_PATH=/playground); local dev and
the Playwright suite leave it unset, so apps/studio keeps serving from / exactly as before —
this decision changes nothing about local development.
Because the rewrite is a transparent proxy, the browser's address bar and Origin never change —
the playground genuinely runs same-origin with the rest of modeller.website. That also means each
app's own response headers (including its Content-Security-Policy) pass straight through
unmodified; apps/website's headers() config explicitly excludes /playground so its own,
narrower CSP doesn't also get sent alongside apps/studio's wider one (which Monaco/onigasm need —
sending both would recreate the blank-page CSP regression hit while implementing this issue, just
via a second conflicting header instead of a too-strict single one).
SiteFooter.tsx's "try it live" link is now a same-origin relative link (/playground); the
playground links back to https://modeller.website for the flagship examples and
https://modeller.wiki for documentation, matching the existing modeller.wiki-links-to-the-
playground / modeller.website-hosts-the-playground split from issue #68's own site-split
decision.
Note: apps/website's existing /examples/[slug] pages (issue #69) already describe themselves in
copy and metadata as "a public playground for exploring Modeller examples" — a static, read-only
viewer, unrelated to and predating the interactive apps/studio playground this issue is about.
There's no routing collision (/examples/* vs /playground), but the two features sharing the
word "playground" in visitor-facing copy is a naming overlap worth resolving in site copy at some
point; out of scope for this infrastructure decision.
Environment separation
The modeller-api and modeller.website Vercel projects keep their own Production and Preview
environments per Vercel's standard model. The new apps/studio project (internal-only — no public
domain) gets the same treatment: a Preview deployment (any non-main branch or PR) is the
"owner-only preproduction deployment before public launch" this issue's scope asks for, with no
separate infrastructure to provision. modeller.website's STUDIO_DEPLOYMENT_URL (consumed by its
rewrites()) points Preview builds at the studio project's own Preview deployment and Production at
its Production deployment. The playground's NEXT_PUBLIC_MODELLER_API_URL is set per-environment on
the studio project so it can point at either the same production Modeller.Api or a Preview
deployment of it. Modeller.Api's Cors:AllowedOrigins
(src/Modeller.Api/appsettings.Production.json) covers https://modeller-next.vercel.app,
https://modeller.website, and https://www.modeller.website. The playground being same-origin
with modeller.website removes the need for a studio-specific entry, but it does not remove the
API from the cross-origin picture at all: the browser still calls Modeller.Api directly, so the
allowlist has to match whichever host the visitor is actually on.
www.modeller.website is that host. The apex 308s to www, so every browser request carries
Origin: https://www.modeller.website. An earlier revision of this decision claimed "no new CORS
entry is needed" and listed only the apex; the result was a deployed playground where every analyze
call failed with Failed to fetch and a deployed Initiative form that could never start a session.
Both surfaces were reported as broken before anyone noticed the redirect. Any future domain change
(adding a custom API domain, dropping www, adding a marketing host) has to be paired with the
allowlist, and the check is cheap: load the deployed page and read the Origin the browser sends.
TLS, security headers, and error/leak safety
TLS is automatic on every Vercel-assigned and custom domain; nothing to configure beyond attaching
the domain. Both Next.js apps that serve the playground's UI now set baseline security headers via
headers() in their next.config.mjs (apps/website, apps/studio) — X-Content-Type-Options,
Referrer-Policy, Permissions-Policy, frame-ancestors 'none' (neither app is meant to be
iframed), and a Content-Security-Policy scoped to what each app actually loads (apps/studio's is
the more involved of the two: worker-src blob: for Monaco's editor worker,
script-src ... 'wasm-unsafe-eval' for the onigasm WASM grammar engine, and connect-src
restricted to 'self' plus the configured Modeller.Api origin). These are a considered starting
baseline, not an exhaustively audited final policy — tighten further once real deployed traffic
confirms exactly which asset origins each app actually needs (a follow-up, not blocking here).
Both of those headers describe cross-origin permission, which makes them easy to fall out of step
with the code that actually makes the call. apps/website resolves the API origin once in
next.config.mjs and feeds the same value to both its connect-src and (via Next's env config)
its client bundle, so the two cannot disagree; the resolution prefers
NEXT_PUBLIC_MODELLER_API_URL when the Vercel project sets it and otherwise defaults to the
deployed API in production and http://localhost:8080 in development. The default matters: while
the variable was unset on that project, production shipped the localhost fallback and the CSP
listed no API origin at all, so the Initiative form failed twice over — once on the CSP, once on
the unreachable host — behind a single "Is the Modeller API running?" message.
apps/studio's basePath has the same class of hazard on the asset side. public/ files are
served under /playground, so any hard-coded absolute asset path (/grammars/onigasm.wasm and
friends in src/lib/monaco-languages.ts) resolves to the site root and 404s once deployed, taking
syntax highlighting down with it while local dev — where basePath is unset — stays green. Those
paths are built from NEXT_PUBLIC_STUDIO_BASE_PATH, the same source as basePath itself.
Modeller.Api registers no UseExceptionHandler/UseDeveloperExceptionPage middleware, so an
unhandled exception in Production falls through to Kestrel's own default: an empty-bodied 500, no
stack trace, no source. Combined with the no-source-logging guarantee already established in
hosted-workspace-api.mdx (structured logs carry only request metadata — document/projection
counts, outcome, elapsed time, diagnostic codes — never document content), no code path exposes
submitted model source or infrastructure credentials in an error response or a log line.
Monitoring, budgets, and privacy
apps/website and apps/studio both already depend on @vercel/analytics (and apps/website
additionally on @vercel/speed-insights) — real-user monitoring and Core Web Vitals without any
new infrastructure; enabling them is a Vercel dashboard action per project (see Action items).
Modeller.Api already registers OpenTelemetry tracing/metrics unconditionally
(hosted-workspace-api.mdx's Observability section) — cost/availability alerting on top of that is
whichever OTLP collector/backend is chosen; no collector is provisioned in this pass (same "not yet
chosen" state hosted-workspace-api.mdx already documented), and none of the three projects has
custom cost/usage alerts, because Vercel's Spend Management (configurable alert destination and
threshold) is a Pro-plan feature and all three projects run on the free Hobby plan. Hobby's
alternative — an automatic hard usage cap that pauses the project rather than overspending, with no
configurable destination or threshold — is accepted as the cost control for this deployment. If
traffic grows enough that an unplanned pause becomes a real availability risk, revisit by either
upgrading to Pro for real alerting or standing up an external check (e.g. a scheduled job hitting
/healthz and Vercel's usage API).
apps/website/src/app/privacy/page.tsx is the privacy statement this issue's scope asks for,
linked from SiteFooter.tsx and from the playground's own footer text — it covers: no accounts;
playground drafts live in browser sessionStorage only, never persisted server-side; share links
encode content into the URL fragment client-side (never uploaded, but anyone the link is sent to
can read it — the same caution as sharing any link); Modeller.Api logs request metadata only,
never submitted document content; anonymized Vercel Analytics/Speed Insights telemetry, no
model-content tracking.
Deployment and rollback
All three projects follow the same Vercel-native pattern already established for modeller-api in
hosted-workspace-api.mdx: build → deploy as a new revision → rollback by promoting the previous
deployment from the Vercel dashboard's Deployments list (or vercel rollback from the CLI) — no
rebuild required, so rollback is fast and doesn't depend on the failing commit's build succeeding.
Because each project is independent, a bad Modeller.Api deploy and a bad apps/studio deploy roll
back separately without coupling one project's incident to another's. A bad apps/studio deploy is
also naturally low-blast-radius for modeller.website itself: the rewrite only affects the
/playground path, so a broken studio deployment doesn't take down the flagship example pages or
anything else on the site — rolling back modeller.website isn't even needed unless
STUDIO_DEPLOYMENT_URL itself needs to change.
Action items (require Vercel dashboard access — not achievable from source control)
- Create the
apps/studioVercel project (a project likemodeller-studiois fine — it needs no public domain of its own): link this repo, set Root Directory toapps/studio, setNEXT_PUBLIC_MODELLER_STUDIO_MODE=playground,NEXT_PUBLIC_STUDIO_BASE_PATH=/playground, andNEXT_PUBLIC_MODELLER_API_URL=https://modeller-next.vercel.app(Production) plus whatever Preview-environment equivalents are wanted. - Set
STUDIO_DEPLOYMENT_URLon themodeller.websiteproject to that project's own Vercel deployment URL (e.g.https://modeller-studio.vercel.app), for both Production and Preview. - Enable Vercel Analytics/Speed Insights for both projects (
modeller-api,modeller.website) plus the newapps/studioproject. Analytics is currently unenabled onmodeller.website: its injected script 404s on every page load. Per-project cost/usage alerts are not configured — Spend Management requires the Pro plan, which none of the three projects is on; Hobby's automatic usage-cap pause is the accepted substitute (see Monitoring, budgets, and privacy). - Optional: set
NEXT_PUBLIC_MODELLER_API_URLon themodeller.websiteproject. Production no longer depends on it (next.config.mjsdefaults to the deployed API), but setting it per environment is how a Preview build is pointed at a Preview API. - Keep
Cors:AllowedOriginsin step with the domains actually served.www.modeller.websiteis the canonical host today; the apex redirects to it.