WBSP Project Guidelines

These guidelines are normative. Every project that deploys on the WBSP platform should reference this document from its constitution, and should follow the conventions below. They exist to make deployment smooth, predictable, and boring — the platform can only hide infrastructure details from you if your project is laid out the way the platform expects.

If a rule here ever conflicts with a more specific platform document (for example application-creator-guide.md or parallel-services-guide.md), follow the more specific document and raise the discrepancy.


1. File locations

A project repository has a small number of well-known directories. Keeping to them means tooling, reviewers, and the platform always know where to look.

/initial-research

Early project planning, specifications, and research live here and stay here. These are working documents that informed the project; they are not the project itself. Typical files:

  • data-model-suggestion-*.md
  • development-plan.md
  • features.md
  • legal.md
  • research.md
  • standards.md

/target

Place all application code and files under /target. This is the only directory whose application changes the platform tracks and deploys. Treat everything outside /target as supporting material (research, docs, tooling) rather than the shippable product.

/user-docs

All user-facing documentation lives here. See §3 Pre-deployment documents for the specific files required before a milestone.

/user-docs/website/index.html

A project may publish a single web page describing itself. If it does, it must be:

  • a single page,
  • static and self-contained (no build step, no external assets),
  • inline CSS (no linked stylesheets),
  • openable directly in a browser by double-clicking the file.

2. Websites and services

If your project exposes a web interface or an API, it needs to be deployable on the cloud. Provide the two platform files at the root of your project:

FilePurpose
wbsp.yamlTells the platform what to deploy, where to route traffic, and what resources/backing stores to provision.
wbsp.DockerThe Dockerfile the platform uses to build your application image.

Your application should follow the platform's basic contract — be stateless, listen on the port the platform tells it to, and take all configuration from environment variables. See wbsp-yaml-reference.md for the full wbsp.yaml field reference and worked examples, and application-creator-guide.md for the end-to-end deploy walkthrough.

Preferred web + API stack: Next.js

Next.js is the platform's preferred (but not mandatory) web + API stack. If you choose Next.js, write your APIs following nextJS-api-guidelines.md, unless there is a good reason not to do so — in which case ask the user for permission to deviate and document the decision and reason clearly (in your project's constitution or an ADR). The guidelines exist so that an API written today in Next.js can be ported to a different server framework (most likely NestJS) with minimal rework if the project ever outgrows Next's API surface.

If your project uses services (rather than providing them), start those services for local development — preferably with a docker-compose.yaml file — so a developer can run the whole project on their own machine.

Avoiding port clashes in docker-compose

Common ports (3000, 4000, 5432, 6379, …) collide when several WBSP projects run their docker-compose stacks on the same machine at once. To keep stacks runnable side by side, derive the publicly-published ports from the project's numeric prefix whenever the project name / directory has one.

For a project directory with a numeric prefix (e.g. 41-ai-native-crm), publish each service on a port formed by the first two digits of the service's conventional port as the prefix and the three-digit, zero-padded project number as the suffix:

ServiceConventional portFirst two digitsProject suffixPublished port
Web app30003004130041
PostgreSQL54325404154041
Redis63796304163041

Only the host-published (left-hand) port changes; the container-internal port stays conventional, so application code and service-to-service URLs inside the Compose network are unchanged:

services:
  app:
    ports:
      - "30041:3000"      # host 30041 → container 3000
  postgres:
    ports:
      - "54041:5432"

This rule applies to docker-compose (local development) only — it does not affect AWS or other cloud targets, where the platform assigns ports and routes traffic itself (your container still listens on the platform-specified port, e.g. 8080). If a derived value would exceed the maximum TCP port (65535), pick a nearby free high port instead.

