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: true is 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 up output. ${VAR} credentials are read from your shell at up time 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 durable flag 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.ts and the /e2e suite — 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's scripts/ or src/. Declaring layout: { kind: monorepo, source-roots: [backend, frontend] } in the root wbsp.yaml is 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-component autoscale is likewise not applied. Treat multi-component as forward-looking — a components: 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. A postgres compose service defaults to a managed RDS database (no pod runs); down never deletes data — only --purge does, 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 wbsp auth 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: true or memorydb.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: true without redis.enabled: true is 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 --all compares 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 the variant: 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/invitations with its own client credentials must have invitation_return_url set on the registration (via PATCH /v1/applications/{id}) — it is never passed per request, and an app without one is refused 409. Send X-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 a client_id used by more than one tenant is refused 401 invalid_client as 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 for ollama and for any OpenAI-compatible endpoint), AI_MODEL, with optional AI_MODEL_<ROLE> overrides and the AI_EMBEDDING_* set when embeddings use a different provider. One factory reads AI_PROVIDER; availability means the selected provider's own requirements are met, not that a key is present, so demanding a key makes the keyless ollama path 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 userinfo where 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.scopes in wbsp.yaml with every scope your client library requests — Auth.js/NextAuth ask for offline_access by default. Omitting scopes: means openid profile email; asking for an unregistered scope makes the provider refuse the ENTIRE authorize request as invalid_scope at the last hop of sign-in, in a redirect nobody reads. wbsp deploy --dry-run prints 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 none the 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" for none, 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 true on 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_session cookie 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_values above aal1) is not offered on the hosted surface. Run each app on the exact port its redirect_uri was registered with. Omitting consent=granted used 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 aud to the issuing application's client_id at 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 iss claim against {hap-url}/t/{tenant} and check the signature against that issuer's keys — the aud is the client_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-Origin response 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/aud validation 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 aws ElastiCache 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) and wbsp.managed-by=wbsp-platform, or wbsp list --provider compose cannot 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_text and body, plus service.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: name must be DNS-safe ([a-z0-9][a-z0-9-]*), variant must be a canonical 8-4-4-4-12 UUID, path_prefix must start with /, auth.callbackPath must start with /, and database.extensions names must match [a-z][a-z0-9_-]*. non_variant: true exists only for platform-internal (non-catalog) apps and is mutually exclusive with variant — 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=granted is no longer required at /oauth2/authorize (omit it and the person is asked), but interaction_required is a dead end: hosted step-up is not offered, so an authorize carrying acr_values=aal2 renders a terminal 200 explanation page and never redirects back. Send prompt=none only if you can handle a silent redirect_uri?error=… return — with it, no screen is ever rendered. (hap-api-reference-md)
  • Point links at the current hosted addresses: /t/{slug}/hosted/apps is being retired in favour of the parameter-free /t/{slug}, and the exit control moved from /t/{slug}/hosted/exit to /t/{slug}/exit (the old spelling still 307s, with no removal date). /t/{slug}/selector is 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-level env: applies to EVERY destination, including the public demo lane, 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 under secrets: 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_uri to revoke the shared hap_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 via X-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 userinfo always carry both is_app_admin and is_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 gets is_app_admin: false, so OR the two yourself if either should count. Both are forced to false while the identity is not active. An ID token is minted only at sign-in — a refresh does not produce one — so read userinfo with the access token to observe a promotion or revocation without a fresh sign-in. (hap-api-reference-md)
  • The redirect_uri is 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's authorizationCodeGrant) 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 with invalid_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_uri you send to /oauth2/token must 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 as 127.0.0.1.attacker.example, a non-loopback host, or a port like :0 or :99999 is rejected with invalid_request and no redirect at all. (hap-api-reference-md)
  • The access token your API verifies carries sub, aud, iss, exp and scope and NO profile claims: name and email are 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 calls userinfo; five projects read name off the access token instead, got undefined, 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_prefix and the default strip_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/authorize must be a subset of the scopes the registration holds, or the request is refused with invalid_scope. If your OIDC library asks for a refresh token, offline_access must be in the registered scopes at 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_admin for most first-party apps). Claim-reading code that walks candidate names and stops at the first match never sees is_tenant_admin, because is_app_admin is 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 dev destination your application runs as HOST processes started from your IDE; only the data services are containers. Never document docker compose up for 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/tenants with admin_email designates a TENANT administrator only — on a fresh installation is_app_admin is false for everyone. And on a local destination every project signs in through one shared test client, so is_app_admin says nothing about YOUR application there: read both claims, treat is_tenant_admin as the designation where the shared client is in use, and log which claim admitted someone. (wbsp-client-guidelines.md §6)
  • redirect_uris are matched EXACTLY at authorize time — scheme, host, port, path, trailing slash. Register home_url on 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, not application_type, is what mints a secret, shown once. (hap-integration-guide-md)

