Day one: a multi-tenant framework from an empty repository
The platform's first evening: a written plan, a toolchain that refused sloppy code, and the request context that tenancy would hang from.
The repository went from empty to seven commits on the evening of 1 February 2026, between twenty past ten and ten to midnight. None of them served a request. They held a plan, a toolchain, a settings object, a request context, translations and a documentation standard, and two of those, the plan and the context, still shape the platform.
The plan came before the code#
The first commit carries more planning than code: an architecture document, an implementation plan in ten phases, and twenty-two task files, one per piece of work, from the repository skeleton to a documentation site. The code in it is an empty package and a placeholder test.
The plan describes an opinionated backend framework rather than an application, because the application did not exist yet. What was known was the kind of software it would carry: business applications where several organisations share one deployment, where who may see what is decided by rules rather than by each screen, and where files, audit trails and translations are properties of the base rather than features to add. Those went into the plan as the framework's job, so that every model written later would get them without asking.
Its central decision was tenancy by logical separation in a single database. Every request would carry the organisation it acts for, every tenant-owned table would carry an organisation column added by the base model, and the database session would add the organisation filter to every query on its own. A second post covers how that went. The rest of the plan was the operational floor: code-defined access rules, audit logs, soft deletes, rate limits per tenant, feature flags, health checks, and migrations kept additive so a blue/green deployment never meets a column the old version still reads.
The implementation plan was addressed to a coding agent. I made the decisions and reviewed the result; the plan was how the decisions reached the agent intact, one task file at a time.
What the first evening built#
The tooling came in the first minute after the skeleton: pre-commit running Ruff, codespell and mypy, so nothing that failed them could be committed from the start. The dependencies named the stack the plan chose: FastAPI, async SQLAlchemy 2.0, Pydantic, PostgreSQL, Redis, and arq for background jobs.
Configuration went into a Pydantic settings class read from the environment: the database, Redis and the security settings, typed and tested.
The request context was the piece that mattered most. It is a small module of context variables holding the current organisation, the current user and the database session, set when a request starts and reset when it ends. Context variables follow each asyncio task, so two requests handled concurrently on one worker each see their own organisation. The first test of the context made exactly that point: it ran two tasks at once with two different organisations and checked that each read back its own.
Translations came next, with Babel: a middleware that reads Accept-Language, falls back from a
regional variant such as nl-BE to its language, and a gettext wrapper for messages. English,
French and Dutch were set up, and the validation errors got a French translation on the first
night, before there was much other text to translate.
The last three commits of the evening applied Google-style docstrings across the code and the tests, and then applied the written style rules to every remaining file. There were only a handful of files, which is exactly when a standard is cheap to adopt.
What survived and what did not#
By September 2026 most of the stack from that evening is still in place: FastAPI, async SQLAlchemy, Pydantic, PostgreSQL, Redis, arq and Babel. The request context is the same module grown larger. Beside the organisation and the user it now carries the effective groups, privileges and access rights the security layer reads, and background jobs replay it so a job runs as the request that queued it.
Several choices were reversed, some within days:
- mypy gave way to basedpyright four days later, for better inference over the field system.
- Code-first policy classes, the plan's security design, became a declarative rules DSL ten days in: rules declared on the model and combined with logical operators, which one engine turns into both a query filter and a check on a single record.
- The organisation was renamed tenant on the fourth day, then back to organisation in April, when tenancy was rebuilt around organisations and many-to-many memberships.
- Poetry gave way to uv in April.
- Google Cloud Storage, the plan's file store, gave way to an S3-compatible backend in July, once the infrastructure was going to run on Scaleway.
- Dutch did not stay: the application speaks English and French.
Rate limiting, soft deletes and health checks were built as planned.
What it cost#
An evening, and a habit: the second commit put a gate in front of every commit that followed. The gates themselves changed. Pre-commit gave way to prek two weeks later, mypy to basedpyright, and the checks have been added to since rather than relaxed, down to rules on the prose in comments and pages like this one.