Architecture Decision Records
An ADR captures one significant architectural decision: the problem we faced, what we chose, and what that choice costs us. The point is that a developer who joins later and asks "why is it like this?" finds the answer here instead of reverse-engineering intent from code or asking someone who has forgotten.
These first twelve records were written retroactively on 2026-07-24, after the decisions were made and shipped. Each one was written against the actual code — the References section links the files that prove the decision is real.
Format
Each ADR is one markdown file, NNNN-short-slug.md, containing:
# ADR-NNNN: Title
**Status:** Accepted | Accepted (retroactive) | Superseded by ADR-XXXX | Deprecated
**Date:** YYYY-MM-DD
## Context — the problem and the constraints that shaped the choice
## Decision — what was chosen, concretely (names, versions, config)
## Consequences — positive AND negative; every ADR lists real trade-offs
## References — relative links to the code/docs that embody the decision
Status conventions
- Accepted — decided and in force; written at decision time.
- Accepted (retroactive) — in force, but the record was written after the fact by inspecting the code. All ADRs below carry this status.
- Superseded by ADR-XXXX — replaced; keep the file, add the pointer.
- Deprecated — no longer in force and nothing replaced it.
Never delete or rewrite an accepted ADR's Decision; write a new one that supersedes it.
When to write a new ADR
Write one whenever a decision would make a future developer ask "why?": picking or dropping a dependency, a security mechanism, a data-modeling rule, a deployment shape, anything that constrains future work, or anything you had to argue about. Small reversible choices (a variable name, one component's props) do not need one. Next number: 0018.
Index
| ADR | Title | One-line summary |
|---|---|---|
| 0001 | PostgreSQL 16 + Flyway | Versioned SQL migrations own the schema; Hibernate only validates (the legacy app let Hibernate generate MySQL DDL) |
| 0002 | Monorepo of independent projects | One repo, no parent POM/workspace — backend, frontend, e2e, legacy app, and inventory microservices build separately |
| 0003 | JWT + rotating refresh tokens | 15-min HMAC256 access tokens paired with 14-day rotating opaque refresh tokens with family reuse-detection |
| 0004 | Argon2id with transparent rehash | Argon2id via DelegatingPasswordEncoder; legacy BCrypt hashes upgrade silently on next login, no forced resets |
| 0005 | Self-hosted ALTCHA | Proof-of-work CAPTCHA with no third-party calls, at the price of CSP wasm/worker allowances and weaker bot deterrence |
| 0006 | In-memory Bucket4j rate limiting | Per-IP token buckets on auth endpoints, in process memory — justified by one-backend-per-client deployment |
| 0007 | Per-client isolated stacks | One compose stack (db+backend+frontend) per client instead of a multi-tenant DB; no tenant_id anywhere |
| 0008 | Traefik edge, TLS at Cloudflare | Traefik v3 label-routing on :80 only; Cloudflare terminates TLS; origin hop is plain HTTP |
| 0009 | Redux / React Query / Zustand split | Redux for the auth slice only, React Query for all server state, Zustand for portal/UI islands |
| 0010 | AG Grid Community + in-house grid subsystem | Rebuild saved views/tabs/bulk actions/export on Community instead of buying AG Grid Enterprise |
| 0011 | Permission-name authorities; activeRole is a UI lens | Backend authorizes on the union of permission strings; role switching exists only in the frontend |
| 0012 | springdoc as the single API contract | Annotation-generated OpenAPI at /v3/api-docs, gated by SWAGGER_ENABLED; no hand-maintained Postman collection |
| 0013 | Shared-database multi-tenancy | One stack serves many companies via a tenant_id discriminator, enforced by Hibernate and again by Postgres row-level security; supersedes 0007 for the hosted offering |
| 0014 | Caddy origin TLS + GHCR CI/CD | Caddy terminates Let's Encrypt TLS behind Cloudflare Full (strict); GitHub Actions builds sha-tagged images to GHCR and deploys over SSH with a health gate and auto-rollback; supersedes 0008 for the hosted shape |
| 0015 | Staging tier + promote-by-button | A second full stack on the same VPS at stage.<domain> behind a shared Caddy edge; merges auto-deploy staging, production ships the exact staged image tag — no stage branch, no rebuild (promotion model amended by 0016) |
| 0016 | Continuous promotion + Secrets as config | Production promotes automatically once staging's health gate passes — no routine manual step; VPS env files are rewritten from PROD_ENV_FILE/STAGE_ENV_FILE GitHub Secrets on every run |
| 0017 | Documentation Central on Docusaurus | docs/ stays the source of truth; a staging script builds a public Docusaurus site with a generated OpenAPI reference, shipped by its own image, compose project and workflow so docs and app deploys never block each other |