# 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](application-creator-guide.md) or
> [parallel-services-guide.md](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](#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:

| File | Purpose |
| --- | --- |
| `wbsp.yaml` | Tells the platform what to deploy, where to route traffic, and what resources/backing stores to provision. |
| `wbsp.Docker` | The 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](wbsp-yaml-reference.md) for
the full `wbsp.yaml` field reference and worked examples, and
[application-creator-guide.md](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](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:

| Service | Conventional port | First two digits | Project suffix | Published port |
| --- | --- | --- | --- | --- |
| Web app | 3000 | `30` | `041` | `30041` |
| PostgreSQL | 5432 | `54` | `041` | `54041` |
| Redis | 6379 | `63` | `041` | `63041` |

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:

```yaml
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 types** — `compose`,
`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`:

| Label | Value | Source |
| --- | --- | --- |
| `wbsp.tenant` | your tenant | `tenant:` in `wbsp.yaml` |
| `wbsp.app` | your app name | `name:` in `wbsp.yaml` |
| `wbsp.managed-by` | `wbsp-platform` | constant |

```yaml
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)](https://www.openapis.org/) 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](application-creator-guide.md) and
   [wbsp-yaml-reference.md](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](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](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`:

```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)
```

```dotenv
# .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:

```dotenv
# .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](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](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](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](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](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`](#user-docstestingmd)
  (§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](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.