For apps deployed via the platform's local-machine typescompose, dev, and standalone (features 055 / 063) — the above derivation is automatic. The operator declares projectNo: <N> at the top level of wbsp.yaml (or omits it and lets the platform parse the leading numeric prefix of the app directory's name), and the platform host-publishes app/postgres/redis on <two-digits>NNN ports per this rule. A derived value that would exceed 65535 (e.g. an app on port 8080 → 80NNN) folds deterministically into the IANA dynamic range [49152, 65535], and a host-port clash is reported by the deploy's pre-flight. No hand-written docker-compose.yaml is required. See docs/migration-from-local-to-compose.md.

Labelling docker-compose services for the platform

So that wbsp-app list --provider compose can see your locally-running stack and report it accurately, label every service in your docker-compose file with the platform identity from your wbsp.yaml:

LabelValueSource
wbsp.tenantyour tenanttenant: in wbsp.yaml
wbsp.appyour app namename: in wbsp.yaml
wbsp.managed-bywbsp-platformconstant
services:
  app:
    labels:
      - "wbsp.tenant=test"
      - "wbsp.app=music-rights-management"
      - "wbsp.managed-by=wbsp-platform"
  postgres:
    labels:
      - "wbsp.tenant=test"
      - "wbsp.app=music-rights-management"
      - "wbsp.managed-by=wbsp-platform"

Apply the same wbsp.tenant/wbsp.app pair to every service in the stack (web, database, redis, worker, …). The platform groups them into a single application row, derives running/stopped from whether the containers are actually up, and excludes unrelated Compose stacks that carry no wbsp.app label. Without these labels the platform cannot tell your stack apart from any other Compose project, so it will not appear in the application list.


3. Pre-deployment documents

The following documents must be created and kept up to date prior to any major milestone. They are written for people who are not on your team and may have no access to our platform, so favour plain language over jargon.

/user-docs/api-reference.md and /user-docs/openapi.yaml

Required if the project provides an API.

  • api-reference.md must contain everything an unrelated project would need to consume this project's services — endpoints, authentication, request and response shapes, error handling, and examples.
  • openapi.yaml must describe the same API in valid OpenAPI Specification (OAS) format.

/user-docs/testing.md

Explain, in layman's terms, the testing this project has been put through — what is covered and why it gives confidence. Then enumerate the specific individual tests as an appendix at the end of the file.

/user-docs/useful-commands.md

A summary of the commands and configuration an application developer needs: relevant CLI commands, configuration, and any AWS / EKS / database commands.

  • This document is only for app developers, who may not have control of or access to our actual platform.
  • Do not include anything about setting up the platform itself.
  • Do show the useful commands for docker compose, for deploying to AWS, and for running the parallel-services environment.

/user-docs/getting-started.md

Written in layman's terms, this is the front door to the project. It must:

  1. Describe what the project does, including short explanations of things that might be assumed obvious (e.g. what a monorepo is) and any jargon or acronyms used.
  2. Explain how a reader can experience the application — either for a real purpose or via a demonstration scenario.
  3. Finish with a list of useful commands and pointers to other documentation in the project that might help.

