Ready for sandbox
What a project must satisfy before its image is published to the reserved
sandbox or demo destination and launched from wbsp.ai. The build applies
this list on entry to the sandbox phase.
How to read an item. Each begins with a six-character identifier — quote
that, never a position in the list, because items are reordered, regrouped and
retired and the identifier is the only thing that stays put. Each ends with
where it comes from: G is the client guidelines index (with the section), M
a rule moved here from that index — the section named is where it used to
sit and where it no longer appears, because this page now owns it — R
the manifest reference, D the credential-less deploy page, E the platform's
standalone entrypoint, X a behaviour the platform verified in its own code,
Phil a direct instruction (dated), P a catalogue project by number, and C
the conductor's field record (list, item, date) from which the item was vetted.
Owner. The platform writes this page. Regenerating the guidelines digest never touches it. A retired item stays on the page under Retired so a quoted identifier always resolves.
A. Manifest
- 5dgf4p
destination.demois declared and carries none oftype,mode,tenant,cluster,enclave— the platform supplies all five and any one of them, even an empty one, is a hard error. Writedemo: {}or give it a real key: a baredemo:followed only by comments parses as null and is refused. (RReserved destinations;Csandbox 1;P42) - caxnpp Declare
sandbox:only when the sandbox must differ from the demo. An undeclaredsandboxborrows thedemo:block and reads.env.demo; a declared one resolves to its own block and reads.env.sandboxonly — the files never layer. Onwbspv0.2.44 and earlier the deploy wrongly refused an undeclared sandbox with "no destination sandbox is declared"; the borrow works on the deploy path from v0.2.45. Upgrade rather than mirror the block. (Rsandbox borrows demo;Xreleased in v0.2.45;Csandbox 2;P45, 61) - qgb3dc
release:is inert on demo and sandbox — the instance is one ephemeral pod that starts Postgres, creates the declared extensions, loads the sample data and starts the app; there is no step in which a migration could run. Declaring it is still correct foraws. Nothing else depends on it running here. (RRelease;Csandbox 3) - k2g5pp
sample_data.pathnames a directory of.sqlfiles, andtimeout_minutesstates the instance lifetime (default 10). The project has considered whether a walkthrough fits in it. (R;Csandbox 4) - 645fft No auth client secret is declared for demo or sandbox, at top level or in the block. The platform injects
AUTH_HAP_ISSUER,AUTH_HAP_ID,AUTH_HAP_SECRETandNEXT_PUBLIC_HAP_ENABLEDafter the app's own env. A top-levelsecrets:entry binds demo too and a declared secret with no value fails the publish closed, so anything declared at top level either has a value in.env.demoor isrequired: false. No OIDC client value — real or placeholder — belongs in.env.demoeither: the injected names arrive after the app's own environment and override anything baked in, so a value there is at best ignored and at worst the one the app reads. (M§6 Demo/sandbox destinations need no auth client secrets;R;Csandbox 5) - eyf24p A
source: generatedvalue is not minted on the sandbox or demo lane and blocks the publish until it has a value. Put a freshly generated value in.env.sandbox— that file and no other, because a sandbox publish with no.env.sandboxreads.env.demoand would hand a throwaway value to the public demo lane silently. Neverwbsp config set: that creates an operator-managed installation which the deploy path then refuses for ever. Never mark itrequired: falseand never carry the value to another lane — a recorded value outlives every instance and is shared between them, which is acceptable only where nothing real is stored. (X.env.sandboxfallback and the refusal, 2026-09-12;Csandbox 38–40;P45) - 8ycuum A project shipping more than one manifest (API, web, worker) publishes one combined image for demo and sandbox, both processes behind one origin. A launch is one manifest, one image, one random hostname, and nothing injects a sibling's address. (
Csandbox 6;P261) - 5g8sfd
universalis published after any cloud or demo publish, never before — a cloud or demo publish moves the image tag off the appliance index. (R;Dorder matters;Csandbox 7)
B. Sample data
- 9xx6bk The dump is a full
pg_dump— schema and data — never--data-only: no release step runs, so the schema exists only if the dump creates it. When a seed fails, the instance log names thepg_dumpflag that would have prevented it. (RRelease;E;Csandbox 8) - y8czk3 The seed loads as the application's own unprivileged role. Every extension the schema needs is under
database.extensionsso the platform creates it first, and the dump carries no statement only a superuser or extension owner may run: noCOMMENT ON EXTENSION, noALTER TABLE … DISABLE TRIGGER ALL, noOWNER TOa role that does not exist, noCREATE SCHEMAfor a schema an extension owns. A dump that loads on a laptop proves nothing, because a developer restores as superuser. (E;Csandbox 9–10;P261) - jmg2c3 The dump is taken from a clean migrate-and-seed in a throwaway database, never from the development database, so it carries the curated set and no test residue. The project states the counts (workspaces, users, records). (
Csandbox 11;P123) - q453k9 The demonstration data makes a credible first screen: populated, internally consistent, dated so that "scheduled" items are not all in the past at launch, and owned by a stated demo principal. (
G§4.4;Csandbox 12;P123) - h9k6gk The sample visitor is granted every demonstration workspace, not only the landing one. A feature spanning two workspaces — a consolidated statement, a group report — re-checks membership of every member and refuses the whole thing when the visitor holds only the parent, which is a refusal on exactly the data the demonstration exists to show. Check the grant against the widest-spanning feature. (
Csandbox 43;P61) - f8xwbd Where row-level security is forced and the application's own role owns the tables, the sample grant cannot discover its target by querying — a caller with no membership reads zero rows exactly when the landing set is needed. State the landing set as data, in a small table with no tenant rows and no policy, filled only by the seed. Do not widen a tenant table's read policy on every destination to serve one lane. (
Csandbox 44;P61)
C. Sign-in and the demo visitor
-
ag3mky A sandbox or demo application never asks the visitor to sign in. The visitor arrives from wbsp.ai already holding a platform session, and the demonstration either uses that session or is given invisibly-provisioned credentials from the demo tenant: the application starts, and there is no sign-in page, no button, no provider chooser, no username field, no "continue with…" and no consent screen. A visitor shown a sign-in form, a provider-choice screen or a "Sign in to continue" button has met a defect regardless of what happens after they click it, and a sign-in flow that silently fails to complete breaks the rule just as surely — nobody was asked, and the application still did not start. This holds for every project alike, and the mechanism is the platform's, not each project's to invent. (
Phil2026-09-13;M§6 A demo never asks the visitor to log in — at all;Csandbox 50) -
s6g9ve The first hop starts the authorization request. An unauthenticated visitor is redirected straight to the sign-in start route — never to an in-app sign-in page — and HAP completes silently for a visitor who already holds a session. The sign-in start route, the OIDC callback, the health endpoint and static assets are exempt from that redirect, or the callback loops. The test a project can apply without adjudication: does anything between arriving and the application being usable require input from the person? A button, a "continue", a chooser or a cookie banner all fail it. The injected client is what can sign a wbsp.ai visitor in without a prompt, but nothing happens until the application begins the authorization request: HAP answers without a prompt for a visitor who already holds a platform session, and shows Hosted Login only for one who does not, so an application that lands the visitor on its own sign-in page and waits for a button never gives the silent exchange a chance. The symptom does not name the cause — a demo that asks a signed-in visitor to log in looks like the platform failed to inject the client, and is reported that way, when it is almost always the application's own first hop. Keep an in-app sign-in page only as the fallback for an installation with no identity provider configured. (
M§6 Silent SSO only happens if your application starts it;Csandbox 15, 48) -
hjh8ys The application consumes the injected
AUTH_HAP_*names, mapping them onto its own variable names at startup if needed. It does not read a per-app client from.env.demo. (M§6 Demo/sandbox destinations need no auth client secrets;Csandbox 13) -
xjga9h The callback path is one of the four the shared demo client accepts under the
*.wbsp-demo.comwildcard:/api/auth/callback/hap,/api/auth/callback,/login/oauth2/code/hap,/signin-oidc. A version-nested path is not on the list and fails at the callback. No registration is made for demo or sandbox — the shared demo client already accepts those paths under the wildcard host, so the ephemeral URL works with no registration step, and a non-standard path is not a candidate for a one-off registration but a path to conform away from. (M§6 You also do not register any redirect URI;Csandbox 14) -
jcztwm The
redirect_uriis sent twice — on the authorize request and on the code exchange — and HAP compares the two as exact strings, so both are built from the same pinned instance URL. A client library handed the incoming callback URL derives the exchange's value from that URL instead of from configuration, and inside a Next.js standalone container that URL has been observed as the server's bind address (http://0.0.0.0:8080/…): every sign-in is refused withinvalid_grantwhile the readiness endpoint reports the right callback. Rebuild the URL from configuration and take only the query string from the request. The login bypass hides this underdevand a locally minted token hides it in the suites, so a launched sandbox is the first place it runs: the provider's refusal is shown on the application's error page, with the request URL's origin logged beside the configured one. (G§6 The redirect_uri is sent twice;Cnote 2026-09-15;P5) -
wmq2ep A callback whose pending sign-in state is missing restarts the sign-in once, carrying a short-lived marker, and renders a page on a second miss in a row. On this lane a dead-end page is also an application that did not start, which ag3mky counts as a failure. (
G§6 Make identity resolution resilient;Cnote 2026-09-15;P439) -
8r79ad The visitor is a subject the seed has never seen. In sample mode (
WBSP_SAMPLE_MODE=true, injected at launch) the application attaches every newly provisioned user to the seeded demonstration data with the role the demonstration needs; a per-workspace model otherwise shows an empty picker over a full database. Gate this on sample mode only, never on a real destination. (M§1;G§6;Csandbox 16;P123, 61) -
yvhyxd The local login bypass is off and unreachable in the demo image: not baked into the image, Dockerfile, entrypoint or compose file, and refused by the code on a cloud destination. (
G§6 The standalone image must ship with the bypass disabled;Csandbox 17) -
584s74 In a demo or sandbox the
Exititem does not forward to HAP's exit or end-session endpoint — that ends the wbsp.ai session the visitor arrived on. Gated onWBSP_SAMPLE_MODE, either omit the item or point it athttps://wbsp.ai, where the visitor came from. Prefer one of those two to an item that visibly does nothing. Outside demo and sandbox,Exitforwards to the issuer's/exitas usual. (M§6 A demo or sandbox is the exception;Phil;Csandbox 18, 34) -
azfe9u On the demo and sandbox lane the instance URL arrives as
NEXT_PUBLIC_APP_URLandAUTH_URL, never asAPP_URL: the hostname does not exist until launch, and the machinery that fills a declaredAPP_URLruns only on the image-deploy path that serves twist and prod. ReadAPP_URL ?? NEXT_PUBLIC_APP_URL ?? AUTH_URL, in that order. MarkingAPP_URLrequired: falseis correct; having no fallback to the names that are supplied is the bug. (Xlauncher env, verified 2026-09-13;Csandbox 45;P45) -
7u2f5j Sign in from the server, as a confidential client. The application discovers HAP's endpoints and exchanges the authorization code for tokens itself, authenticating with the injected
AUTH_HAP_SECRET; the browser performs only the two page navigations — out to the authorize endpoint and back to theredirect_uri— which is all a browser is needed for. The injected secret is the tell: a browser-side public client has nowhere safe to keep a secret and so never uses one, and the platform injects one into every instance. This was previously only implied, and a browser-side public client with PKCE is a perfectly standard pattern that nothing here forbade — so it is stated now rather than inferred. It also cannot work against HAP as deployed today: neither the discovery document nor the token endpoint returns anAccess-Control-Allow-Originheader, so the browser fetches each reply and then refuses to let the script read it, while the two page navigations are unaffected because browsers do not police navigation. That is exactly why it fails so quietly — the visitor is asked nothing, the page looks entirely correct, and it rests for ever on "redirecting to sign in" or a spinner, with nothing displayed as an error because from the application's point of view nothing failed; a fetch simply never returned a readable answer. Re-verified againsthttps://hap.wbsp.ai/t/wbspon 2026-09-13: discovery answers 200 with no such header andOPTIONSon the token endpoint answers 405. A public browser client has been asked for, so the CORS half may not always be true; the server-side shape is not a workaround for it but what a demonstration should do regardless, and ag3mky holds either way. (M§6 Sign in from your SERVER, with the client secret — not from the browser;Xre-verified 2026-09-13;Csandbox 51;P61)
D. No egress from a demo
- 7qsnzt A demo or sandbox instance does not interact with the public Internet or any external service: no email, no third-party API (language models, social networks, payment providers, CRMs, calendars, analytics, geocoding, package registries, third-party trace or metrics services), no webhooks, nothing synced, nothing passed out of the container. Structured logs to stdout are not egress — the platform collects them; log more in sample mode, not less. A visitor is looking at a demonstration, and nothing they type into it should reach a real inbox, a real account or a real invoice. (
M§1 A demo talks to nothing outside itself;Phil2026-09-11;Csandbox 19) - 3h67dq Every feature that would do one of those things is disabled when
WBSP_SAMPLE_MODEis set, and the interface shows a plain explanation in its place ("sending is switched off in the demonstration; the full product would email the guest"). Silently failing, queueing forever or logging an error is not disabling. A visible line is the difference between a demonstration that looks deliberately scoped and one that looks broken. (M§1 Say so in the interface, where the feature would have been;Phil;Csandbox 20) - 753fbh The gate is sample mode, not the absence of a key. An absent key degrades a feature by accident; a demo refuses egress by design, even with a key present. The dangerous integration is the one with a keyless public default — a rate feed, a holiday feed, a package index — because it is on by default and survives every review that asks "is the key missing?". Background workers, schedulers and queue consumers that would send or sync are inert, or drain to a visible "would have sent" record — a background job that fires an hour after a visitor leaves is still egress, and is the path most likely to be forgotten because nothing in the interface triggers it. A key baked into an image, left in
.env.demo, or injected later turns real sending back on silently, and the visitor has no way to know. (M§1 Gate on WBSP_SAMPLE_MODE, never on a missing key;Csandbox 21, 42;P61) - gxett5 A call to an AI model is refused because
WBSP_SAMPLE_MODEis set, not becauseAI_PROVIDERisnoneorAI_API_KEYis absent.AI_PROVIDER: noneis the honest declaration for the demo lane, but it is a configuration value, and a configuration value turns the feature back on the day someone supplies one; sample mode is the gate. The visitor sees the feature explained as off for the demonstration, never a configuration error. (G§5 The AI provider contract;M§5 In a demo or sandbox, the gate is still WBSP_SAMPLE_MODE, §1;Cnote 2026-09-15;P169) - 89r5xf The project inventories its outbound-capable code and accounts for each path in its reply: HTTP clients to external hosts, mail transports, vendor SDKs, OAuth "connect" flows to third parties, telemetry and error-reporting agents — with the behaviour of each in sample mode. Anything that opens a socket to a host the project does not control belongs on that list, so a reviewer can check each one rather than infer from the absence of failures. (
M§1 List your outbound paths;Csandbox 22) - ab5m6t Inbound webhook verification can itself be egress: an endpoint that verifies a signed notice by fetching the sender's certificate lets an outsider who posts to the public instance make it call out. In sample mode refuse before the fetch — answer politely, record a security event, make no request. Audit every inbound endpoint for a fetch performed during verification. (
Csandbox 35;P45) - vgcgj7 Assets and fonts are served from the image, not fetched from a CDN at runtime, so the page renders whole without leaving the container. (
M§1 A demo talks to nothing outside itself;Csandbox 23) - d5effp A capability, health or status endpoint is itself a claim and needs the same gate. An endpoint reporting
provider: ecborlanguage_model.configured: truedescribes a call the deployment now refuses to make; report every capability as it will actually behave underWBSP_SAMPLE_MODE. (Csandbox 41;P61) - jqux97 A demo instance reports itself healthy. Reserve the unhealthy state for something that ought to work on this deployment and does not; a feature switched off for the demonstration, or absent from this version, is listed and explained, not called a fault. The load-bearing part is the test that pins the opposite case — outside a demo a genuinely unconfigured dependency still reports unhealthy and names itself — because without it this item degrades into an endpoint that says
okunconditionally, which is worse than the noise it replaced. The reason recorded must say why a capability is unavailable, never merely that it was excluded. (Csandbox 47;P61) - xqvu6s Every behaviour gated on
WBSP_SAMPLE_MODEhas a test that setsWBSP_SAMPLE_MODE. Existing suites run with it off, so they pass on an application that is wrong in a demo — and pass for the right reason, since underdevthe ordinary rule applies. Put these tests where sample mode can be set, usually the unit suite. (G§8;Csandbox 46;P61) - y376rq The screen that carries the "switched off" explanation is the one least likely to have been rendered in tests, because it renders rows only in a state no test puts it in. The local proof renders the disabled-feature screens with the sample data loaded, not merely opens them. (
Csandbox 36;P45)
E. Runtime shape inside the demo image
- f7jktc One container: the demo image bundles the application with in-pod PostgreSQL (and Redis if enabled). Parallel services — a git server, a search engine, an analytics stack, anything beyond the managed database, Redis and object storage — are never bundled here, and never baked into an appliance image either; only a cloud deployment runs them in full. A feature that needs one is disabled with a note saying it is available in the full product, and a project whose demonstration would show nothing without it may decide it offers no demonstration at all rather than an empty one. (
M§1 Deployment topology and parallel services;R;Csandbox 24) - 643a46 The demo launcher forces port 8080: it injects
PORT=8080, routes the public hostname to 8080 and probes 8080, whatever the manifest'sport:says, anduse_declared_port:has no effect on this lane (the tool warns so). The image either listens on 8080 or honours the injectedPORT; a start script that hard-codes a port does not. A health path the platform can probe is declared. (RThe port contract;Xlauncher, verified 2026-09-13;Csandbox 25) - cjrctw The application reads the discrete
DATABASE_*andREDIS_*the entrypoint sets — noDATABASE_URLassumption — and treats Redis as a cache that starts empty. (G§4;E;Csandbox 26) - dncwau No environment check refuses a data-store host of
localhostwhen the app believes it is in production. The demo lane is the only place that is legitimately both: the launcher injectsDATABASE_HOST,REDIS_HOSTandMEMORYDB_HOSTaslocalhostbecause an all-in-one instance runs its stores beside the app, whileNODE_ENV=production(or its equivalent) is usually baked into the image. A guard written as "production must not point at localhost" — good practice everywhere else — then throws during environment validation, before a socket is opened, so the pod never becomes ready and the failure looks like a launch that died silently. Key the exception onWBSP_SAMPLE_MODE=true, which the launcher always sets, rather than on the project's own preview flag, which the platform does not set. (Xlauncher injects the three hosts as localhost andWBSP_SAMPLE_MODE=true, verified 2026-09-19;C2026-09-19;P117) - g5gmgt Redirects and generated links use the request origin or forwarded headers, because the instance lives on a random
<app>-<token>.wbsp-demo.comhostname nobody can configure in advance. (Csandbox 27;P123) - as7xfn A Next.js standalone image sets
HOSTNAME=0.0.0.0in its runtime stage.server.jsbindsprocess.env.HOSTNAME || '0.0.0.0'and a container runtime setsHOSTNAMEto the container id, so the server answers on that one address and loopback inside the container fails — taking the image's ownHEALTHCHECKand any in-container call with it. Every check made through a published port still passes, which is why it survives a proof made from outside. A demo pod is probed by address so it still launches;compose,standaloneand the appliance report the containerunhealthyfor its whole life. Prove it from inside the container. (Csandbox 37, twist 47;P45)
F. Publishing
-
c4a442
wbsp deploybuilds from the working tree. Before a publish the project readsgit statusat build time and records the output, and publishes from a clean tree or from a checkout of the intended commit, stating the commit in its reply. Onwbspv0.2.44 and earlier every demo or sandbox publish carried a-dirtysuffix whether or not the tree was clean — the publish wrote a scratch Dockerfile into the tree and then inspected the tree — so a suffix on an image published before v0.2.45 means nothing either way. From v0.2.45 the suffix is honest again. (Xreleased in v0.2.45;Csandbox 28, twist 43;P123, 45) -
d7fjhk Sign in again before publishing, and do not trust
wbsp login statusto say whether you need to: its "expired — refreshes automatically on next use" line has preceded both a publish that worked and one that died. The failure lands mid-publish — the layers push, then the command dies at the push grant before the tag moves. If that happens, compare registry digests rather than retry blind: when the failed run's immutable tag andlatestshare a digest, only the naming failed.wbsp login --tenant <tenant>is interactive and is the operator's to run. (D;Csandbox 49;P61) -
fqx8h4 The project cannot see a running instance's log (demo status is operator-only), so it proves the seed and the first hop on the standalone preview image locally before publishing: the seed loads as the app role with the declared extensions, the bypass is off, root redirects to the start route, and the health path answers. (
Csandbox 30;P261, 123) -
y8fpuy The reply carries the publish command's final output and the immutable image reference. (
Csandbox 29) -
w9m9qk A publish can report success and change nothing that is running — see pjdnma on the ready-for-twist page. Verify the published instance by something that changes between builds, not by the command's exit status. (
Ctwist 56;P45, 61)
G. After the launch
- ehyutj Someone with a wbsp.ai session opens a fresh instance: no page appears before the application, the visitor lands on populated data with the stated role, and every disabled feature shows its explanation rather than an error. That is the only proof; the project asks for the result. (
Csandbox 31;P123) - q4tmck The instance answers within its lifetime and the "demo ended" page appears after it, not a broken login. Anyone reporting "the sandbox asks me to log in" is asked which URL and whether the instance had expired. (
Csandbox 32)
H. Correspondence
- phg9mt The project changes nothing outside its repository, registers nothing with HAP for demo or sandbox, and sends nothing to a core project. Anything it believes is a platform matter goes in its reply, for the operator to take up. (
G§9;Csandbox 33)
Retired
None yet.