demo-host-servingrun.md02_define/output/spec.mdThe demo tenant is only reachable at platform.sustentus.com, behind the ordinary sign-in. There is
no address to hand a prospect, nothing that says "this is the demo", and — because the app has no
notion of which host it is answering on — nowhere for demo-only behaviour to hang. That missing
front door is what blocks the presenter-free demo the batch is building toward, and with it the
vendor-partner conversations that Refine the bridge / Q2-2026 Objective 1 — Establish
Product-Market Fit with Vendor Partners depends on: today every prospect walkthrough costs a
scheduled call and a human driver.
This is stub 1 of 3 in demo-public-access. It deliberately stops at the front door: the entry page
is a placeholder, so the host is provably live and the demo-only branch provably isolated before
demo-auto-sign-in puts a real session behind it.
Serve the existing apps/web deployment at a second domain, demo.sustentus.com, on the same
Vercel project, and teach the app which host it is answering on.
Host resolution is configuration, not a literal. A new apps/web/lib/ module owns the whole
question "is this a demo request?":
DEMO_HOSTS — a comma-separated allowlist of exact hostnames (production: demo.sustentus.com),
read from the environment and declared in turbo.json → globalEnv. Unset or empty means no host
is a demo host, and every behaviour below is exactly today's.VERCEL_ENV is not "production", ?demo=1 marks the session as a
demo request via a cookie (and ?demo=0 clears it). The override is hard-off in production —
there, allowlist membership is the only thing that makes a request a demo request.On a demo request, an unauthenticated visitor to a protected path is redirected to a new demo
entry page at /demo instead of /sign-in, carrying the attempted path as redirect_url so
demo-auto-sign-in can later land the visitor where they aimed. The entry page is a placeholder: a
short introduction to the demo and a link to the ordinary sign-in. Signed in, /demo redirects to
the user's role home, mirroring how app/page.tsx already behaves.
On the platform host nothing changes. /demo is not public there — an unauthenticated visitor is
sent to /sign-in like any other protected path, which is what keeps the demo branch provably
isolated to the demo host. The demo branch sits entirely in front of the !userId check in
apps/web/proxy.ts, so once a session exists both hosts run the identical path: role resolution,
route policies, View As, the expert-onboarding gate and the first-run onboarding gate are untouched.
Demo-host responses carry X-Robots-Tag: noindex, nofollow, declared in apps/web/next.config.ts
via a host-matched headers() rule rather than in the proxy, so it covers every response without
perturbing the proxy's control flow.
Three setup actions are the operator's, not the code's, and are written into the docs at Ship: adding
the domain to the apps/web Vercel project plus its DNS record, setting DEMO_HOSTS in the
Production environment, and confirming the production Clerk instance's allowed-origins list (if it is
restricted) includes the new host. Clerk needs no further configuration — session cookies are shared
across subdomains of one root domain, and satellite domains apply only to different root domains.
/admin) redirects to
/demo, not /sign-in, preserving the attempted path and query as redirect_url./demo renders to an unauthenticated demo-host visitor: an introduction to the demo and a
working link to /sign-in./demo redirects to the signed-in user's role home./demo redirects to /sign-in (with
redirect_url=/demo), and every other unauthenticated path redirects to /sign-in exactly as
it does today — no demo entry page is reachable, and the sign-in flow is untouched.!userId check.X-Robots-Tag: noindex, nofollow; platform-host responses do
not.VERCEL_ENV set to "production", ?demo=1 and the
override cookie have no effect — only DEMO_HOSTS membership makes a request a demo request.DEMO_HOSTS unset or empty, every route behaves exactly as it does today on every host.DEMO_HOSTS is declared in turbo.json → globalEnv, and pnpm lint is clean of
turbo/no-undeclared-env-vars.DEMO_HOSTS, override honoured off production, override ignored on production.?demo=1: the demo branch is exercised end to end
(redirect → entry page → sign-in link) and the same preview without the override behaves as the
platform host does.Operator steps, outside the diff, recorded in the docs at Ship and required before the production
host is live: add demo.sustentus.com to the apps/web Vercel project and its DNS record; set
DEMO_HOSTS=demo.sustentus.com in the Production environment; confirm the production Clerk
instance's allowed-origins list includes the new host.
demo-auto-sign-in (stub 2). This feature's entry page is a placeholder by
design.demo-public-guardrails (stub 3).isDemo semantics, the nightly reset's
purge/keep policy — and the reset cadence for public traffic.sitemap.* or robots.* in any app (verified across apps/web and apps/marketing), so
there is nothing listing the demo host. X-Robots-Tag above is what actually delivers the intent;
this run introduces no sitemap or robots artefact.sustentus.com, a session started on
either host is visible on the other. Recorded, not solved here — demo-auto-sign-in owns it.apps/web (and apps/demo) rows to the per-app Vercel table in
technical/deployment, which predates this run.platform.sustentus.com/demo does) were settled in the Define conversation and are written into
Proposed change above.03_build/output/notes.mdci-status.sh call returned PENDING on its 900s window with three previews
still building; nothing had failed, and the re-run settled GREEN.)apps/web/lib/demo-host.ts (new): the whole "is this a demo request?" question in one
module — the DEMO_HOSTS allowlist, the non-production ?demo=1 override, the entry
route constant. Pure and env-driven, so next.config.ts, proxy.ts and the page all
answer the question the same way and the tests can drive it directly.apps/web/proxy.ts: three additions, all in front of the !userId check so nothing
downstream can behave differently on the two hosts —
(1) ?demo=1/demo=0 sets or clears the override cookie and redirects to the same URL
without the param, so the flag lives in one cookie rather than every link;
(2) the demo entry path returns early only on a demo request, which is what keeps
the page off the platform host;
(3) the unauthenticated redirect targets the entry page on a demo request and
/sign-in everywhere else, carrying redirect_url identically in both cases.apps/web/app/(auth)/demo/page.tsx (new): the placeholder entry page — introduces the
demo, links to /sign-in, and redirects a signed-in visitor to their role home the way
app/page.tsx already does.apps/web/next.config.ts: X-Robots-Tag: noindex, nofollow on demo-host responses, via
a host-matched headers() rule. Chosen over a proxy branch because it covers every
response, including the paths the proxy matcher skips, and leaves the proxy's control
flow alone. A cookie-matched twin covers the preview door, and is emitted only off
production so a forged cookie can never put the header on a platform-host response.turbo.json: DEMO_HOSTS and VERCEL_ENV declared in globalEnv — turbo runs in
strict env mode, so an undeclared read is stripped from the build and absent from the
cache key, and turbo/no-undeclared-env-vars fails lint.apps/web/lib/demo-host.test.ts (new): unit tests written from the acceptance criteria./demo, not /sign-in, with
redirect_url — proxy.ts !userId branch./demo renders unauthenticated on the demo host, with a link to /sign-in — the
demo-only early return plus the new page./demo → role home — the page's own auth() check./demo → /sign-in?redirect_url=/demo, every other path
exactly as before. /demo is deliberately not in isPublicRoute and has no
route-policies.ts entry — deny-by-default is what makes it unreachable there, and
that absence is the mechanism, not an oversight.!userId check; nothing after it reads the host.X-Robots-Tag: noindex, nofollow on the demo host only — next.config.ts.readDemoOverride and isDemoRequest both gate on
isDemoOverrideAllowed(); covered by tests.DEMO_HOSTS unset or empty → today's behaviour on every host — parseDemoHosts
returns [] and isDemoHost is false for every host; covered by tests.DEMO_HOSTS declared in turbo.json → globalEnv.?demo=1 — Verify's to run on the deploy preview; it is an
operator check, not something Build can assert.…vercel.app/admin?demo=1 → expect a redirect to
/demo?redirect_url=%2Fadmin, the entry page, and its sign-in link working. Then
…/admin?demo=0 → expect the ordinary /sign-in redirect, i.e. the same preview
behaving as the platform host. curl -I the preview with the cookie set to confirm
X-Robots-Tag.next.config.ts now imports ./lib/demo-host.
Next compiles the config and supports local TS imports, but it is the only place in
this repo that does it — if the Vercel build trips, that import is the first suspect
and the fallback is to inline the parse there.sustentus.com, so a session started
on either host is visible on the other. Known and recorded in the spec; demo-auto-sign-in
owns it. Not a defect in this run.DEMO_HOSTS in Production, Clerk allowed origins). Those are written into
the docs at Ship.04_verify/output/verify.mdapps/web onlyUnauthenticated criteria were demonstrated against the live preview with curl, using the
?demo=1 override that stands in for the not-yet-existing demo domain.
GET /admin with the demo cookie →
307 location: /demo?redirect_url=%2Fadmin (agent)GET /demo with the cookie →
200, body contains "The Sustentus demo" and href="/sign-in" (agent)GET /admin with no cookie → 307 location: /sign-in?redirect_url=%2Fadmin,
byte-identical to today's behaviour (agent)GET /demo with no cookie →
307 location: /sign-in?redirect_url=%2Fdemo (agent)X-Robots-Tag: noindex, nofollow on a demo response, and NOT on a non-demo one — the
differential is clean because Vercel's own preview header is the bare value noindex,
so noindex, nofollow can only be this feature's rule (agent)GET /admin?demo=0 → the cookie is cleared with a 1970 expiry (agent)VERCEL_ENV=production. readDemoOverride and
isDemoRequest both gate on isDemoOverrideAllowed() (agent, by test + code reading)!userId check, and nothing after that check reads
the host. Recorded as not verified rather than assumed.DEMO_HOSTS is read at build time, so setting it in Vercel requires a redeploy.
next.config.ts evaluates headers() when the app is built, which bakes the host-matched
noindex rule into the routing manifest; the middleware's read is inlined by the same build.
Setting the variable in the Vercel dashboard and waiting will therefore do nothing — the
operator must set it and then redeploy. This is the one way the rollout can look broken
while everything is correct, and it belongs in the runbook at Ship. Not a code change."undefined" into process.env — afterEach restored the
saved values by assignment, and assigning undefined to process.env.X coerces to "undefined",
leaking a bogus value into every later test in the same worker. Fixed on branch: restore now
deletes the key when it was unset to begin with.demo-auto-sign-in, not a defect here: redirect_url is built from
nextUrl.pathname + search, so a request to //evil.com yields a protocol-relative value. This
is pre-existing behaviour of the /sign-in branch and unchanged by this run — but
demo-auto-sign-in will be the first code to actually consume redirect_url, and must
validate it as a same-origin path before redirecting.sustentus.com, so a session started on either
host is visible on the other. Known, recorded in the spec, owned by demo-auto-sign-in.DEMO_HOSTS, redeploys, and confirms Clerk's allowed origins. With DEMO_HOSTS unset the
feature is inert and every host behaves exactly as it does today — the rollout fails safe.05_ship/output/investor-update.mdWho it's for: Admin, and every prospect given the demo What shipped: The demo tenant now answers at demo.sustentus.com — an unauthenticated visitor meets a public entry page, not sign-in. Why it matters: Refine the bridge — an address to hand a prospect, before the auto sign-in drops the presenter.
Live once DNS and DEMO_HOSTS are set.
Dig deeper: https://github.com/sustentus/sustentus/pull/864
05_ship/output/release.mdtechnical/demo-environment (the two hosts, the ?demo=1 preview override, and
a "Standing the demo host up" runbook including the build-time read of DEMO_HOSTS) ·
technical/deployment (custom domains now names the second web domain and the redeploy caveat)demo.sustentus.com does not resolve until the operator adds DNS, sets
DEMO_HOSTS and redeploys, so a help-centre changelog entry announcing the address would be
false on the day it published. demo-auto-sign-in (stub 2) is the run that makes this visible
to a user, and is where the end-user note belongs..icm/runs/demo-host-serving/ to apps/docs/archive/pipeline-runs/. The
demo-public-access epic is not finished — demo-auto-sign-in and demo-public-guardrails
are still in intake — so the epic folder stays put./demo with redirect_url — demonstrated
on the preview: GET /admin + demo cookie → 307 /demo?redirect_url=%2Fadmin200, body carries
"The Sustentus demo" and href="/sign-in"/demo → role home — the page's own auth() check (code-read; no operator pass)GET /admin with no cookie → 307 /sign-in?redirect_url=%2Fadmin;
GET /demo with no cookie → 307 /sign-in?redirect_url=%2Fdemo!userId check (code-read; not demonstrated by an operator)X-Robots-Tag: noindex, nofollow on the demo host only — demonstrated, and distinguishable
from Vercel's own bare noindex preview headerDEMO_HOSTS unset or empty → today's behaviour everywhere — unit testsDEMO_HOSTS declared in turbo.json → globalEnv — lint green in CIapps/web/lib/demo-host.test.ts, run by the
Quality workflow?demo=1 end to end — performed in Verify, including the ?demo=0
off-switch clearing the cookie