4. Data stores: Postgres and Redis

  1. The platform provides PostgreSQL and Redis when required. Applications should use these in preference to standing up their own data stores. Enable them through wbsp.yaml:

    • a dedicated database via database.enabled: true, and
    • a Redis store via redis.enabled: true.

    Choosing a Redis mode. Default to cache mode (redis.enabled: true with durable: false, the default) — it is provisioned on ElastiCache and is the right choice for caching, rate-limiting, sessions, and retryable/periodic job queues. Only set durable: true when your data must survive a node failure or restart (a persistent store rather than a cache); it is provisioned on MemoryDB and takes longer to create.

    Treat cache-mode Redis as ephemeral. Anything in it — including queued jobs — can be lost on a failover or restart. If you run a job queue (e.g. BullMQ) on cache mode, design jobs to be idempotent and safely retried, and do not use Redis as the system of record. If you cannot tolerate that, use durable: true (or keep the source of truth in Postgres).

    When enabled, the platform injects connection details as environment variables (DATABASE_* and REDIS_* — including REDIS_NAMESPACE, the key prefix your app should use, and REDIS_TLS). Build your client from these discrete variables (there is no single REDIS_URL/DATABASE_URL). See application-creator-guide.md and wbsp-yaml-reference.md.

    This contract is identical across every deployment type. Whether your app runs on aws or on any of the local-machine types — compose (whole stack in containers), dev (data services in containers, your app run from your IDE), or standalone (everything in one container) — it receives the same DATABASE_*/REDIS_* variables. Read those and your code needs no change between local and the cloud. (Local types reach the stores at localhost or the in-stack service name on derived ports; you never hard-code that — it comes from the variables.)

  2. If your project needs other backing services (for example Forgejo, ClickHouse, and similar), provision them through the platform's Parallel Namespace facility rather than embedding them in your app. See parallel-services-guide.md.

  3. Evolve schema through migrations — never hand-edit a deployed database. Manage your schema with versioned migrations checked into the repository (e.g. Prisma Migrate) and apply them as part of your deploy/release process. Never make ad-hoc, out-of-band changes to a deployed database — no manual CREATE/ALTER/DROP DDL, no hand-edited rows, no one-off SQL data fixes run by hand. Direct connections (e.g. psql via a port-forward) are for inspection and debugging; every schema or data change must flow through a migration or the application's own code so it is reviewed, versioned, and reproducible across all targets.

    ORM choice. Prisma is preferred as the default ORM and migration tool. Drizzle is acceptable where it is required for performance reasons — either at startup (e.g. cold-start-sensitive environments such as Lambda) or at runtime (where specific optimised or customised SQL is required and Prisma's query layer is a poor fit). TypeORM is discouraged. If these guidelines cannot be followed for a given project, ask permission and document the decision and reasons (in the project's constitution, an ADR, or equivalent) so the choice is visible to reviewers and future maintainers.


5. Environment configuration and secrets

The platform delivers all configuration to your container as environment variables. Split configuration into three tiers — non-secret config, secrets, and per-destination overrides — so the same image runs everywhere and no secret is ever committed.

Configuration is delivered per destination. Each destination reads one environment file — .env.<destination> — and the per-destination, never-layer rules below apply to every destination. See wbsp-yaml-reference.md for the full wbsp.yaml field reference and worked examples.

Non-secret config → wbsp.yaml env:

Plain runtime settings (feature flags, log levels, public URLs) go in the env: map of wbsp.yaml. These are committed. You may override per destination under destination.<name>.env: (the destination wins over the base env:).

Secrets → wbsp.yaml secrets: + a git-ignored .env

Never put secret values in wbsp.yaml. Declare the secret env vars your app needs in a secrets: block by reference (the key to read), and put the actual values in a git-ignored .env beside wbsp.yaml:

# wbsp.yaml — declares WHICH secrets the app needs, never their values
secrets:
  HAP_SMTP_PASSWORD: SES_SMTP_PASSWORD   # env var the app receives : .env key holding the value
  HAP_ADMIN_API_KEY: HAP_ADMIN_API_KEY   # (a key may reference itself)
# .env  (git-ignored — never committed)
SES_SMTP_PASSWORD=BICoEXAMPLE...realtoken
HAP_ADMIN_API_KEY=…

At deploy the platform resolves each reference, stores the values in a protected secret object (not in the pod spec, not in git), and surfaces them to your container as the named environment variables. A declared secret with no value fails the deploy (fail-closed) — it never deploys silently unset.

Per-destination values → .env.<destination>

Each destination reads one environment file: .env.<destination> (e.g. .env.prod, .env.dev). When a value differs by destination — e.g. a real SES password on AWS but a dummy locally — supply the right value in each destination's file, keeping a single secrets: declaration:

# .env.dev
SES_SMTP_PASSWORD=dev-dummy
# .env.prod
SES_SMTP_PASSWORD=BICoEXAMPLE...realtoken

Destinations never layer — each stays in its own lane. A .env.<destination> is the only file consulted for that destination; it does not fall back to a bare .env or borrow from another destination's file. Every key a destination needs must live in that destination's own file.

