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.demo is declared and carries none of type, mode, tenant, cluster, enclave — the platform supplies all five and any one of them, even an empty one, is a hard error. Write demo: {} or give it a real key: a bare demo: followed only by comments parses as null and is refused. (R Reserved destinations; C sandbox 1; P 42)
  • caxnpp Declare sandbox: only when the sandbox must differ from the demo. An undeclared sandbox borrows the demo: block and reads .env.demo; a declared one resolves to its own block and reads .env.sandbox only — the files never layer. On wbsp v0.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. (R sandbox borrows demo; X released in v0.2.45; C sandbox 2; P 45, 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 for aws. Nothing else depends on it running here. (R Release; C sandbox 3)
  • k2g5pp sample_data.path names a directory of .sql files, and timeout_minutes states the instance lifetime (default 10). The project has considered whether a walkthrough fits in it. (R; C sandbox 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_SECRET and NEXT_PUBLIC_HAP_ENABLED after the app's own env. A top-level secrets: 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.demo or is required: false. No OIDC client value — real or placeholder — belongs in .env.demo either: 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; C sandbox 5)
  • eyf24p A source: generated value 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.sandbox reads .env.demo and would hand a throwaway value to the public demo lane silently. Never wbsp config set: that creates an operator-managed installation which the deploy path then refuses for ever. Never mark it required: false and 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.sandbox fallback and the refusal, 2026-09-12; C sandbox 38–40; P 45)
  • 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. (C sandbox 6; P 261)
  • 5g8sfd universal is published after any cloud or demo publish, never before — a cloud or demo publish moves the image tag off the appliance index. (R; D order matters; C sandbox 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 the pg_dump flag that would have prevented it. (R Release; E; C sandbox 8)
  • y8czk3 The seed loads as the application's own unprivileged role. Every extension the schema needs is under database.extensions so the platform creates it first, and the dump carries no statement only a superuser or extension owner may run: no COMMENT ON EXTENSION, no ALTER TABLE … DISABLE TRIGGER ALL, no OWNER TO a role that does not exist, no CREATE SCHEMA for a schema an extension owns. A dump that loads on a laptop proves nothing, because a developer restores as superuser. (E; C sandbox 9–10; P 261)
  • 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). (C sandbox 11; P 123)
  • 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; C sandbox 12; P 123)
  • 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. (C sandbox 43; P 61)
  • 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. (C sandbox 44; P 61)

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. (Phil 2026-09-13; M §6 A demo never asks the visitor to log in — at all; C sandbox 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; C sandbox 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; C sandbox 13)

  • xjga9h The callback path is one of the four the shared demo client accepts under the *.wbsp-demo.com wildcard: /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; C sandbox 14)

  • jcztwm The redirect_uri is 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 with invalid_grant while 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 under dev and 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; C note 2026-09-15; P 5)

  • 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; C note 2026-09-15; P 439)

  • 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; C sandbox 16; P 123, 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; C sandbox 17)

  • 584s74 In a demo or sandbox the Exit item does not forward to HAP's exit or end-session endpoint — that ends the wbsp.ai session the visitor arrived on. Gated on WBSP_SAMPLE_MODE, either omit the item or point it at https://wbsp.ai, where the visitor came from. Prefer one of those two to an item that visibly does nothing. Outside demo and sandbox, Exit forwards to the issuer's /exit as usual. (M §6 A demo or sandbox is the exception; Phil; C sandbox 18, 34)

  • azfe9u On the demo and sandbox lane the instance URL arrives as NEXT_PUBLIC_APP_URL and AUTH_URL, never as APP_URL: the hostname does not exist until launch, and the machinery that fills a declared APP_URL runs only on the image-deploy path that serves twist and prod. Read APP_URL ?? NEXT_PUBLIC_APP_URL ?? AUTH_URL, in that order. Marking APP_URL required: false is correct; having no fallback to the names that are supplied is the bug. (X launcher env, verified 2026-09-13; C sandbox 45; P 45)

  • 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 the redirect_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 an Access-Control-Allow-Origin header, 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 against https://hap.wbsp.ai/t/wbsp on 2026-09-13: discovery answers 200 with no such header and OPTIONS on 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; X re-verified 2026-09-13; C sandbox 51; P 61)

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; Phil 2026-09-11; C sandbox 19)
  • 3h67dq Every feature that would do one of those things is disabled when WBSP_SAMPLE_MODE is 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; C sandbox 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; C sandbox 21, 42; P 61)
  • gxett5 A call to an AI model is refused because WBSP_SAMPLE_MODE is set, not because AI_PROVIDER is none or AI_API_KEY is absent. AI_PROVIDER: none is 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; C note 2026-09-15; P 169)
  • 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; C sandbox 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. (C sandbox 35; P 45)
  • 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; C sandbox 23)
  • d5effp A capability, health or status endpoint is itself a claim and needs the same gate. An endpoint reporting provider: ecb or language_model.configured: true describes a call the deployment now refuses to make; report every capability as it will actually behave under WBSP_SAMPLE_MODE. (C sandbox 41; P 61)
  • 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 ok unconditionally, which is worse than the noise it replaced. The reason recorded must say why a capability is unavailable, never merely that it was excluded. (C sandbox 47; P 61)
  • xqvu6s Every behaviour gated on WBSP_SAMPLE_MODE has a test that sets WBSP_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 under dev the ordinary rule applies. Put these tests where sample mode can be set, usually the unit suite. (G §8; C sandbox 46; P 61)
  • 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. (C sandbox 36; P 45)

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; C sandbox 24)
  • 643a46 The demo launcher forces port 8080: it injects PORT=8080, routes the public hostname to 8080 and probes 8080, whatever the manifest's port: says, and use_declared_port: has no effect on this lane (the tool warns so). The image either listens on 8080 or honours the injected PORT; a start script that hard-codes a port does not. A health path the platform can probe is declared. (R The port contract; X launcher, verified 2026-09-13; C sandbox 25)
  • cjrctw The application reads the discrete DATABASE_* and REDIS_* the entrypoint sets — no DATABASE_URL assumption — and treats Redis as a cache that starts empty. (G §4; E; C sandbox 26)
  • dncwau No environment check refuses a data-store host of localhost when the app believes it is in production. The demo lane is the only place that is legitimately both: the launcher injects DATABASE_HOST, REDIS_HOST and MEMORYDB_HOST as localhost because an all-in-one instance runs its stores beside the app, while NODE_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 on WBSP_SAMPLE_MODE=true, which the launcher always sets, rather than on the project's own preview flag, which the platform does not set. (X launcher injects the three hosts as localhost and WBSP_SAMPLE_MODE=true, verified 2026-09-19; C 2026-09-19; P 117)
  • g5gmgt Redirects and generated links use the request origin or forwarded headers, because the instance lives on a random <app>-<token>.wbsp-demo.com hostname nobody can configure in advance. (C sandbox 27; P 123)
  • as7xfn A Next.js standalone image sets HOSTNAME=0.0.0.0 in its runtime stage. server.js binds process.env.HOSTNAME || '0.0.0.0' and a container runtime sets HOSTNAME to the container id, so the server answers on that one address and loopback inside the container fails — taking the image's own HEALTHCHECK and 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, standalone and the appliance report the container unhealthy for its whole life. Prove it from inside the container. (C sandbox 37, twist 47; P 45)

F. Publishing

  • c4a442 wbsp deploy builds from the working tree. Before a publish the project reads git status at 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. On wbsp v0.2.44 and earlier every demo or sandbox publish carried a -dirty suffix 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. (X released in v0.2.45; C sandbox 28, twist 43; P 123, 45)

  • d7fjhk Sign in again before publishing, and do not trust wbsp login status to 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 and latest share a digest, only the naming failed. wbsp login --tenant <tenant> is interactive and is the operator's to run. (D; C sandbox 49; P 61)

  • 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. (C sandbox 30; P 261, 123)

  • y8fpuy The reply carries the publish command's final output and the immutable image reference. (C sandbox 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. (C twist 56; P 45, 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. (C sandbox 31; P 123)
  • 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. (C sandbox 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; C sandbox 33)

Retired

None yet.