Decision records (ADRs)
Every significant architectural call is recorded as an ADR in
docs/adr/ in the
platform repository — the why behind the design, kept honest by marking
superseded decisions instead of rewriting history. Check the relevant ADR
before proposing an alternative to something already settled; if a decision
looks wrong for what you’re building, propose a new ADR rather than
quietly working around it.
| ADR | Decision | Status |
|---|---|---|
| 0001 | App-level multi-competition tenancy (a competition_id on every scoped table), not schema-per-competition |
Accepted |
| 0002 | Kernel / required-core / optional-module split — nearly everything is a module | Accepted |
| 0003 | JWT access + refresh tokens, one scheme shared by REST and WebSockets | Accepted |
| 0004 | Roles and permissions as data, not a hardcoded enum | Accepted |
| 0005 | An async in-process pub/sub event bus as the mutation-notification core | Accepted |
| 0006 | pytest (SQLite-backed, infra-free) + Vitest | Accepted |
| 0007 | First registered user becomes Administrator | Superseded by 0010 |
| 0008 | Refresh tokens are stateful, hashed, rotating DB sessions | Accepted |
| 0009 | Synchronous event dispatch in Tier 0 | Superseded by 0012 |
| 0010 | Seeded default administrator credentials | Superseded by 0017 |
| 0011 | Theming is site-wide only; per-competition theming deferred | Accepted |
| 0012 | Event dispatch splits into sync-critical and background lanes | Accepted |
| 0013 | Webhook egress policy — SSRF blocklist, header stripping, value hardening | Accepted |
| 0014 | CRDT transport as a dumb relay with client-snapshot persistence | Accepted |
| 0015 | Username is the primary identifier; email is optional | Accepted |
| 0016 | Platform export/import — registry-driven, additive backup | Accepted |
| 0017 | First-run setup wizard; no seeded default admin | Accepted |
| 0018 | Regex flag evaluation contained against ReDoS | Accepted |
| 0019 | Per-install derived JWT secret — never a repo-public default | Accepted |
| 0020 | Hash what is only verified; encrypt what must be retrieved | Accepted |
| 0021 | External identity via OIDC, with local login as break-glass | Accepted |
| 0022 | SAML and LDAP identity providers — generalizing the provider model | Accepted |
| 0023 | AI assistant provider integration and execution model | Accepted |
| 0024 | Built-in SSO provider presets — configuration, never credentials | Accepted |
| 0025 | Multi-worker via a Redis broadcast relay behind the connection manager | Accepted |
| 0026 | Cross-worker presence via heartbeat-TTL liveness in Redis | Accepted |
| 0027 | Certificate rendering — server-side Pillow → PNG, not a headless browser | Accepted |
| 0028 | Competition status is an explicit gameplay gate; the schedule drives it | Accepted |
| 0029 | Frontend i18n — next-intl with a cookie locale, translations in Crowdin | Accepted |
| 0030 | Post-event report rendering — HTML + WeasyPrint, not Pillow or a headless browser | Accepted |
| 0031 | Multi-instance deployment behind a load balancer (Fargate/ALB) | Accepted |
| 0032 | Multi-tenant Entra issuer validation via a tenant-substituted issuer template | Accepted |
| 0033 | A generic oauth2 provider kind, with userinfo as the identity source |
Accepted |
| 0034 | Custom pages store rich text and render as a React tree, never as HTML | Accepted |
| 0035 | Relicense from AGPL-3.0 to Apache 2.0; module exception retired | Accepted |
| 0036 | Challenge instancing — provisioner kinds, background-lane lifecycle, hashed unique flags | Accepted |
| 0037 | Prometheus /metrics — kernel middleware, off by default, gated by token/allowlist |
Accepted |
Two threads are worth reading in sequence to see the project’s decision style: the bootstrap story (0007 → 0010 → 0017 — from “first user wins” to seeded credentials to a proper setup wizard) and the event-dispatch story (0005 → 0009 → 0012 — from a simple bus to explicitly split delivery lanes once automations needed slow external calls).