Post-production
Items on this page are known gaps that were deliberately left out of the production cut. Each one is recorded with the same three things: what the system does today, why deferring it was safe, and the trigger that makes it due. Nothing here is an open bug report. A gap that could bite before its trigger fires would not be on this page, it would be fixed.
Read this page before planning a hardening sprint or a new deployment topology. Two of the entries below become due the moment the production topology changes.
Rate limiter hardening
apps/api/src/middleware/rate-limit.ts runs two independently tunable guards over one counting core: a global guard on all traffic, and a stricter guard on the anonymous auth surface. Both are a coarse backstop against request-volume abuse. The primary OTP defense is the OTP service's own per-phone and per-code guards (D-03), which are identity-keyed and therefore unaffected by everything below.
Trusted-proxy allowlist for forwarding headers
Today. getRateLimitKey() derives the bucket key from X-Forwarded-For, falling back to X-Real-IP, then the socket's remote address, then a per-request random key. The forwarding headers are trusted unconditionally.
Exposure. A client that rotates X-Forwarded-For mints a fresh bucket per request and dilutes both guards. Credential attacks stay capped regardless, because the OTP guards key on identity rather than address.
Trigger: the production topology is decided. Behind a proxy or CDN, honor X-Forwarded-For only from allowlisted proxy addresses. With no proxy in front, ignore the header entirely and key on the socket address.
Sliding-window counter core
Today. A fixed window. A caller can spend a full allowance at the end of one window and another full allowance at the start of the next, so a brief 2x burst is reachable at window boundaries.
Trigger: real traffic volume. The swap is internal to the counting core. The middleware interface does not change.
Redis-backed counters
Today. Counters live in an in-memory store, one per process.
Exposure. Running more than one API instance multiplies the effective allowance by the instance count, because each pod counts on its own.
Trigger: more than one API instance. Redis is already in the stack for the OTP guards and BullMQ, so this is an in-house move rather than a new dependency.
Standalone apps/api/Dockerfile
Today. The standalone apps/api/Dockerfile runs a full-workspace pnpm install --frozen-lockfile while copying only four workspace manifests, so it fails on the missing ones. This predates the B2B portal work and is not a regression.
Why it is safe. Nothing uses it. CI and compose both build from the root Dockerfile, which is correct and green.
Trigger: someone needs a per-app image. Either copy the full manifest set or delete the file. The disposition is the maintainer's call, which is why it is recorded rather than resolved.
Consent wording and the copywriting pass
Today. apps/api/src/services/consent-service.ts captures the three PDPL-required consent types (terms_of_service, privacy_policy, data_processing) with a version string per record, per D-11 through D-13. The capture mechanism, the storage, and the version pinning are all production-ready.
The gap. The authoritative legal text each of those versions points at has not been through legal review. The same applies to the wider product copy, which has not had an editorial pass.
Why it is safe. Consent records are versioned, so text finalized later is attributable. Records captured against an earlier version stay identifiable as such and can be re-collected if legal requires it.
Trigger: before real end users are onboarded. Legal review of the three consent documents, then the copywriting pass across the product surface. These travel together because both are content work on the same screens.
Real operator activation plugins
Today. apps/api/src/plugins/plugin-manager.ts resolves an operator plugin by slug. Only the mock operator is registered. The registry, the interface, and the activation service's error propagation are all complete.
The symptom. An activation against any non-mock operator raises OPERATOR_NOT_FOUND, which the activation service propagates with its own code and a 404. That surfaces to the end user as a failed activation rather than as an internal configuration error.
Why it is safe. The environment runs the mock operator, so the path is not reachable in the current configuration.
Trigger: the first real operator contract. Implement that operator's plugin against the existing OperatorPlugin interface and register it. Consider whether an unregistered operator should present to the end user as a configuration fault rather than an activation failure, since the current message tells the user nothing they can act on.