Blog
Inside the UserBaseAPI ADRs
2026-05-15
Nineteen decisions, not nineteen implicit choices
UserBaseAPI is a FastAPI backend for account management, subscriptions and billing, and S3-backed file storage built around a quarantine-first upload and scanning pipeline — nothing lands in the approved bucket until a scanner clears it. It's currently in beta architecture verification: the goal right now is confirming the architecture holds up under real use, not production readiness. That framing matters here specifically, because it's exactly the kind of project where the temptation is to let every technology and design choice live only in the code and in memory — discoverable, if at all, by reading a diff or asking whoever wrote it. UserBaseAPI's answer is to make every one of those choices a numbered, checkable Architecture Decision Record instead. Nineteen of them exist today, covering everything from the web framework itself down to how local dev decides whether it needs Docker.
This post isn't a restatement of what each ADR says — that's what the records themselves are for, and every one of them is linked below. It's a walkthrough of the process: how the records are numbered, what their status vocabulary actually means, what one looks like on the inside, and where all nineteen came from.
The rules the process runs on
The ADR index states the whole process in one line: numbered, chronological, and never edited after acceptance. A decision that changes doesn't get its old file rewritten — it gets a new ADR that points back at the one it replaces, and the old one stays exactly as it was, as a historical record of what was actually decided and when. Three states show up in practice:
- Accepted — the default state, and where most of the nineteen sit today.
-
Superseded by … — a later ADR replaced this
one's decision outright.
ADR-0007
picked Authlib for JWT and Google OAuth; once
authlib.joseturned out to already be deprecated in the pinned version, ADR-0011 replaced it with PyJWT +httpx-oauthand says so explicitly in its own status line: "Accepted — supersedes ADR-0007." ADR-0007 wasn't deleted or rewritten to match — it's still there, marked superseded, showing what the project actually ran on before and why that stopped being true. -
Amends / Amended by … — a softer relationship
than superseding: a later ADR changes what an earlier one said the
future would look like, without necessarily reversing the earlier
decision itself.
ADR-0010
deferred migrations to SQLAlchemy's
create_all()for beta and planned to "introduce Alembic once the schema stabilizes." By the time that plan was revisited — aftercreate_all()'s real limitations had already been hit twice in practice — ADR-0015 amended that stated plan: no Alembic is coming, at any schema-stability point, in favor of plain numbered SQL files plus a version table. The original decision to defer migrations for beta wasn't wrong; what it said would happen next was, and the record shows exactly that distinction instead of blurring the two together.
What one ADR actually looks like
Every record follows the same shape: a short metadata block (Status, Date, and a Source line naming where the decision was originally documented), then Context, Decision, and Consequences — not just what was chosen, but the situation that forced the choice and what it costs going forward. ADR-0014, the quarantine-first upload pipeline the README leads with, is a good example of the shape doing real work rather than just formatting:
- Context: Linode Object Storage (the choice in ADR-0008) has no built-in scanning and no event triggers to hook a scan into automatically the way AWS S3 + Lambda would — and an upload response can't block on a full scan without making uploads unacceptably slow.
-
Decision: no file is ever written directly to the bucket
other code reads from. Every upload lands in a quarantine bucket first,
gets SHA-256-deduplicated, and only a
CLEANscan result copies it into the approved bucket — with a cron job separately recovering uploads stuck mid-flight if the API crashed between the S3 write and the database update. - Consequences: every approved upload now costs an extra S3 copy operation (quarantine → approved) instead of writing to the final bucket once — stated plainly as the direct cost of not having a cloud-native scanning hook, accepted in exchange for a hard guarantee that unscanned content is never reachable through the approved-files code path.
That last part is the part worth noticing about the format itself: the record doesn't just justify the decision, it names the price being paid for it, in the same document, right next to the decision that incurred it.
Where nineteen records actually came from
Not every ADR here was written the moment its decision was made — the index is honest about that, broken into three batches:
-
0001–0010 were extracted from the "Architecture
Decision Records" section of the original build spec
(
archive/userbase_build_spec.md, 2026-05-18) and reformatted into individual files after the fact — real decisions, retroactively given their own numbered record. -
0011–0018 were pulled from decisions already
documented across various planning docs under
_userbaseapi/complete/that hadn't yet been captured as formal ADRs in the build spec. -
0019 is the one record with no prior planning document at
all — written directly against the working tree, sourced straight
from the actual commit (
app/core/db_types.py,app/core/database.py) that made SQLite a real local-dev option alongside Postgres. It's the clearest example of the process running the way it's meant to going forward: the record and the decision arriving together, instead of the record catching up to a decision made somewhere else first.
All nineteen, linked
The full, current index — every record on GitHub, in order:
Browse the adrs/ directory on GitHub
for the current, canonical version of this list — it will keep
growing past nineteen, and the table above is a snapshot, not a live mirror.
Why this is the discipline worth keeping in beta
None of this is process for its own sake. A project explicitly in architecture-verification mode is, by definition, still finding out which of its own choices actually hold up — and a decision that only lives in code or in someone's memory can't be argued with, revisited cleanly, or handed to the next person (human or agent) touching the codebase. Numbering every decision, dating it, and never rewriting it after the fact turns "why does it work this way" from a question that needs the original author into one the repository can answer on its own.
Get the code
The full UserBaseAPI repository is on GitHub, BSD 3-Clause licensed.
See the UserBaseAPI project outline for the architecture and feature rundown these decisions actually produced.