local-run

  • A compose deploy WRITES beside your manifest: it generates wbsp.docker-compose.yml next to wbsp.yaml and auto-seeds POSTGRES_PASSWORD / REDIS_PASSWORD into 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-run shows 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>.example per 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 in value_specs. An optional value that is not supplied is entirely ABSENT, never present-and-empty; test both states. A partially supplied group fails the deploy. (wbsp-client-guidelines.md §5)
  • Declare projectNo: <0..999> at the top level of wbsp.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>NNN ports 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 with redirect_uri does not match. (wbsp-client-guidelines.md §5)
  • Enable managed stores only via the top-level database.enabled / redis.enabled / s3.enabled blocks — the old resources: { database, redis, s3 } shape has been REMOVED. s3 is not supported on the compose provider (fails closed). database.shared_with requires the owning app to be deployed first and shares one schema. (application-creator-guide.md)
  • For a self-hosted platform, WBSP_HAP_BASE_URL is the provider's BASE with NO /t/<tenant> on the end — the exact opposite of AUTH_ISSUER, which is always tenant-scoped and 404s without the tenant path. WBSP_API_URL overrides the API endpoint (default https://api.wbsp.ai) and is required for a universal publish; WBSP_CLI_CLIENT_ID is 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 s3 on the compose provider (fails closed), no Kubernetes features. compose advertises the app at <app>.<projectNo>.wbsp.local — resolve it with a wildcard DNS provider or the /etc/hosts line the deploy prints. The shared wbsp-shared Docker network is operator-managed: create it once, the provider never creates or destroys it. wbsp stop keeps the database; wbsp remove DELETES it. (useful-commands.md)
  • On the local-machine types (compose, dev, standalone) the source: and dockerfile: fields are IGNORED — the platform uses the image: tag directly and you must have built it yourself with docker build. Only an aws deploy 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 --image instead. (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 same secrets: 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 in env: or secrets: 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 dev deploy writes a .env beside the app with discrete DATABASE_/REDIS_ on localhost at derived ports; source it and run the app from your IDE. wbsp db run injects 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 — --destination is 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, demo and sandbox included — 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 own env: 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 tenant otherwise). With HAP_EMAIL_MODE=console the login code is printed in docker 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_TOKEN set, wbsp whoami reports THAT token's identity — the service account's — not your cached login, because the token is what every call presents; environment.token says which is in play, and bare --user answers from the local cache with no network call. Automate against --json only: its keys are a documented contract while the labelled output may change between releases, and unavailable fields are null rather than omitted. (useful-commands.md)
  • You sign in TO A TENANT. wbsp login with no flag signs in to tenant wbsp (website accounts); --tenant names one; --destination reads the tenant a destination declares. They are NOT interchangeable — a destination called aws may declare tenant philcal. Being signed in is not being allowed: the tenant's own administrator grants the deployer role in HAP's screens. (useful-commands.md)
  • mode: demo means two unrelated things: on aws it is reserved to the demo/sandbox destinations and no other aws destination may claim it, while on a compose destination mode: demo is the production-parity shape and its DEFAULT, with nothing to do with sample data. Do not read a compose destination's mode: demo as a demo lane. (wbsp-yaml-reference.md)
  • wbsp config set is a CREATE path: the first write to an installation that does not exist yet decides its owner mode PERMANENTLY (operator-managed), and wbsp deploy --destination is refused for it from then on. Modes never change; config unset empties but does not release it, and wbsp remove takes the application away and leaves the record behind. Only wbsp config remove-installation ends 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_in from selector registrations. (hap-application-selector-md)
  • Give every application a home_url or the selector shows it as a row that cannot be opened (launchable: false in /v1/me/applications — do not forward to a non-launchable app). https://<hap>/t/<slug> is the canonical selector; /t/<slug>/hosted/apps is being retired; /menu/<slug> and /t/<slug>/menu redirect to it; /t/<slug>/selector NEVER existed and 404s. (hap-application-selector-md)
  • Leaving through Exit must 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 (typically invalid-state on 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 encoded post_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}/exit built 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: false HIDES 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 to restricted does 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}/selector NEVER existed — one earlier version of the guidelines named it, HAP has never served it, so an application linking there has a broken Exit button 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 generated value with stable: true is 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 as ENCRYPTION_KEY, never with a re-mintable one. The minting shape defaults to 32 bytes, base64 (hex/base64url allowed), and is declared so every destination mints interchangeable values. (wbsp-yaml-reference.md)
  • A universal publish refuses with declares neither source: nor dockerfile: if the manifest only names an image: — the destination builds, it does not re-tag — and it needs WBSP_API_URL set plus a wbsp login session. --destination may be omitted when universal is the only destination declared. (useful-commands.md)
  • Before building anything, wbsp deploy asks BOTH gates and you should see both lines: Authorized to publish … (deploy/publish, also admits destination-tenant holders) AND Authorized to push … (variant owner/collaborator/operator only). not allowed to push … 403 means you are signed in but neither own nor collaborate on the variant; the registry has no container repository means the catalog knows no variant for your git remote origin. (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 deploy retries once automatically; a hand-run docker push needs one manual retry. (credential-less-deploy.md)
  • The first universal publish creates a wbsp-multiarch buildx builder because stock Docker Desktop cannot build multi-platform images — do not delete it between publishes. WBSP_BUILD_PLATFORMS=linux/amd64 narrows the set but the result will NOT run on an appliance. --publish-only, --universal and --sandbox on 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_KEY with a value_specs entry of required: false (a declared secret with no spec is required, and a required secret with no value fails the deploy), and never put AI_API_KEY in a group with the other AI_* values (a group is all-or-nothing, so AI_PROVIDER plus AI_BASE_URL with no key reads as half-supplied and is refused). Where an installer should choose the provider, AI_PROVIDER/AI_MODEL/AI_BASE_URL must be declared under secrets: too — an operator-supplied value lives there even when it is not secret. (wbsp-client-guidelines.md §5)
  • Your image carries an ai.wbsp.manifest label with the effective configuration — release, port, extensions, topology, secret NAMES — readable by anyone who can pull it. Your non-secret env: values travel in the CLEAR in that label, so anything merely sensitive (internal hostnames, account ids, partner endpoints) belongs in secrets:, 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 to prod, twist or any other destination never checks for it and will not refuse for want of it. Only wbsp deploy --destination universal refuses without it — unlike an undeclared sandbox, which borrows the demo: block, universal has 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 adds universal: {} and commits it if it is missing. (wbsp-client-guidelines.md §5)
  • universal publishes an appliance-runnable image and deploys NOTHING — no lifecycle commands apply. It accepts a release: (the appliance runs it) and stamps this destination's effective configuration onto the image because an appliance cannot read wbsp.yaml. Publishing requires wbsp login; --backdoor is refused. (wbsp-yaml-reference.md)
  • value_specs rules that decide whether an install is accepted: a secret with NO spec means source: operator, required: true; required: false means an absent value is omitted entirely (never an empty string) with a warning; every member of a group must itself be required: false; kind is mandatory when source: destination; and operator specs must be declared in secrets: (top level, destination or component) while generated/destination specs may stand alone. Each refusal names the offending entry. (wbsp-yaml-reference.md)
  • wbsp login registers wbsp as your git AND docker credential helper, so git push, docker push and wbsp deploy all work off one login — but only if wbsp is on your PATH (git and docker invoke it by name; a checked-out bin/wbsp needs bin/ on PATH). The gateway address registry.wbsp.ai/<owner>/<repo>/app is git-keyed; the underlying ECR repository wbsp-v-<short-uuid>-app is variant-keyed; you never address the ECR id. (credential-less-deploy.md)

sandbox

  • A broker retains the user's multi-use login_token SERVER-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), passes state through untouched, and asserts consent=granted only 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 / proxy to reach, and every byte of its data is discarded at reap. The environment list at the top of useful-commands.md still labels aws.demo as Lambda; the aws.demo section 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 under demo: — 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 set required: false in its value_specs entry rather than deleting it: a secret with no spec means source: 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, demo and sandbox included, 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.demo on the launch path to supply one. If an appliance genuinely needs the declaration, keep it and set required: false in its value_specs entry rather than deleting it: that keeps source and kind (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's CREATE EXTENSION IF NOT EXISTS in 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) switches standalone, demo and sandbox to 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 localhost while 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=none returns login_required unless 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 --sandbox is retired — the sandbox is a destination (wbsp deploy --destination sandbox) — while --sandbox survives on wbsp 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 --sandbox shows 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 --sandbox shows who tried the variant, when, for how long and how it ended (--csv for 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 logs names the pg_dump flag that would have prevented it. (wbsp-yaml-reference.md)
  • sample_data.path is REQUIRED for a demo/sandbox placement and points at the directory of .sql seed 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 with wbsp dump-data whenever the schema or the sample content changes — nothing warns that a stale dump shipped. (wbsp-yaml-reference.md)
  • wbsp deploy --destination demo builds and pushes the demo image and STARTS NOTHING; an instance exists only when someone launches one (wbsp demo launch or the website button) at a unique subdomain, and is reaped after the timeout with a grace page. The separate step is wbsp 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 sandbox borrows the demo: block and .env.demo when no sandbox: block is declared — the only case where a destination name resolves a different block. With neither declared the sandbox publish fails. mode: demo is exclusive to the demo/sandbox reserved names on aws. use_declared_port has 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-destination release: "" disables it (e.g. a dev destination on a shared DB). (wbsp-yaml-reference.md)
  • A self-chosen client_id must be 8–128 characters of letters, digits, hyphen, period, underscore or tilde with at least one letter or digit, and may not begin hap_. Breaking the charset or length rule is 400 client_id_invalid (with rule: charset|length), a reserved prefix is 400 client_id_reserved, and an identifier already held by a live application in the tenant — including a case-variant of one — is 409 client_id_unavailable. Nothing is created when a request is refused, so retry with a corrected identifier. (hap-registering-an-application-md)
  • An aws destination with mode: on-demand runs 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 ordinary aws destination instead. (useful-commands.md)
  • Cross-app access is declared by the PROVIDER (ports: with tenant/any/public + allow-list); a consumer cannot self-grant via uses:. 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 / — otherwise wbsp list --health and info probe 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. --domain is 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 universal LAST. A cloud deploy of an app that declares universal: 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:write is 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_uris on 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: and tenant: are both REQUIRED, and for type: aws so are cluster: and enclave:; cluster: is REJECTED on the local-machine types (enclave: is accepted there but purely informational), and dev/standalone take no mode: 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 in GET /v1/me/applications with launchable: false and cannot be opened. home_url is 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/applications accepts applications: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:write is 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, add identities:read — GET /v1/applications is gated on that, not on an applications scope. (hap-registering-an-application-md)
  • The connection URL wbsp db url prints — and the DATABASE_URL that db run injects — carries uselibpqcompat=true beside sslmode=require, because modern node-postgres otherwise reads a bare require as verify-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 with unrecognized configuration parameter "uselibpqcompat" — naming something you never wrote. Pass --libpq-compat=false to 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 create and wbsp token revoke are gone, along with the platform-signed CI tokens they produced. A pipeline mints WBSP_API_TOKEN itself with the client-credentials grant against https://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 a uses: dependency) while keeping iss/aud validation 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/standalone read your .env.<destination> directly, aws/aws.demo/aws.on-demand read the values the PLATFORM holds for that installation (your deploy records them there from your .env first), and universal reads 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 with wbsp config set, or supply it locally and deploy again, and check with wbsp 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 --image into 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 explicit null leaves the stored value alone and "" clears it — so an automated redeploy that patches only redirect_uris will not disturb a label, description, icon_url or home_url an administrator set. The loopback-derived default for show_in_menu is applied at creation only and is never revisited when redirect URIs change later, so re-assert show_in_menu explicitly if a deploy depends on it. label is capped at 60 characters and description at 200, and icon_url must be https or browsers block the image. (hap-api-reference-md)
  • POST /v1/applications/{id}/rotate-secret with overlap_seconds absent or 0 is 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 positive overlap_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 400 secret_rotation_not_applicable. (hap-registering-an-application-md)
  • confidential: true, not application_type, is what mints a client secret on POST /v1/applications. Register a server-backed web app with confidential omitted or false and no client_secret comes back at all; it is a public client and must use PKCE. There is no way to add a secret afterwards — PATCH cannot set confidential, so the fix is a new registration. (hap-registering-an-application-md)
  • name and client_id are PERMANENT on every surface; scopes cannot be changed through the API afterwards (the console edit form can); post_logout_redirect_uris is 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 iff access: public on aws and ignored on the local types. Gateways that grant cloud access (registry with aws.ecr: true) or open a public TCP port (ssh-git) activate ONLY from enabled: true under destination.<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_arn and cluster_role are platform-internal. (wbsp-yaml-reference.md)
  • value_specs and auth declarations need a CURRENT CLI: an older wbsp binary silently publishes a manifest WITHOUT them — safe for consumers, who refuse as before, but your declarations go nowhere. Rebuild bin/ after upgrading. On HAP, WIDENING auth.scopes on 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 set treats every value as SECRET unless --plain; an empty value is refused (use unset); a bulk load from a file is all-or-nothing and names the offending line. config status runs 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 --enclave runs in its OWN enclave; an unregistered --enclave name refuses at install. (configuring-an-installation.md)
  • wbsp db run / connect / proxy / url read 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-cluster wbsp-db-proxy relay provisioned with the platform (the commands say so when it is missing). Only wbsp db proxy ever prints the password. A local-machine destination connects straight to the docker-compose database with no relay. (useful-commands.md)

handover

  • universal has no lifecycle commands (list, status, logs, stop, remove do not apply); verify a publish by reading image_ref and platforms from its JSON result. After any Go change to the platform, make build restamps 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.