WBSP guidelines digest — routing
A router, not a summary. Find the phase you are in, read only what it binds.
Generated 2026-09-21 against 37 documents (corpus unrecorded).
Phase bindings
Find the phase you are in. Read what it binds and nothing else.
setup
Nothing beyond the warnings that always apply.
direction
- application-creator-guide.md
- parallel-services-guide.md
- single-tenant-guidelines.md
- wbsp-client-guidelines.md §3
- wbsp-client-guidelines.md §4
- wbsp-yaml-reference.md
implement
- hap-administrator-claims-md
- hap-client-cookbook-md
- hap-hosted-login-guidelines-md
- hap-integration-guide-md
- logging-client-guidelines.md
- nextJS-api-guidelines.md
- path-prefixes.md
- wbsp-client-guidelines.md §1
- wbsp-client-guidelines.md §2
- wbsp-client-guidelines.md §6
- wbsp-client-guidelines.md §7
- wbsp-client-guidelines.md §8
local-run
- configuring-an-installation.md
- hap-getting-started-md
- hap-local-login-guidelines-md
- useful-commands.md
- wbsp-client-guidelines.md §4
- wbsp-client-guidelines.md §5
- wbsp-yaml-reference.md
refine
- hap-application-selector-md
- ready-to-test-checklist-md
- wbsp-client-guidelines.md §6
test
- hap-testing-md
- ready-to-test-checklist-md
- wbsp-client-guidelines.md §8
package
- credential-less-deploy.md
- wbsp-client-guidelines.md §3
- wbsp-yaml-reference.md
sandbox
- application-creator-guide.md
- hap-sso-broker-guidelines-md
- hap-sso-client-guidelines-md
- ready-for-sandbox-checklist-md
- wbsp-client-guidelines.md §1
- wbsp-client-guidelines.md §6
- wbsp-yaml-reference.md
production
- configuring-an-installation.md
- credential-less-deploy.md
- hap-application-lifecycle-md
- hap-registering-an-application-md
- path-prefixes.md
- ready-for-twist-checklist-md
- wbsp-client-guidelines.md §5
- wbsp-yaml-reference.md
handover
- credential-less-deploy.md
- useful-commands.md
- wbsp-client-guidelines.md §3
- wbsp-yaml-reference.md
Bound to no phase
Recorded rather than dropped, so a reader can judge the decision.
- HOWTO-bootstrap-developer.md — pre-pipeline: a developer's first login
- hap-api-reference-md — 89 KB endpoint reference; the integration guide and cookbook carry what a build needs, and this is looked up on demand rather than read
- hap-appearance-customization-md — tenant branding, an operator concern rather than a project build step
- hap-auth-config-md — operator: HAP server environment
- hap-backup-restore-md — operator: backing up auth data
- hap-docs — navigation index for the HAP set, not content
- hap-entitlement-administration-md — administering who may use an app; an operator task after deployment
- hap-env-example — operator: HAP server environment
- hap-installation-guide-md — operator: running a HAP server
- hap-openapi-yaml — machine-readable specification, 323 KB — a third of the whole corpus; only for generating a typed SDK or an API client import
- hap-technical-overview-md — orientation, not a task
- hap-useful-commands-md — operating a HAP instance, not building against one
- hap-user-guide-detailed-md — orientation, not a task
- installation.md — pre-pipeline: setting up a machine
Landmines by phase
Traps that live inside a bound document and are not visible from its title. The warnings that apply whatever you are doing are not here — they are in your context file.
direction
- Amazon MemoryDB is a DIFFERENT product from Redis and is provisioned only by its own
memorydb: { enabled: true }block: it runs the Redis protocol in cluster mode (a cluster-aware client, slotted keys, hash tags for multi-key commands), injects its own MEMORYDB_* variables (never REDIS_), and costs about six times a Redis instance. REDIS_ always describes plain Redis and never needs a cluster client. All three settings — redis.enabled, redis.durable, memorydb.enabled — default to false. (wbsp-client-guidelines.md §4) - Cache-mode Redis (the default,
durable: false) is ephemeral: anything in it, including queued jobs, can be lost on failover or restart. Make jobs idempotent and retryable.durable: trueis still Redis — the same protocol and the same REDIS_* variables — on a SEPARATE instance whose data is retained across a failure, at roughly double the tenant's Redis cost; it is never MemoryDB, and one app's flag never changes a neighbour's instance. (wbsp-client-guidelines.md §4) - Compose translation for parallel services is a curated subset: unsupported constructs (profiles, custom networks, extends, build, depends_on ordering, healthchecks) are reported as TODO, never silently dropped — read the
upoutput.${VAR}credentials are read from your shell atuptime and applied in memory; export them first. Configure Forgejo via FORGEJO__* env with INSTALL_LOCK=true, not a read-only mounted app.ini. (parallel-services-guide.md) - Durable Redis still has a loss window on every provider: a replica-based instance acknowledges a write before its replica holds it, so a failover can lose the last unreplicated moments, and an append-only file can lose up to a second. Anything that cannot tolerate that window belongs in PostgreSQL or MemoryDB. A durable instance refuses writes when full rather than evicting, and changing the
durableflag between deploys attaches you to the OTHER instance with fresh credentials — data is not migrated. (wbsp-client-guidelines.md §4) - In a monorepo the project-wide build-pipeline artifacts move to the repository ROOT —
playwright.config.tsand the/e2esuite — while.env.<destination>files may sit at the root or beside a source tree, and pipeline-detected scripts (demo-data seed, production init) live under a source tree'sscripts/orsrc/. Declaringlayout: { kind: monorepo, source-roots: [backend, frontend] }in the rootwbsp.yamlis the explicit override that outranks everything else. (wbsp-client-guidelines.md §1) - Multi-component (
components:) parses and validates but only the single unnamed top-level component is wired into the deploy path; per-componentautoscaleis likewise not applied. Treat multi-component as forward-looking — acomponents:map may silently deploy less than you expect. (wbsp-yaml-reference.md) - Parallel services run ONLY beside an
aws(EKS) app in the same cluster; an on-demand (Lambda) app cannot join one. Injection of _HOST/_PORT happens at deploy time, so bring the environment up and then REDEPLOY the app. Apostgrescompose service defaults to a managed RDS database (no pod runs);downnever deletes data — only--purgedoes, and it snapshots first unless--no-snapshot. (parallel-services-guide.md) - Rust is evaluated whenever a framework or component language is genuinely being chosen (low memory, small image, small attack surface) and recommended where the project's shape suits it — but never adopted on your own judgement, and never a reason to raise the framework question when there was no reason to. (wbsp-client-guidelines.md §2)
- Single-tenant by preference: multi-tenant is acceptable only when the user specifically asks for it, never chosen on your own. Users sign in once and identity is shared across their apps (SSO); an app must not present an isolated login that re-prompts a user with a platform session. Development reuses the wbsp-website login via the
wbspauth tenant. (single-tenant-guidelines.md) - The pipeline asks you tenancy, repo layout, parallel services, topology and (only with a concrete reason) web framework at Set project direction, and records them in DECISIONS.md and wbsp-status.md; it never guesses the layout from directories and never overwrites a decision you made without asking. /target and monorepo trees never mix — if /target exists the project IS single-app. (wbsp-client-guidelines.md §1)
- The pre-deployment documents (getting-started.md, testing.md with an enumerated test appendix, useful-commands.md for app developers only, api-reference.md + openapi.yaml if there is an API) must exist and be current before any milestone, written in layman's terms for people with no access to the platform. (wbsp-client-guidelines.md §3)
- Topology
split(separately built images) is cloud-only, as a direct deployment arranged with the platform operator. Demo and sandbox instances never bundle parallel services and an appliance image never bakes one in — only a cloud deployment runs them in full, so a feature needing one is switched off with a note saying it exists in the full product. The full rule is on the ready-for-sandbox-checklist gate page, which the sandbox phase binds. (wbsp-client-guidelines.md §1) - Warn the human user and obtain their explicit permission before declaring
redis.durable: trueormemorydb.enabled: true— say what each means and costs. The build pipeline's direction questions are where this happens; a manifest declaring either without the matching user-decided line in DECISIONS.md fails vetting, and a deploy that creates either instance prints a cost notice. (wbsp-client-guidelines.md §4) redis.durable: truewithoutredis.enabled: trueis a validation error. Changing the flag between deploys attaches your app to the OTHER instance with fresh credentials and the data is NOT migrated. Durable also has a loss window on every provider — a replica acknowledges before it holds the write, an append-only file can lose up to a second — so anything that cannot tolerate that belongs in PostgreSQL or MemoryDB, not Redis. (wbsp-yaml-reference.md)wbsp whoami --allcompares the signed-in account with the repository owner and warns when they differ — the usual cause of a baffling 403 on git push or wbsp deploy. It also warns when thevariant:in wbsp.yaml has drifted from what the remote resolves to. Warnings never change the exit code. (application-creator-guide.md)
implement
- A callback that arrives with its pending sign-in state missing must RESTART the sign-in — once — carrying a short-lived marker (a cookie is enough), so that a second miss in a row renders a page saying the sign-in could not be completed. The callback is reached by a provider redirect, so an error page there has nothing on it that starts a sign-in and the back button is the only way on. With the restart in place an expired state is no longer a dead end, so do not lengthen the state's lifetime to soften one. (wbsp-client-guidelines.md §6)
- A just-in-time provisioning path writes the display name once and never revisits it: it creates the row on first sight and afterwards looks the user up by subject, so fixing the identity lookup leaves every row written before the fix wrong — invented names, empty strings and nulls all survive it silently. Refresh name and email when they differ from what the provider reports, and never refresh the ROLE that way; the role is your application's decision. (wbsp-client-guidelines.md §6)
- A split front end and API become TWO origins under dev and only under dev, and three things break at once: the browser cannot reach the API (add a dev-only CORS allowance or a dev proxy — never a permissive rule that survives to the cloud); server-side fetches have no origin to be relative to (read an ABSOLUTE API base from the env file); server-side redirects to
/land on the API's origin as a JSON 404 (build every redirect from an explicit front-end base URL). The full rule is on the ready-to-test-checklist gate page, which the refine and test phases bind. (wbsp-client-guidelines.md §2) - An application inviting people through
POST /v1/invitationswith its own client credentials must haveinvitation_return_urlset on the registration (viaPATCH /v1/applications/{id}) — it is never passed per request, and an app without one is refused 409. SendX-Tenant-Id(tenant slug or UUID) on that call even though you are authenticating with client credentials: it is the one endpoint that names no tenant, and aclient_idused by more than one tenant is refused 401invalid_clientas ambiguous rather than guessed at. (hap-api-reference-md) - An application that calls a model uses exactly these names and no others:
AI_PROVIDER(anthropic|openai|ollama|none),AI_API_KEY(one credential for whichever provider is selected, never one per provider),AI_BASE_URL(required forollamaand for any OpenAI-compatible endpoint),AI_MODEL, with optionalAI_MODEL_<ROLE>overrides and theAI_EMBEDDING_*set when embeddings use a different provider. One factory readsAI_PROVIDER; availability means the selected provider's own requirements are met, not that a key is present, so demanding a key makes the keylessollamapath unreachable. (wbsp-client-guidelines.md §5) - Application code under /target (or declared monorepo roots) only; /initial-research stays as research; user-docs/website/index.html, if present, is a single static self-contained page with inline CSS that opens by double-click. (wbsp-client-guidelines.md §1)
- Behind the platform proxy your pod sees its OWN address as the request
Host, so anything built from the request host — a callback, an absolute link, a redirect back to yourself — comes out wrong on every cloud destination and right on your laptop. Pin the public URL from configuration (APP_URL: scheme, host and port, no path, no trailing slash) and build from that, the same discipline as honouring X-Forwarded-Proto for the scheme. (wbsp-client-guidelines.md §6) - Build one plain OIDC Authorization-Code + PKCE client and treat the authorization endpoint as CONFIGURATION read from per-tenant discovery — Hosted Login, SSO broker and embedded modes then differ by one config value, not code. Key users on the stable
sub(email can change), verify iss/aud/exp/nonce against the public issuer, keep the PKCE verifier server-side, refresh failure ⇒ sign out. (hap-client-cookbook-md) - Build the callback as https behind the platform proxy: TLS terminates at the edge and your app receives plain HTTP, so honour X-Forwarded-Proto (Auth.js AUTH_TRUST_HOST=true + pinned AUTH_URL; uvicorn --proxy-headers --forwarded-allow-ips=*) or the http:// callback will not match the registered https:// URI. (wbsp-client-guidelines.md §6)
- Call
userinfowhere a row is about to be WRITTEN, never in the function that resolves the principal — that one runs on every authenticated request, so a call there makes the identity provider's latency your latency and its outage your outage, for identity you already hold. And its failure must be non-fatal: refusing to authenticate someone whose token is valid, over a display name, is worse than a wrong name. (wbsp-client-guidelines.md §6) - Declare
auth.scopesin wbsp.yaml with every scope your client library requests — Auth.js/NextAuth ask foroffline_accessby default. Omittingscopes:meansopenid profile email; asking for an unregistered scope makes the provider refuse the ENTIRE authorize request asinvalid_scopeat the last hop of sign-in, in a redirect nobody reads.wbsp deploy --dry-runprints the set. (wbsp-client-guidelines.md §6) - Embeddings are a separate axis: when the embedding provider differs from the text provider, use AI_EMBEDDING_PROVIDER, AI_EMBEDDING_API_KEY, AI_EMBEDDING_BASE_URL and AI_EMBEDDING_MODEL, each of which falls back to its text-side equivalent when unset. Absent AI configuration must degrade, not crash: with AI_PROVIDER absent or
nonethe model features are off, the interface says so where they would have been, the health endpoint reports "not configured" for absent and "switched off for this deployment" fornone, and an API that needs the model answers a typed 503 rather than a stack trace. (wbsp-client-guidelines.md §5) - Every Next.js route.ts is a thin HTTP adapter: services, repositories, DTOs and domain errors import NOTHING from next/*, are constructor-injected classes instantiated once in a composition root, validate at the edge, throw typed errors mapped to HTTP in one place, and take config as an injected object rather than reading process.env. The acid test: delete app/ and src/modules must still compile under plain node. (nextJS-api-guidelines.md)
- Handle DATABASE_SSL: it is
trueon AWS (RDS) and NOT SET locally — enable TLS in the database client when it is true. Design for statelessness: the container can be replaced at any moment, so nothing persistent lives on its filesystem. (application-creator-guide.md) - Hosted Login gotchas: the
hap_sessioncookie is Secure, and browsers (notably Safari) DROP a Secure cookie over http://localhost, so the session is never stored and authorize loops — the platform now sets Secure only for an https issuer, so watch any plain-http front. Step-up (acr_valuesabove aal1) is not offered on the hosted surface. Run each app on the exact port its redirect_uri was registered with. Omittingconsent=grantedused to loop forever; it now renders a consent screen, but older notes saying consent is mandatory predate the fix. (hap-hosted-login-guidelines-md) - If you validate the audience, it is your OWN client id for that tenant, as a literal — HAP sets
audto the issuing application'sclient_idat signing and no parameter, scope or setting changes it; combine that with the issuer check rather than replacing it. Deleting a registration frees its id for re-registration, and tokens already issued stay internally well-formed until they expire: an app validating locally (signature and claims only) cannot tell an old token from a new one, so treat a deletion as taking effect one access-token lifetime later, or check revocation with HAP for that window. (wbsp-client-guidelines.md §6) - If you validate tokens yourself, verify the
issclaim against{hap-url}/t/{tenant}and check the signature against that issuer's keys — theaudis theclient_id, which is unique only among live applications in one tenant and can be re-used after a deletion. Deleting a registration stops HAP issuing and makes HAP refuse the old tokens, but a locally-validating consumer cannot tell a deleted app's token from a new registration's until it expires; treat a deletion as fully effective only after one access-token lifetime. (hap-api-reference-md) - If your app creates HAP invitations, send
X-Tenant-Id(tenant slug or uuid) on POST /v1/invitations — it is the one endpoint that identifies an application by client id alone, which shared ids make ambiguous; HAP refuses rather than guesses. (wbsp-client-guidelines.md §6) - If your app handles CORS behind a path prefix, the
Access-Control-Allow-Originresponse must name the EXTERNAL origin, not the internal stripped URL. Static assets referenced by absolute path load outside the prefix — use relative paths, a<base href>, or read X-Forwarded-Prefix into the framework's asset prefix. (path-prefixes.md) - In-cluster apps use SPLIT-HORIZON addressing: back-channel calls (discovery, token, JWKS) go over the in-cluster Service because pods cannot reach the cluster's own public load balancer, but
iss/audvalidation and the BROWSER redirect keep the public issuer/URL. Mixing them yields tokens that verify against the wrong issuer or redirects to an unreachable host. (hap-client-cookbook-md) - Integrate HAP through a replaceable adaptor: standard OIDC discovery + code/PKCE via a maintained library with a provider entry, all provider specifics in config/secrets, and sign-in/out, token validation and session handling confined to one auth module behind a small stable interface. If HAP's guidelines cannot be implemented, tell the user the specific problem and document the alternative — never diverge silently. (wbsp-client-guidelines.md §6)
- Keep every Redis key inside REDIS_NAMESPACE: the per-app ACL denies cross-namespace and admin commands, and on
awsElastiCache and MemoryDB additionally disallow CONFIG, SAVE, DEBUG and the cluster-management commands. Code that works against a laptop Redis is therefore refused in the cloud and nowhere else. Client code is written against the Redis command reference (or, for MemoryDB, the Amazon MemoryDB documentation) — those two are the supported APIs. (wbsp-client-guidelines.md §4) - Keep the log message template stable and put IDs, counts and durations in attributes — interpolating them creates near-infinite unique messages and destroys anomaly grouping. Never pretty-print or colourise in production (no pino-pretty), never mix value types for one attribute key, always UTC, and keep a multi-line stack trace inside ONE JSON record. (logging-client-guidelines.md)
- Label every docker-compose service with
wbsp.tenant,wbsp.app(from wbsp.yaml) andwbsp.managed-by=wbsp-platform, orwbsp list --provider composecannot tell your stack from any other and it will not appear. Derive host-published ports from the project number (30NNN / 54NNN / 63NNN); container-internal ports stay conventional. (wbsp-client-guidelines.md §2) - Logs are one-line JSON on stdout/stderr — never files on disk — with at least
severity_textandbody, plusservice.name,service.version,deployment.environment; UTC RFC 3339 timestamps; trace_id/span_id in request context; never secrets, tokens or unnecessary PII. Keep the message stable and put variables in attributes, or anomaly detection degrades for every tenant sharing the baseline. (wbsp-client-guidelines.md §7) - Manifest naming and shape rules are enforced at parse time:
namemust be DNS-safe ([a-z0-9][a-z0-9-]*),variantmust be a canonical 8-4-4-4-12 UUID,path_prefixmust start with/,auth.callbackPathmust start with/, anddatabase.extensionsnames must match[a-z][a-z0-9_-]*.non_variant: trueexists only for platform-internal (non-catalog) apps and is mutually exclusive withvariant— an ordinary application never sets it. (wbsp-yaml-reference.md) - On a loopback address, prepend
p<port>.as a LEADING segment to the name of EVERY cookie in the sign-in round trip — session, state, PKCE verifier, CSRF — and keep the framework's documented names on a deployed host. Never APPEND the port: the auth library cannot tell an appended port from a chunk index. Several projects on one machine share a single localhost cookie jar, because a browser scopes a cookie to the host and not the port, so whichever signed in last owns the name. The full rule is on the ready-to-test-checklist gate page, which the refine and test phases bind. (wbsp-client-guidelines.md §6) - On a tenant with Hosted Login,
consent=grantedis no longer required at/oauth2/authorize(omit it and the person is asked), butinteraction_requiredis a dead end: hosted step-up is not offered, so an authorize carryingacr_values=aal2renders a terminal 200 explanation page and never redirects back. Sendprompt=noneonly if you can handle a silentredirect_uri?error=…return — with it, no screen is ever rendered. (hap-api-reference-md) - Point links at the current hosted addresses:
/t/{slug}/hosted/appsis being retired in favour of the parameter-free/t/{slug}, and the exit control moved from/t/{slug}/hosted/exitto/t/{slug}/exit(the old spelling still 307s, with no removal date)./t/{slug}/selectoris not an address on this platform and answers 404 — it was withdrawn on 2026-09-04 and never reached a deployed server, so a link to it never worked. (hap-api-reference-md) - Put AI_PROVIDER and AI_MODEL in each destination's own
env:block, never the top-level one: the top-levelenv:applies to EVERY destination, including the publicdemolane, and a per-destination block only merges over it. Where an installer should choose the provider (an appliance), declare AI_PROVIDER, AI_MODEL and AI_BASE_URL as optional operator values as well — an operator value must be declared undersecrets:even when it is not secret. (wbsp-client-guidelines.md §5) - Redirect an unauthenticated visitor STRAIGHT into the authorization request on any HAP-backed destination — never onto an in-app sign-in page waiting for a button — and exempt the sign-in start route, the OIDC callback, the health endpoint and static assets from that redirect, or you build a loop: the callback arrives unauthenticated by definition. An in-app sign-in page is only the fallback for an install with no identity provider. On demo and sandbox this is the single thing that makes the silent sign-in possible. The full rule is on the ready-for-sandbox-checklist gate page, which the sandbox phase binds. (wbsp-client-guidelines.md §6)
- Removing an administrator designation does NOT touch the user's session — they continue as an ordinary user only once you re-read the claim; suspension or deletion cuts sessions immediately where revoke-on-suspend is enabled. Do not ask for an admin API key so your app can query the administrators API; the claims exist so you never have to. (hap-administrator-claims-md)
- Single logout: clear the LOCAL app session AND redirect to the end-session endpoint with a registered
post_logout_redirect_urito revoke the sharedhap_session— otherwise the next sign-in silently re-authenticates from the surviving platform session. Propagate suspension two ways: refresh failure ⇒ sign out (pull), and identity-lifecycle webhooks verified viaX-HAP-Signature: t=<unix>,v1=<hmac-sha256(secret, t.body)>(push). (hap-hosted-login-guidelines-md) - Switch on the destination's TYPE, never its NAME, and default to cloud. An application that matches on the name meets a name it has never heard of (
twist), matches nothing, and falls back to its LOCAL behaviour on a real deployment — that is how a login bypass or a relaxed check reaches production. Read the placement type and treat an absent or unrecognised marker as a cloud placement, so the unknown case fails safe rather than permissive. (wbsp-client-guidelines.md §6) - Test-driven development is mandatory wherever a silent defect is costly — money, identity, authorization, data integrity; auth in particular is developed test-first. Integration tests run against real Postgres/Redis (platform stores or ephemeral containers), never in-memory substitutes. Where TDD does not fit, notify the user, explain why, and document the substitute — never skip quietly. (wbsp-client-guidelines.md §8)
- The ID token and
userinfoalways carry bothis_app_adminandis_tenant_admin(present even when false, and needing no scope), and HAP asserts no implication between them: a tenant admin who was not explicitly designated for this application getsis_app_admin: false, so OR the two yourself if either should count. Both are forced tofalsewhile the identity is notactive. An ID token is minted only at sign-in — a refresh does not produce one — so readuserinfowith the access token to observe a promotion or revocation without a fresh sign-in. (hap-api-reference-md) - The
redirect_uriis sent twice — once on authorize, again on the token exchange — and the provider compares them as exact strings. A client library handed the incoming callback URL (openid-client'sauthorizationCodeGrant) DERIVES the exchange's value from that URL and never reads your configuration; inside a Next.js standalone container the request URL has been observed carrying the server's own bind address (http://0.0.0.0:8080/…), so every sign-in is refused withinvalid_grant. Rebuild the URL from the pinned configuration and take only the query string from the request. Nothing you test locally reaches that code — the dev bypass stands in for it and a readiness endpoint reporting the configured callback stays correct throughout. (wbsp-client-guidelines.md §6) - The
redirect_uriyou send to/oauth2/tokenmust be byte-identical to the one sent to/oauth2/authorize, including the port — the two legs describe the same listener. The loopback exception ignores only the port: scheme, host literal, path and query must still match the registered URI exactly, so a look-alike host such as127.0.0.1.attacker.example, a non-loopback host, or a port like:0or:99999is rejected withinvalid_requestand no redirect at all. (hap-api-reference-md) - The access token your API verifies carries
sub,aud,iss,expandscopeand NO profile claims:nameandemailare not in it, on any destination, for any user, however wide the scope you requested. They are in the ID TOKEN, which only the tier that ran the code exchange holds, and at{AUTH_ISSUER}/oauth2/userinfo, which takes the access token as a bearer. An API with no ID token callsuserinfo; five projects readnameoff the access token instead, gotundefined, and one of them put an invented display name on a live site. (wbsp-client-guidelines.md §6) - The platform does NOT rewrite the Location header in responses (needs a Traefik plugin not yet loaded), on local or AWS. With
path_prefixand the defaultstrip_prefix: true, redirects, absolute links, form actions and asset paths your app emits land OUTSIDE the prefix unless you use relative URLs, read X-Forwarded-Prefix, or disable stripping and use the framework's base path. X-Forwarded-Prefix is only set when stripping is on AND the prefix is not/. (path-prefixes.md) - The scopes your app requests at
/oauth2/authorizemust be a subset of the scopes the registration holds, or the request is refused withinvalid_scope. If your OIDC library asks for a refresh token,offline_accessmust be in the registeredscopesat creation — omit it and sign-in succeeds in testing and then fails the first time the app tries to stay signed in. (hap-registering-an-application-md) - The two administrator claims are INDEPENDENT — a tenant administrator does not automatically get
is_app_admin: true. Decide explicitly what counts as admin (is_app_admin || is_tenant_adminfor most first-party apps). Claim-reading code that walks candidate names and stops at the first match never seesis_tenant_admin, becauseis_app_adminis always present. A suspended identity reports both false. The id-token copy is as old as the session; call userinfo for a live answer. (hap-administrator-claims-md) - Under a
devdestination your application runs as HOST processes started from your IDE; only the data services are containers. Never documentdocker compose upfor the app under dev. Pin the DERIVED port yourself (for project 356:next dev -p 30356, API on 80356) — the platform cannot derive it because it does not start your app — and moving port needs no HAP change: the shared test client accepts standard callback paths on localhost at any port. The full rule is on the ready-to-test-checklist gate page, which the refine and test phases bind. (wbsp-client-guidelines.md §2) POST /v1/tenantswithadmin_emaildesignates a TENANT administrator only — on a fresh installationis_app_adminis false for everyone. And on a local destination every project signs in through one shared test client, sois_app_adminsays nothing about YOUR application there: read both claims, treatis_tenant_adminas the designation where the shared client is in use, and log which claim admitted someone. (wbsp-client-guidelines.md §6)redirect_urisare matched EXACTLY at authorize time — scheme, host, port, path, trailing slash. Registerhome_urlon the application or it appears on everyone's menu as a row that cannot be opened. Public clients (spa/native) use PKCE only;confidential: true, notapplication_type, is what mints a secret, shown once. (hap-integration-guide-md)
local-run
- A
composedeploy WRITES beside your manifest: it generateswbsp.docker-compose.ymlnext towbsp.yamland auto-seedsPOSTGRES_PASSWORD/REDIS_PASSWORDinto that destination's.env.<destination>on first deploy. Expect both to change under you on the first run, and do not HAND-EDIT the compose file — the next deploy regenerates it and your edit is gone. It carries only${POSTGRES_PASSWORD}-style references, never values, so whether you commit it is your project's call; the.env.<destination>holding the values is git-ignored and stays that way. (useful-commands.md) - A destination name may appear only once — a duplicate key fails the parse.
command:is tokenised with shell-style quoting and malformed quoting fails validation.wbsp deploy --dry-runshows every declared value's provenance, requiredness, type and minting shape, and the resolved scope set — read it before an install rather than diagnosing from a URL afterwards. (wbsp-yaml-reference.md) - A placeholder for a paid third-party key is acceptable; silent degradation is not. Detect it at STARTUP, report the feature as degraded in the health endpoint or first screen naming the variable, name it in the README with cost and source, and refuse to start in cloud destinations rather than degrade. (wbsp-client-guidelines.md §5)
- Add
.env*to .gitignore AND the un-ignore rule!.env.*.example, otherwise the committed templates are ignored too. Commit a.env.<destination>.exampleper destination (no secrets except the shared test-HAP credentials on dev/standalone) and keep a git-ignored.env.<destination>fully populated for local destinations so the app runs with no manual edits. (wbsp-client-guidelines.md §5) - Auth settings are per tenant and a fresh HAP has none: create a tenant first, then read
…/t/<slug>/.well-known/openid-configuration— that discovery document is the single check that the platform is fully working, and the URL your OIDC client points at. (hap-getting-started-md) - Consume the destination-neutral identity names — AUTH_ISSUER (always tenant-scoped,
/t/{tenant}; the bare host 404s), AUTH_CLIENT_ID, AUTH_CLIENT_SECRET, APP_URL (scheme+host+port, no path, no trailing slash), AUTH_SECRET (generated), ENCRYPTION_KEY (generated,stable: true) — and declare provenance invalue_specs. An optional value that is not supplied is entirely ABSENT, never present-and-empty; test both states. A partially suppliedgroupfails the deploy. (wbsp-client-guidelines.md §5) - Declare
projectNo: <0..999>at the top level ofwbsp.yaml(or let the platform parse the leading numeric prefix of the app directory's name) and the local-machine types host-publish app/Postgres/Redis on the derived<two-digits>NNNports automatically — no hand-written docker-compose is needed. A derived value above 65535 folds deterministically into[49152, 65535], and a host-port clash is reported by the deploy's pre-flight. (wbsp-client-guidelines.md §2) - Declare the OIDC callback path your framework serves under
auth: { callbackPath: … }— destinations register it EXACTLY as your redirect URI and providers match it as an exact string. Use a standard path (/api/auth/callback/hap for Auth.js/NextAuth, /api/auth/callback hand-rolled, /login/oauth2/code/hap Spring, /signin-oidc ASP.NET); a version-nested path like /api/v1/auth/callback is not registered anywhere and fails withredirect_uri does not match. (wbsp-client-guidelines.md §5) - Enable managed stores only via the top-level
database.enabled/redis.enabled/s3.enabledblocks — the oldresources: { database, redis, s3 }shape has been REMOVED.s3is not supported on the compose provider (fails closed).database.shared_withrequires the owning app to be deployed first and shares one schema. (application-creator-guide.md) - For a self-hosted platform,
WBSP_HAP_BASE_URLis the provider's BASE with NO/t/<tenant>on the end — the exact opposite ofAUTH_ISSUER, which is always tenant-scoped and 404s without the tenant path.WBSP_API_URLoverrides the API endpoint (defaulthttps://api.wbsp.ai) and is required for auniversalpublish;WBSP_CLI_CLIENT_IDis only for a provider lacking the built-in CLI client. (useful-commands.md) - Local-machine limits: HTTP only on host ports (no TLS/WAF edge behaviour), no
s3on the compose provider (fails closed), no Kubernetes features.composeadvertises the app at<app>.<projectNo>.wbsp.local— resolve it with a wildcard DNS provider or the /etc/hosts line the deploy prints. The sharedwbsp-sharedDocker network is operator-managed: create it once, the provider never creates or destroys it.wbsp stopkeeps the database;wbsp removeDELETES it. (useful-commands.md) - On the local-machine types (
compose,dev,standalone) thesource:anddockerfile:fields are IGNORED — the platform uses theimage:tag directly and you must have built it yourself withdocker build. Only anawsdeploy builds from source, so a local run can silently exercise a stale image while the cloud gets your new code. (wbsp-yaml-reference.md) - Personal access tokens and WBSP_REGISTRY_TOKEN are retired (feature 075) — setting WBSP_REGISTRY_TOKEN is a hard error. CI supplies ONE secret, WBSP_API_TOKEN, minted per job by a service account at HAP; it authenticates the deploy API but is NOT admitted at the registry gateway today, so a pipeline that builds and pushes fails at the push with 401 — deploy an already-published image with
--imageinstead. (useful-commands.md) - Real OS environment variables always override
.env.<destination>— precedence is operator env first, then the single destination file. Today secret values come from your .env as a STOPGAP; a future secrets manager resolves the samesecrets:references unchanged, which is why secrets are always declared by reference, never inline. (wbsp-client-guidelines.md §5) - Stored secret values are WRITE-ONLY — no command, log or page ever returns one. Before swapping an installation's owner mode, list what is stored and find each secret's real source, because removing the installation discards everything and a value with no source has to be reissued. From v0.2.38 a deploy is REFUSED when the owner cannot be verified (no or expired session) unless
--allow-unverified-owner. (configuring-an-installation.md) - The
.env.<destination>file is where values LIVE, not what SELECTS them: only names declared inenv:orsecrets:are sent to the container. A key present in the file and declared nowhere is read, dropped and never mentioned, so the container starts without it and the failure arrives later as behaviour rather than an error. On the local destinations the file effectively is the interface, which is why this only bites on a cloud destination. (wbsp-client-guidelines.md §5) - The
devdeploy writes a.envbeside the app with discrete DATABASE_/REDIS_ on localhost at derived ports; source it and run the app from your IDE.wbsp db runinjects the same discrete contract plus an EXTRA DATABASE_URL as a convenience for tools — pods never get one, so do not build application code around it. Over the aws tunnel the cert CN is the RDS host, so use sslmode=require, never CA verification. (useful-commands.md) - The pipeline's status vocabulary includes
dev(a dev destination is up; you run the app) beside pending/deploying/running/failed/removing/removed. Lifecycle commands (stop, status, logs, remove) scope by--enclave, auto-detected from deployment state and needed only to disambiguate same-name deployments —--destinationis a deploy-time concept. (application-creator-guide.md) - The local destinations point at the shared test HAP server (issuer https://hap.wbsp.ai/t/wbsp, shared test client hap_lIT7x7SZ30e898mr); its credentials are safe to commit in the local .example templates only. Cloud destinations get placeholders in the committed template and the operator supplies the real secret in the git-ignored file. (wbsp-yaml-reference.md)
- The top-level
env:map applies to EVERY destination,demoandsandboxincluded — a per-destination block is merged over it, never instead of it. Put anything placement-specific (an AI provider, a model id, a banner, an internal endpoint) inside that destination's ownenv:block, or it reaches the public demo lane too. (wbsp-client-guidelines.md §5) - When running HAP locally: tenant create/PATCH take NO X-Tenant-Id but application calls REQUIRE it, and its value is the tenant UUID from the create response, not the slug (
404 unknown tenantotherwise). With HAP_EMAIL_MODE=console the login code is printed indocker logs hap, never emailed. Mount the keystore volume or set HAP_KEY_PROVIDER=db, or signing keys regenerate on restart and every token becomes invalid. (hap-local-login-guidelines-md) - With
WBSP_API_TOKENset,wbsp whoamireports THAT token's identity — the service account's — not your cached login, because the token is what every call presents;environment.tokensays which is in play, and bare--useranswers from the local cache with no network call. Automate against--jsononly: its keys are a documented contract while the labelled output may change between releases, and unavailable fields arenullrather than omitted. (useful-commands.md) - You sign in TO A TENANT.
wbsp loginwith no flag signs in to tenantwbsp(website accounts);--tenantnames one;--destinationreads the tenant a destination declares. They are NOT interchangeable — a destination calledawsmay declare tenantphilcal. Being signed in is not being allowed: the tenant's own administrator grants the deployer role in HAP's screens. (useful-commands.md) mode: demomeans two unrelated things: onawsit is reserved to thedemo/sandboxdestinations and no other aws destination may claim it, while on acomposedestinationmode: demois the production-parity shape and its DEFAULT, with nothing to do with sample data. Do not read a compose destination'smode: demoas a demo lane. (wbsp-yaml-reference.md)wbsp config setis a CREATE path: the first write to an installation that does not exist yet decides its owner mode PERMANENTLY (operator-managed), andwbsp deploy --destinationis refused for it from then on. Modes never change;config unsetempties but does not release it, andwbsp removetakes the application away and leaves the record behind. Onlywbsp config remove-installationends it. The warning is printed from v0.2.37; earlier CLIs write silently. (configuring-an-installation.md)
refine
- Choosing an application from the selector is a POST that writes an entitlement and is re-checked server-side against the person's real list; a forged or stale id is refused. Reaching an app THROUGH A GROUP records nothing — clicking a list item is navigation, not a decision to entitle someone. Audit rows distinguish
sign_infromselectorregistrations. (hap-application-selector-md) - Give every application a
home_urlor the selector shows it as a row that cannot be opened (launchable: falsein /v1/me/applications — do not forward to a non-launchable app).https://<hap>/t/<slug>is the canonical selector;/t/<slug>/hosted/appsis being retired;/menu/<slug>and/t/<slug>/menuredirect to it;/t/<slug>/selectorNEVER existed and 404s. (hap-application-selector-md) - Leaving through
Exitmust never return to your own application. Ending the platform session with a post-logout return to your own root is RP-initiated logout exactly as OIDC describes it and is still wrong here: the browser arrives unauthenticated, your application correctly starts a sign-in, and leaving has delivered the person to the way back in. It presents as a sign-in fault (typicallyinvalid-stateon the next attempt), so it is diagnosed in the wrong place. No hop of the Exit chain may name your own host — including inside an encodedpost_logout_redirect_uri. (wbsp-client-guidelines.md §6) - The exit page takes NO parameters — an appended
?return_to=is ignored so it can never become an open redirect. A person with at most one application and no admin reach lands on the signed-out page and their session ends (exit means exit); use the end-session endpoint with a registered post_logout_redirect_uri if you need the browser returned to an address of your choosing. (hap-application-selector-md) - The user menu on a top nav or left bar shows who is signed in (name, else email, on the bar itself), a profile item only where there is something to show, and ONE way out labelled
Exit— not 'Sign out' — that first clears your own session, then forwards to{AUTH_ISSUER}/exitbuilt from config, never hard-coded. Do not also offer a sign-out item. Deprecated but still redirecting: /menu/{tenant}, /t/{tenant}/menu, /t/{tenant}/hosted/apps, /t/{tenant}/hosted/exit. (wbsp-client-guidelines.md §6) show_in_menu: falseHIDES an application from the selector but never denies access; clients whose redirect URIs are all loopback are registered off the menu by default. Open applications are on EVERYONE's menu since 2026-09-11 (discoverability, not access). Signing in to an open app now records a grant so that later flipping it torestricteddoes not lock everyone out — but only sign-ins since the feature shipped; grant dormant users explicitly before tightening. (hap-application-selector-md){hap-url}/t/{tenant}/selectorNEVER existed — one earlier version of the guidelines named it, HAP has never served it, so an application linking there has a brokenExitbutton today rather than a deprecated one. Fix it now to{hap-url}/t/{tenant}/exit. (The truly deprecated spellings still redirect; this one does not.) (wbsp-client-guidelines.md §6)
test
- Check the sign-in round trip by what the application SENDS, not what it HOLDS. A health or readiness endpoint that reports your callback, issuer or exit address reports what you hold, and will say everything is right while the application sends something else — neither the redirect_uri derived from the request nor an Exit that returns to your own application is visible from inside the pod. Observe from outside: follow the running application's redirects from a shell (
curl -s -o /dev/null -L -D - https://<domain>/<exit-route> | grep -i '^location:') or read the address bar. A test that greps your source for the logout call, or mints a token locally, passes for the whole life of either defect. (wbsp-client-guidelines.md §6) - HAP's own suite (318 tests) runs against real PostgreSQL and Redis in throwaway containers, isolates every test with its own tenant and users, and deliberately exercises the refusal paths — replayed codes, tampered tokens, wrong tenant. Hold your auth surface to the same standard: real stores, isolated fixtures, and tests that prove the system says no. (hap-testing-md)
- Record what is covered in user-docs/testing.md — the layman's summary plus the enumerated test appendix — and, if the log is noisy by design, say there which lines are expected so the next person can tell a known warning from a new failure. (wbsp-client-guidelines.md §8)
package
- A
generatedvalue withstable: trueis minted once and never re-minted, and a destination that LOSES a stable value refuses to start the app rather than destroy what the value protected — so encrypt persistent data only with a stable value such asENCRYPTION_KEY, never with a re-mintable one. The minting shape defaults to 32 bytes, base64 (hex/base64urlallowed), and is declared so every destination mints interchangeable values. (wbsp-yaml-reference.md) - A
universalpublish refuses withdeclares neither source: nor dockerfile:if the manifest only names animage:— the destination builds, it does not re-tag — and it needsWBSP_API_URLset plus awbsp loginsession.--destinationmay be omitted whenuniversalis the only destination declared. (useful-commands.md) - Before building anything,
wbsp deployasks BOTH gates and you should see both lines:Authorized to publish …(deploy/publish, also admits destination-tenant holders) ANDAuthorized to push …(variant owner/collaborator/operator only).not allowed to push … 403means you are signed in but neither own nor collaborate on the variant;the registry has no container repositorymeans the catalog knows no variant for your git remoteorigin. (credential-less-deploy.md) - Package the standalone/preview image to listen on whatever external port the user maps (derive from PORT) or build links from the incoming request host/port, and never bake the sign-in bypass variable into it. (wbsp-client-guidelines.md §3)
- The FIRST push to a new variant repository can return
403 … do not have access to this container repository: a known gateway bug, not a permissions problem — the repository is created by the request that is refused.wbsp deployretries once automatically; a hand-run docker push needs one manual retry. (credential-less-deploy.md) - The first
universalpublish creates awbsp-multiarchbuildx builder because stock Docker Desktop cannot build multi-platform images — do not delete it between publishes.WBSP_BUILD_PLATFORMS=linux/amd64narrows the set but the result will NOT run on an appliance.--publish-only,--universaland--sandboxon deploy are all retired in favour of--destination. (credential-less-deploy.md) - Two platform behaviours decide whether a keyless AI deploy is accepted at all: declare
AI_API_KEYwith avalue_specsentry ofrequired: false(a declared secret with no spec is required, and a required secret with no value fails the deploy), and never putAI_API_KEYin agroupwith the otherAI_*values (a group is all-or-nothing, soAI_PROVIDERplusAI_BASE_URLwith no key reads as half-supplied and is refused). Where an installer should choose the provider,AI_PROVIDER/AI_MODEL/AI_BASE_URLmust be declared undersecrets:too — an operator-supplied value lives there even when it is not secret. (wbsp-client-guidelines.md §5) - Your image carries an
ai.wbsp.manifestlabel with the effective configuration — release, port, extensions, topology, secret NAMES — readable by anyone who can pull it. Your non-secretenv:values travel in the CLEAR in that label, so anything merely sensitive (internal hostnames, account ids, partner endpoints) belongs insecrets:, where only the name is published. The release command runs inside your own image: if it runs psql, ship psql. (credential-less-deploy.md) universal: {}is required of you but enforced by nothing: deploying toprod,twistor any other destination never checks for it and will not refuse for want of it. Onlywbsp deploy --destination universalrefuses without it — unlike an undeclaredsandbox, which borrows thedemo:block,universalhas no fallback block to borrow — so the entire cost of omitting it is that there is no appliance image. The build pipeline's Universal build step addsuniversal: {}and commits it if it is missing. (wbsp-client-guidelines.md §5)universalpublishes an appliance-runnable image and deploys NOTHING — no lifecycle commands apply. It accepts arelease:(the appliance runs it) and stamps this destination's effective configuration onto the image because an appliance cannot read wbsp.yaml. Publishing requireswbsp login;--backdooris refused. (wbsp-yaml-reference.md)value_specsrules that decide whether an install is accepted: a secret with NO spec meanssource: operator, required: true;required: falsemeans an absent value is omitted entirely (never an empty string) with a warning; every member of agroupmust itself berequired: false;kindis mandatory whensource: destination; andoperatorspecs must be declared insecrets:(top level, destination or component) whilegenerated/destinationspecs may stand alone. Each refusal names the offending entry. (wbsp-yaml-reference.md)wbsp loginregisterswbspas your git AND docker credential helper, sogit push,docker pushandwbsp deployall work off one login — but only ifwbspis on your PATH (git and docker invoke it by name; a checked-out bin/wbsp needs bin/ on PATH). The gateway addressregistry.wbsp.ai/<owner>/<repo>/appis git-keyed; the underlying ECR repositorywbsp-v-<short-uuid>-appis variant-keyed; you never address the ECR id. (credential-less-deploy.md)
sandbox
- A broker retains the user's multi-use
login_tokenSERVER-SIDE only (never in a URL, cookie-readable store or response body), brokers only for an ALLOWLISTED client_id + exact redirect_uri (anything else is an open redirector), passesstatethrough untouched, and assertsconsent=grantedonly for first-party apps it controls. (hap-sso-broker-guidelines-md) - A demo instance is an ephemeral EKS pod carrying its own bundled Postgres (and Redis) inside the image — it is NOT a Lambda function and has no platform RDS — so there is no external database for
wbsp db connect/run/proxyto reach, and every byte of its data is discarded at reap. The environment list at the top of useful-commands.md still labelsaws.demoas Lambda; theaws.demosection itself is the one to believe. (useful-commands.md) - A service-account token carries the ACCOUNT's rights, not a person's: its tenant is the one in its issuer and its verbs are the scopes its administrator granted, re-checked on every call, so a staffing change never silently breaks a pipeline. But because a demo publish follows PROJECT access (owner or developer), the service account itself — not the person who created it — must hold that access on the project, or the pipeline's demo publish is refused. (credential-less-deploy.md)
- A top-level
secrets:block binds EVERY destination, demo and sandbox included, so declaring OIDC/auth client variables there — not only underdemo:— fails the demo publish closed, waiting for values the platform injects anyway (the refusal names all of them at once). If an appliance genuinely needs the declaration, keep it and setrequired: falsein itsvalue_specsentry rather than deleting it: a secret with no spec meanssource: operator, required: true, so an installer would prompt a person for a value the destination was going to supply. (application-creator-guide.md) - A top-level
secrets:entry binds every destination,demoandsandboxincluded, and a per-destination block adds to it rather than scoping it — so a demo or sandbox publish refuses on any declared secret with no value, and there is no.env.demoon the launch path to supply one. If an appliance genuinely needs the declaration, keep it and setrequired: falsein itsvalue_specsentry rather than deleting it: that keepssourceandkind(the fields the install path reads) while letting the injecting lanes proceed with a warning. (wbsp-yaml-reference.md) - Declare every PostgreSQL extension your schema uses under
database.extensions(e.g.[vector]). Your app role is not a superuser on any destination, so pg_dump'sCREATE EXTENSION IF NOT EXISTSin the sample data is refused, the seed aborts, and the demo instance never becomes ready; migrations cannot rescue it because they run as the same role. Keep your own CREATE EXTENSION IF NOT EXISTS — once it exists it is a no-op. (wbsp-yaml-reference.md) - Declaring
extensions: [postgis](or postgis_topology, postgis_raster, postgis_tiger_geocoder, address_standardizer) switchesstandalone,demoandsandboxto a SPATIAL VARIANT of the bundled Postgres image — a separate base carrying GDAL, GEOS and PROJ, so the first build on a machine takes a few minutes longer. Extensions are created with CASCADE, so ordering does not matter and you declare only what your schema uses, not what those extensions themselves require. (wbsp-yaml-reference.md) - Do not let a production guard reject the demo lane. A demo or sandbox instance runs its Postgres and Redis beside the app in one container, so the launcher injects DATABASE_HOST, REDIS_HOST and MEMORYDB_HOST as
localhostwhile your image usually still declares NODE_ENV=production — the one lane that is legitimately both at once. A check written as "in production the database host must not be localhost" throws during environment validation before the app opens a socket, so the pod never becomes ready and the launch fails with nothing visible from outside. Exempt the demo lane on WBSP_SAMPLE_MODE=true, which the platform always sets — never on your own preview or standalone flag, which it does not. (application-creator-guide.md) - In the cookie-less direct-integration model HAP holds no browser session, so
prompt=nonereturnslogin_requiredunless the user signed in via Hosted Login. An SSO client generates its own PKCE pair and NEVER sends the verifier to the broker — only the challenge — which is what makes the brokered code usable only by you. Cross-tenant SSO is impossible. (hap-sso-client-guidelines-md) - On demo and sandbox the sign-in is a CONFIDENTIAL client: the SERVER discovers HAP's endpoints and exchanges the authorization code using the injected AUTH_HAP_SECRET, while the browser performs only the two page navigations. The injected secret is the tell — a browser-side public client has nowhere to keep one, and fails with an otherwise unrecognisable symptom: a demo that asks the visitor nothing, looks entirely correct and never arrives. The full rule is on the ready-for-sandbox-checklist gate page, which the sandbox phase binds. (wbsp-client-guidelines.md §6)
- Sandbox items in the pipeline self-mark by writing
.wbsp/sandbox-demo.done; an API-only or CLI-only project has nothing to demo in a browser and completes without deploying. A visitor already signed into wbsp.ai is signed into the demo automatically via shared-HAP SSO — there is no second login and no per-app client. (application-creator-guide.md) - The sandbox is a routine-developer action through the credential-less flow: never pass
--backdoor(operator-only, live demo) and never supply AWS keys.wbsp deploy --destination demo --sandboxis retired — the sandbox is a destination (wbsp deploy --destination sandbox) — while--sandboxsurvives onwbsp demo launch/remove/status/logs/audit/promote, which act on an already-published sandbox. Demo publishing follows PROJECT access (owner/developer), not tenant grants. (credential-less-deploy.md) - When a demo or sandbox instance never becomes ready, read it directly:
wbsp demo logs --sandboxshows the instance's output tagged[app](your application) and[platform](database init, seed, app start), streams with--follow, and stays readable for 7 DAYS after the instance is reaped — so a failed start is diagnosable immediately instead of being inferred from a pod that simply never came up.wbsp demo audit --sandboxshows who tried the variant, when, for how long and how it ended (--csvfor a file). Reading either is open to whoever launched the instance, the variant's owners and collaborators, and operators. (useful-commands.md) release:NEVER runs on demo or sandbox — an instance is one ephemeral pod that starts bundled Postgres, creates extensions, loads sample data and execs the app, with no step in which a migration could run. So an app whose schema exists only in migrations must ship the schema IN its sample data (a full pg_dump, not --data-only), or the seed fails against tables nothing created and the pod never becomes ready.wbsp logsnames the pg_dump flag that would have prevented it. (wbsp-yaml-reference.md)sample_data.pathis REQUIRED for a demo/sandbox placement and points at the directory of.sqlseed files baked into the image;sample_data.timeout_minutes(default 10) is the instance TTL, after which the platform reaps it through a short grace page. The publish bakes whatever is in that directory at build time, so regenerate it withwbsp dump-datawhenever the schema or the sample content changes — nothing warns that a stale dump shipped. (wbsp-yaml-reference.md)wbsp deploy --destination demobuilds and pushes the demo image and STARTS NOTHING; an instance exists only when someone launches one (wbsp demo launchor the website button) at a unique subdomain, and is reaped after the timeout with a grace page. The separate step iswbsp dump-data, which regenerates the seed — whatever sits in the seed directory at publish time is baked into the image, silently. Nobody visits a URL because you deployed. (application-creator-guide.md)wbsp deploy --destination sandboxborrows thedemo:block and .env.demo when nosandbox:block is declared — the only case where a destination name resolves a different block. With neither declared the sandbox publish fails.mode: demois exclusive to the demo/sandbox reserved names on aws.use_declared_porthas NO effect on demo: instances always get PORT=8080. (wbsp-yaml-reference.md)
production
- A
release:failure or timeout (default 600s, max 3600) FAILS the deploy: no Deployment is updated and no route flips, so the previous version keeps serving. Migrations run from the NEW image while the OLD version may still serve, so they must be additive (expand/contract). The image must contain the migration tooling — a standalone Next.js build may omit the prisma CLI. Per-destinationrelease: ""disables it (e.g. a dev destination on a shared DB). (wbsp-yaml-reference.md) - A self-chosen
client_idmust be 8–128 characters of letters, digits, hyphen, period, underscore or tilde with at least one letter or digit, and may not beginhap_. Breaking the charset or length rule is 400client_id_invalid(withrule: charset|length), a reserved prefix is 400client_id_reserved, and an identifier already held by a live application in the tenant — including a case-variant of one — is 409client_id_unavailable. Nothing is created when a request is refused, so retry with a corrected identifier. (hap-registering-an-application-md) - An
awsdestination withmode: on-demandruns as a Lambda function with no Kubernetes namespace, so it CANNOT join a parallel-services environment — that environment reaches its allow-listed apps through network rules matched on the app's own namespace. If your application needs parallel services, deploy it to an ordinaryawsdestination instead. (useful-commands.md) - Cross-app access is declared by the PROVIDER (
ports:with tenant/any/public + allow-list); a consumer cannot self-grant viauses:. Same-tenant is open by default. Enforcement is real on aws (NetworkPolicy) and absent on local types, which still inject the same __HOST/_PORT env. Authenticating the caller is your application's job. (wbsp-yaml-reference.md) - Declare
health_check: { path: /healthz }if your app legitimately answers 404 at/— otherwisewbsp list --healthandinfoprobe the route root and report a perfectly healthy application as unhealthy. (wbsp-yaml-reference.md) - Deleting a HAP client registration frees its id for re-registration, and tokens already issued to the deleted registration stay internally well-formed until they expire. HAP refuses them for their full remaining life, but an application that validates tokens LOCALLY — signature and claims only, without asking HAP — cannot tell an old one from a new one, because id, tenant and issuer are identical. If you validate locally, treat a deletion as taking effect one access-token lifetime later, or check revocation with HAP for that window. (wbsp-client-guidelines.md §6)
- Deploying from a published image (
wbsp deploy --image) needs a PTA from the website (or WBSP_PTA); a revoked PTA fails the NEXT image access, including a node re-pulling later.--domainis optional (no public route without it). Platform-supplied names (DATABASE_, REDIS_, S3_*, PORT) WIN over a stored value of the same name. The platform refuses to start a half-configured installation, naming each missing required value. (configuring-an-installation.md) - Deploying to the cloud and publishing for appliances are two commands and the ORDER matters: a cloud deploy publishes linux/amd64 only and moves the image tag off any appliance index, so run
--destination universalLAST. A cloud deploy of an app that declaresuniversal:prints a reminder. (credential-less-deploy.md) - Deploying your code registers NO OAuth client, and you probably cannot register one yourself: only a tenant administrator (console) or a holder of an admin key bound to that tenant (API) can. Registration takes
applications:write;identities:writeis still accepted for older keys, and a key carrying it can make anyone a tenant admin, so treat such a key as a tenant administrator. (hap-registering-an-application-md) - Editing
redirect_urison an existing registration revokes nothing, but ORDER matters: register the new URI BEFORE your application starts sending it or every sign-in fails at authorize. During a host or path change register both, deploy, then remove the old one. Never delete and re-register — that retires the registration for good. (hap-registering-an-application-md) - On a non-reserved destination
type:andtenant:are both REQUIRED, and fortype: awsso arecluster:andenclave:;cluster:is REJECTED on the local-machine types (enclave:is accepted there but purely informational), anddev/standalonetake nomode:at all. (wbsp-yaml-reference.md) - Re-enabling a disabled application restores sign-in but does NOT un-revoke the tokens and sessions the cascade already killed — users simply sign in again. Rotate on a public (PKCE-only) client returns
secret_rotation_not_applicable. Every disable/enable/delete/rotate is audited; secrets are never logged. (hap-application-lifecycle-md) - Rotating a secret with overlap 0 (the default) retires the old secret immediately AND revokes that application's access tokens, refresh sessions and per-app grants — the app is signed out until redeployed; use an overlap window (max 2 hours) for a routine rotation. Disable is a scoped kill switch (the tenant-wide SSO session survives); delete is terminal and the client_id is never reused. The application's OWN credentials cannot rotate or disable it. (hap-application-lifecycle-md)
- Set
home_url(an absolute http(s) URL) on every application you register. Nothing about sign-in breaks without it, so nobody notices: the application simply appears on everyone's application menu and inGET /v1/me/applicationswithlaunchable: falseand cannot be opened.home_urlis administrator-supplied only and is never accepted as a request parameter, so the app cannot supply it at runtime. (hap-registering-an-application-md) - Since 2026-09-13
POST /v1/applicationsacceptsapplications:write, and that is the scope to ask for: it also governs PATCH, disable, enable, rotate-secret and delete, so one narrow key covers the application's whole life.identities:writeis still accepted but additionally lets its holder make any account a tenant administrator and attach an external identity provider, so a key carrying it is tenant-administrator-equivalent. If your registrant also lists applications first, addidentities:read—GET /v1/applicationsis gated on that, not on an applications scope. (hap-registering-an-application-md) - The connection URL
wbsp db urlprints — and theDATABASE_URLthatdb runinjects — carriesuselibpqcompat=truebesidesslmode=require, because modern node-postgres otherwise reads a barerequireasverify-full, which cannot pass over the tunnel. It is a node-postgres option: a driver that forwards an unrecognised query parameter to the server, as postgres.js does, is refused withunrecognized configuration parameter "uselibpqcompat"— naming something you never wrote. Pass--libpq-compat=falseto leave it out; encryption is unaffected. (useful-commands.md) - The platform does not rewrite Location headers on AWS either — the Traefik rewrite-headers plugin is not loaded. Redirects behind a path prefix must carry the prefix themselves. (path-prefixes.md)
- The platform mints no tokens since feature 093:
wbsp token createandwbsp token revokeare gone, along with the platform-signed CI tokens they produced. A pipeline mints WBSP_API_TOKEN itself with the client-credentials grant againsthttps://hap.wbsp.ai/t/<tenant>/oauth2/token; it serves ONE tenant (the one in its issuer) and lives an hour by default, so mint it per job rather than storing one. Revoke by disabling the service account at the provider — effective only within one token lifetime, never instantly. (credential-less-deploy.md) - When your app runs inside the same EKS cluster as HAP, do not make HTTP calls to the public ingress URL — pods cannot hair-pin to their own cluster's public load balancer and the requests time out. Split the two: send discovery, authorize, token, userinfo and admin calls to the in-cluster Service (
HEADLESS_AUTH_AUTH_HOST/HEADLESS_AUTH_AUTH_PORT, injected by auses:dependency) while keepingiss/audvalidation pointed at the public issuer, because tokens are signed with the public issuer baked in. (hap-api-reference-md) - Where a deploy READS a value differs by destination type:
compose/dev/standaloneread your.env.<destination>directly,aws/aws.demo/aws.on-demandread the values the PLATFORM holds for that installation (your deploy records them there from your.envfirst), anduniversalreads nothing at all — it stamps declarations onto the image and the appliance operator supplies values. A refusal saying "the platform holds no value for X" is literal: set it withwbsp config set, or supply it locally and deploy again, and check withwbsp config status --destination <name>. A variable an operator sets on the platform's own servers can never answer for your app. (wbsp-yaml-reference.md) - Whether anyone registers your client for you depends on the deploy path:
wbsp deploy --imageinto an operator-managed installation registers a sign-in client, derives the callback from the domain and records it; a repository deploy to an ordinary cloud destination does NOT — the client must already exist with the exact callback, or the deploy stops on unresolved identity values or sign-in fails at the last redirect. (wbsp-client-guidelines.md §6) PATCH /v1/applications/{id}is a partial update where an omitted field or an explicitnullleaves the stored value alone and""clears it — so an automated redeploy that patches onlyredirect_uriswill not disturb alabel,description,icon_urlorhome_urlan administrator set. The loopback-derived default forshow_in_menuis applied at creation only and is never revisited when redirect URIs change later, so re-assertshow_in_menuexplicitly if a deploy depends on it.labelis capped at 60 characters anddescriptionat 200, andicon_urlmust behttpsor browsers block the image. (hap-api-reference-md)POST /v1/applications/{id}/rotate-secretwithoverlap_secondsabsent or0is the cascading form: it retires the old secret immediately and revokes that application's access tokens and refresh sessions, so the app is signed out until redeployed. Pass a positiveoverlap_seconds(maximum 2 hours) to keep both secrets valid with no revocation when you are rotating a working secret on a schedule. Public clients get 400secret_rotation_not_applicable. (hap-registering-an-application-md)confidential: true, notapplication_type, is what mints a client secret onPOST /v1/applications. Register a server-backed web app withconfidentialomitted or false and noclient_secretcomes back at all; it is a public client and must use PKCE. There is no way to add a secret afterwards —PATCHcannot setconfidential, so the fix is a new registration. (hap-registering-an-application-md)nameandclient_idare PERMANENT on every surface;scopescannot be changed through the API afterwards (the console edit form can);post_logout_redirect_urisis not on the create call — register, then PATCH immediately. The secret is shown once and never retrievable. The loopback any-port exception NEVER applies to a hosted address, so a shared localhost test client cannot work from a real URL — the failure reads as a platform problem. (hap-registering-an-application-md)routes:is required iffaccess: publicon aws and ignored on the local types. Gateways that grant cloud access (registrywithaws.ecr: true) or open a public TCP port (ssh-git) activate ONLY fromenabled: trueunderdestination.<name>.gateways— the top-level flag alone never suffices. The ssh-git host key persists across redeploys; rotate it only via the CLI.irsa_role_arnandcluster_roleare platform-internal. (wbsp-yaml-reference.md)value_specsandauthdeclarations need a CURRENT CLI: an olderwbspbinary silently publishes a manifest WITHOUT them — safe for consumers, who refuse as before, but your declarations go nowhere. Rebuild bin/ after upgrading. On HAP, WIDENINGauth.scopeson an existing installation makes the platform re-register the client (fresh id and secret, re-injected in the same pass) with a warning naming the scope; narrowing changes nothing. (wbsp-yaml-reference.md)wbsp config settreats every value as SECRET unless--plain; an empty value is refused (useunset); a bulk load from a file is all-or-nothing and names the offending line.config statusruns on your machine and cannot type-check secret values — the platform can and will refuse a stored value that does not parse. An image install with no--enclaveruns in its OWN enclave; an unregistered--enclavename refuses at install. (configuring-an-installation.md)wbsp db run/connect/proxy/urlread the app's connection from the DEPLOYED pod's environment, so the app must be deployed before any of them work, and they tunnel to the private RDS through the in-clusterwbsp-db-proxyrelay provisioned with the platform (the commands say so when it is missing). Onlywbsp db proxyever prints the password. A local-machine destination connects straight to the docker-compose database with no relay. (useful-commands.md)
handover
universalhas no lifecycle commands (list, status, logs, stop, remove do not apply); verify a publish by readingimage_refandplatformsfrom its JSON result. After any Go change to the platform,make buildrestamps the version into bin/ — a build made before its release tag reports the PREVIOUS version while carrying newer code, so believe the commit over the version string. (useful-commands.md)
When to read each document
- application-creator-guide.md — End-to-end walkthrough: destinations vs components vs enclaves, dev-mode workflow, the demo's three parts (publish/launch/teardown), demo auth, universal, CLI and REST reference, troubleshooting table.
- configuring-an-installation.md — wbsp config: supplying values that cannot come from a .env, the one-writer mode trap, image deploys with a PTA, the platform's own config page.
- credential-less-deploy.md — Deploying with only your login; what WBSP_API_TOKEN can and cannot do; sandbox publish/promote; universal in depth; the image manifest label; the troubleshooting table for 401/403.
- hap-administrator-claims-md — Definitive contract for is_app_admin / is_tenant_admin.
- hap-application-lifecycle-md — Disable, delete, rotate a secret; the revocation cascade; overlap windows.
- hap-application-selector-md — The selector at /t/, home_url, show_in_menu, self-enrolment, registration-on-sign-in, the exit page, and which addresses are deprecated or never existed.
- hap-client-cookbook-md — Copy-and-adapt OIDC clients in TypeScript, Python, Go and raw HTTP; the client checklist.
- hap-getting-started-md — Plain-language orientation and a 60-second smoke test; the jargon table.
- hap-hosted-login-guidelines-md — Hosted Login: HAP renders the sign-in UI; cross-app SSO via the auth-domain session; the worked two-app example and its gotchas; single logout; revocation.
- hap-integration-guide-md — The OIDC code+PKCE round trip against HAP step by step, including provisioning a tenant/app and machine-to-machine.
- hap-local-login-guidelines-md — Running HAP on your laptop with the plain docker CLI: env vars, the admin key, one-time tenant/app setup.
- hap-registering-an-application-md — Registering a client in a tenant you do not own: who can, what to supply, the scope trap, redirect-URI exactness, the five things projects get wrong.
- hap-sso-broker-guidelines-md — Acting as an SSO broker: retaining login_token server-side, the handoff endpoint, allowlisting, why PKCE makes it safe.
- hap-sso-client-guidelines-md — Acting as an SSO client of a broker app; fallbacks; in-cluster split-horizon.
- hap-testing-md — How HAP is tested; the model for testing your own auth surface.
- logging-client-guidelines.md — The LogWatch contract: field names mapped to the OTel Logs Data Model, per-language libraries, what not to do, when deviation is acceptable.
- nextJS-api-guidelines.md — How to structure Next.js API code so it ports to NestJS: thin route.ts adapters over framework-agnostic services.
- parallel-services-guide.md — Running compose backing services (Forgejo, ClickHouse…) beside an aws app: the environment file, storage policies, isolation, data-safety rules.
- path-prefixes.md — Running under a path prefix: stripping, the Location-header limitation, the three URL strategies, forwarded headers.
- single-tenant-guidelines.md — Tenancy stance and SSO expectations; the how is HAP's.
- useful-commands.md — Day-to-day commands by environment; the three ways to authenticate (login per tenant, WBSP_API_TOKEN, --backdoor); wbsp whoami; wbsp db; parallel services; universal troubleshooting.
- wbsp-client-guidelines.md — Normative index; read the section bound to your phase (§1 locations/demo rules, §2 websites/dev, §3 pre-deployment docs, §4 data stores, §5 env & secrets, §6 auth, §7 logging, §8 testing, §9 boundaries). Its Quick checklist is a gate before every milestone.
- wbsp-yaml-reference.md — THE schema authority for wbsp.yaml: destinations, reserved names, port contract, release semantics, extensions, value_specs/auth, the worked config and the five per-destination .env examples.
This page routes and warns; it does not describe. Corpus at generation: 1003445 bytes. Regenerated only through the platform's documentation reconciliation, behind gates. Never edited by hand in a project: a project retrieves it.