Precedence (highest first):

  1. A real OS environment variable (operator override)
  2. .env.<destination> (the single file for the selected --destination)

Rules

  • Add .env* to .gitignore so real .env.<destination> files are never committed — but do commit .env.<destination>.example templates (see below). Because .gitignore .env* also matches .env.dev.example, add an un-ignore rule: !.env.*.example.
  • No secret value may appear in wbsp.yaml or any committed file — with the one allowed exception below (the shared test-HAP credentials in the committed local .example templates).
  • Read every value (secret or not) from the environment; never bake config into the image.
  • This is a stopgap: today secret values come from your .env.<destination>. A future platform secrets manager will resolve the same secrets: references from a managed vault with no change to your app — so always declare secrets by reference, never inline.

Ready-to-run local environment files

Every project following these guidelines generates the local-destination environment files pre-wired to the shared test HAP server, so a developer can run and sign in immediately. Concretely, for each destination provide two files:

  • a committed .env.<destination>.example template — no secret values, except the shared test-HAP credentials on the local destinations (dev, prod-parity, standalone), which are safe to share; and
  • a git-ignored .env.<destination> that is as ready-to-run as possible — for the local destinations, fully populated (including the test-HAP values) so the app runs with no manual edits; for cloud destinations (stage, prod), populated except for the managed secrets only your operator can supply, which are clearly marked.

The three local destinations point at the shared test HAP server (issuer https://wbsp-auth.wbsp-demo.com, redirect http://localhost:3000/api/auth/callback). See wbsp-yaml-reference.md for the worked wbsp.yaml and the five per-destination .env examples.


6. Authentication

If your project requires user authentication, prefer the platform's Headless Auth Platform (HAP) — an API-first, multi-tenant OAuth 2.0 / OpenID Connect identity service. Your app delegates login to it and receives standard OIDC tokens, so you do not build or operate your own credential store, password hashing, MFA, or session issuance. See also single-tenant-guidelines.md for single-tenancy and shared sign-on (SSO) across a user's apps.

Integrate over standard OIDC, through a well-supported OIDC client (e.g. Auth.js / NextAuth, or your language's OIDC library) rather than bespoke calls:

  • Discover endpoints from ${AUTH_ISSUER}/.well-known/openid-configuration.
  • Sign in with the Authorization-Code + PKCE flow. One client covers every redirect-based mode — standard sign-in, SSO between sibling apps (so a user already signed into one app isn't asked again), and the optional Hosted Login mode where HAP hosts the login UI and your app ships none.
  • Register your callback as ${YOUR_APP_URL}/api/auth/callback.
  • Resolve identity after the round-trip: HAP access tokens carry only the subject (sub), so your app runs its own "who am I" step (typically a GET /me) mapping the subject to your app's role/tenancy — see "Make identity resolution resilient" below.

HAP owns the authoritative client guidelines (SSO-client, client cookbook, integration guide, hosted-login, and SSO-broker how-tos). They are the source of truth and may evolve, so obtain the current versions from your platform operator (or the hap-docs index page on this wiki, which links every HAP guide) and follow them rather than any copy here. For a ready-to-run local setup, the .env.<destination> examples in wbsp-yaml-reference.md are pre-wired to the shared test HAP server.

If those guidelines cannot be implemented, or are not appropriate for your project, do not silently diverge: tell the user the specific problem and document the proposed alternative (what you will do instead, and why).

Integrate through a replaceable adaptor. Authentication must sit behind a thin, swappable layer so the project can move to a different identity provider — or a different auth model entirely — with minimal work. Concretely:

  • Use the standard OIDC interface, not HAP-specific code. Talk to HAP via OIDC discovery (${ISSUER}/.well-known/openid-configuration) and the authorization-code + PKCE flow, using a well-supported auth library configured with a provider entry (e.g. Auth.js / NextAuth, or your language's OIDC client) rather than bespoke calls.
  • Keep all provider specifics in configuration/secrets (see §5), never hard-coded: issuer URL, client id/secret, scopes, claim mapping. Switching IdP should be an env/secret change plus a provider swap, not a rewrite.
  • Confine sign-in/out, token validation, and session handling to one auth module that the rest of the app depends on through a small, stable interface. An alternate model (a different OIDC IdP, a managed auth service, or SAML) must be a drop-in replacement behind that interface.
  • Inject HAP's connection details (issuer, client credentials) as platform config/secrets at deploy time; register your callback as ${YOUR_APP_URL}/<auth-callback-path>.

This applies the platform's Adaptor-Based Integration principle to identity: depend on the capability ("authenticate a user"), not on a specific vendor.

Make identity resolution resilient — never let optional state break login. HAP access tokens carry only the subject (sub), so your app has its own "who am I" step after the OIDC round-trip (typically a GET /me that maps the subject to the app's role/tenancy). That call is the gate to the whole app: if it fails, the user is bounced back to sign-in, and if it fails repeatedly they are stuck in a sign-in loop. Therefore:

  • Identity resolution must depend only on the verified token, never on optional request headers, query params, or persisted client state (e.g. a saved "view as"/impersonation selection, a remembered tenant/role, a feature flag). If such extra input is present but invalid or stale, degrade to the base identity — ignore it and resolve the real user — rather than returning an error from the identity call.
  • Persisted client state can outlive the server. Ids in localStorage/ sessionStorage/cookies survive redeploys and data reseeds; treat any stored id as possibly dangling. The server should tolerate it (fall back), and the client should self-heal (drop a selection the server reports as not applied) so it can't be re-sent and wedge the session.
  • Diagnose loops by the identity call's status. A sign-in page that just refreshes is almost always the who-am-I call failing. Check its status in the API logs: a repeating non-2xx on that one endpoint is the loop. Keep a short per-app login-troubleshooting note listing the known causes and their fixes.

7. Logging

The platform aggregates application logs and runs anomaly detection over them (the LogWatch platform). That analysis is only as good as the structure of the logs it receives, and unstructured logs degrade detection quality for every tenant sharing a service baseline — so log structure is a shared responsibility, not a per-team preference.

The single most important rule:

Emit machine-readable, structured logs whose fields map cleanly onto the OpenTelemetry Logs Data Model — either as OTLP, or as JSON on stdout.

In practice, applications should:

  • Log to stdout/stderr, not to files on disk. On both EKS and Lambda the platform's collector tails your container output and forwards it — do not manage log files, rotation, or direct network shippers from your app.
  • Emit one-line JSON per event, with at least severity_text and body, plus service.name, service.version, and deployment.environment where available.
  • Keep the message (body) stable and put variables in attributes (e.g. order.id, http.response.status_code) so anomaly detection can group by message template. Use OpenTelemetry Semantic Convention names where one exists.
  • Use UTC, RFC 3339 timestamps, and include W3C trace_id/span_id when emitting inside a request context.
  • Never log secrets, credentials, tokens, or unnecessary PII — ingestion-time redaction is a backstop, not a licence.
  • Prefer the platform's OpenTelemetry SDK/agent and your language's recommended structured logger over a bespoke logging mechanism.

These are recommendations; deviation is permitted (for sources that genuinely cannot emit structured output, vendored components, local development, and extreme hot paths) and should be documented in the project's constitution or an ADR.

The same logging contract applies to any backing services you run through the platform's Parallel Namespace facility — their stdout/stderr is collected the same way, so emit structured logs there too. See parallel-services-guide.md.

For the full field reference, per-language library recommendations, ingestion paths, and the conformance checklist, follow the platform's Client Logging Guidelines — owned by the LogWatch log-aggregation project, which is the source of truth. Obtain the current version from your platform operator (or the LogWatch section of this wiki).


8. Testing and test-driven development

Projects on this platform are expected to follow test-driven development (TDD). Write the test first: express the intended behaviour as a test, run it and watch it fail, then write the minimum code to make it pass, and refactor with the test still green (Red-Green-Refactor). The test exists before the implementation, not after it.

TDD is mandatory for the parts of a project where a silent defect is costly — anything handling money, identity, authorization, data integrity, or other business-critical logic. Auth, in particular, must be developed test-first: if your project integrates the Headless Auth Platform (§6) or implements any credential, token, session, or access-control logic of its own, hold it to the stronger standard (HAP's own constitution makes test-first non-negotiable for that surface).

In practice:

  • Test behaviour, not implementation. A test should describe what the code must do for a caller, so it survives refactoring.
  • Integration tests run against real backing services, not in-memory substitutes — use the platform's PostgreSQL and Redis (§4), or ephemeral containers (e.g. testcontainers), so tests exercise the same engines you deploy on. Mock only third-party HTTP you do not own.
  • Keep tests fast and runnable with one command, and wire them into your build so they run on every change.
  • Record what is covered in /user-docs/testing.md (§3) — the layman's summary plus the enumerated test appendix described there.

It is the project's call whether TDD fits a given piece of work. If TDD cannot be applied or is not appropriate for some part of your project, the project must not silently skip it: notify the user clearly (which code, and why test-first does not fit — for example a spike, a generated client, a UI-layout concern, or an extreme hot path), explain why, and document the proposed alternative (what you will do instead — characterization tests written immediately after, manual verification with evidence, contract tests, etc. — and why it gives comparable confidence). The escape hatch is "explain and substitute", never "skip quietly".


9. Respect other projects' boundaries

Each project owns its own repository. Stay strictly inside your own project and never reach into another one:

  • Do not change files that belong to another project. If you need to send a message, reply, or hand something off to another project, write it into your own /docs directory (not /user-docs), from where the operator can copy it across. Treat another project's repository as read-only.
  • Never run git operations on another project, and never deploy another project. Committing, pushing, branching, or deploying outside your own repository is the operator's responsibility, not yours.

Quick checklist

Before a milestone, confirm:

  • Application code is under /target only.
  • Research/planning docs are under /initial-research.
  • User docs are under /user-docs.
  • wbsp.yaml and wbsp.Docker exist (if the project is a website/service).
  • If the project uses Next.js, APIs follow nextJS-api-guidelines.md (or any deviation has been approved by the user and documented).
  • docker-compose.yaml provided for any services the project consumes.
  • docker-compose host ports derived from the project's numeric prefix (e.g. 30041/54041) to avoid local clashes; internal ports unchanged.
  • Every docker-compose service labelled with wbsp.tenant/wbsp.app (from wbsp.yaml) and wbsp.managed-by=wbsp-platform, so wbsp-app list --provider compose can discover and reconcile the stack.
  • getting-started.md, testing.md, and useful-commands.md are current.
  • api-reference.md + openapi.yaml exist (if the project provides an API).
  • Postgres/Redis enabled via wbsp.yaml; other services via parallel namespaces.
  • Database schema is managed by versioned migrations; no ad-hoc DDL or manual SQL is applied to deployed databases.
  • Secrets declared by reference in wbsp.yaml secrets:; values only in git-ignored .env.<destination> (never committed); .env* is in .gitignore.
  • Committed .env.<destination>.example templates exist (no secrets except the shared test-HAP credentials on local destinations); .gitignore un-ignores them via !.env.*.example; local destinations (dev, prod-parity, standalone) are pre-wired to the test HAP server.
  • If the project authenticates users, it uses the Headless Auth Platform via standard OIDC behind a replaceable auth adaptor (provider specifics in config/secrets, not hard-coded).
  • Code is developed test-first (TDD), mandatory for money/identity/authorization/data-integrity logic; where TDD is not appropriate the project has notified the user, explained why, and documented the alternative.
  • Logs are structured JSON on stdout/stderr, mapped to the OTel Logs Data Model, with no secrets.
  • user-docs/website/index.html is single-page, static, self-contained (if present).
  • No files in other projects were changed; messages to other projects went into this project's /docs (not /user-docs), and no git or deploy operations were run on another project.