← Building
Building6 updates

Turmarium

Multi-tenant school-management SaaS with tenant isolation enforced in the database

A B2B SaaS for schools: people, disciplines, terms, turmas, grades and attendance. A Django modular monolith with tenant isolation enforced by Postgres RLS.

  • Django
  • django-ninja
  • PostgreSQL
  • Postgres RLS
  • JWT

Matrícula by reference: one enrollment number, one home, and the rule a CHECK constraint couldn't hold

Update 0611 min

Update 6 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Update 6 builds the curso: a Course, its matriz curricular (disciplines attached to periods, level-guarded), and student registration that assigns a matrícula (the enrollment number a Brazilian student carries). The número lives on exactly one row (CourseEnrollment), the old Membership.enrollment_number is dropped, and every turma enrollment reads the matrícula through a foreign key instead of copying it. The sequence looks like a calendar and isn't: it's continuous per org, allocated under a locked Organization row so concurrent registrations can't collide. The cross-level guard (a discipline may only sit in a course of its own level) can't be a single-row CHECK constraint because it compares two tables, so it lives in the service with a red-first reject test and a UI that never offers the wrong choice. Discipline.level migrates from free-text to a shared enum through a pure-function backfill map that carries its own unit test. academic fires its second domain event, CourseEnrollmentCreated, still with no subscriber. 162 backend tests green at 95.22% coverage, 163 web tests green at 89.09%, import-linter two contracts kept and zero broken.

White on white: making dark mode a token decision, and the palette that failed its own contrast test

Update 059 min

Update 5 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Updates 1-4 proved tenant isolation, switched it on, built the academic catalog behind an import boundary, and fired the first domain event. Update 5 is the detour before the big feature epics: one web-led sprint that turns the working-but-unstyled scaffold into a real design system. Dark mode becomes a three-layer token decision (primitive to semantic to component); the styling engine the plan had locked (shadcn) gets swapped for daisyUI at the mandatory brainstorm; the chosen Azure palette fails its own WCAG contrast math; and the per-tenant accent ships as a seam with the pipeline deferred on purpose. 144 web tests, 130 backend tests, coverage 92.76% on the browser app and 97% on the API, import-linter still two contracts kept and zero broken.

Shouting into an empty room: offerings, enrollments, and the first domain event I shipped with no subscriber

Update 049 min

Update 4 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Updates 1-3 proved tenant isolation at the database, switched it on for real traffic, and built the academic catalog behind a strict import boundary. Update 4 makes the architecture finally do something: offerings (turmas, a discipline taught in a term to a section) and enrollments, two more tenant-scoped tables with forced RLS, plus the first domain event, EnrollmentCreated, emitted onto the event bus that has sat unused since update 1. The honest twist: the event fires into an empty room. No module subscribes yet (that is a later epic); only a test listens. Which is lucky, because writing this update is how I discovered the emitter does not do what I designed it to do: it fires inside the still-open request transaction, not after commit, and the in-process test that covers it cannot see the difference. PROTECT keeps a class you actually ran from being deleted by accident, limit_choices_to turns out to guard the form and not the API, and update 3's half-built syllabus revisit gets paid off first. 124 backend tests, 80 web tests, import-linter at two contracts kept and zero broken, coverage near 97% on the API and above the gate on the browser app.

Good fences: the academic catalog, and the import boundary that broke once a module used it

Update 039 min

Update 3 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Updates 1 and 2 proved and then switched on tenant isolation with Postgres row-level security. Update 3 builds the first real feature on top of it: the academic catalog (disciplines, syllabi with ordered units, terms with one current at a time), each a new tenant-scoped table with forced RLS, a django-ninja CRUD API, and browser screens driven off the generated OpenAPI client. It is also the first module to live behind the import boundary from update 1, and the boundary broke the moment the module imported anything real. The fix is a one-word import-linter flag, plus a probe test to prove the contract still catches a real violation. Along the way, a route that shadowed itself into a 405, and a syllabus you can create but can't navigate back to, both shipped honestly. 95 backend tests, 62 web tests, import-linter at two contracts kept and zero broken, coverage above the gate on both sides.

Green tests, red browser: turning on tenant isolation for real traffic

Update 028 min

Update 2 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Update 1 proved Postgres row-level security at the database but left the request-path middleware a no-op, so it shipped zero tenant-data endpoints on purpose, behind a written gate. Update 2 closes that gate: the RLS session variable now gets set inside JWT auth, the first tenant-data API (memberships and users) ships behind role guards, and a React + Vite + Tailwind front end drives login, forced password change, and org switching through a client generated off the OpenAPI schema. Hardening the policy turned up a Postgres cast that crashes during planning no matter what guards surround it. Then driving the finished app in a browser turned up two more bugs the green unit suite never saw: switch-org failing closed under its own RLS, and a CORS wall the front end hit on its first request. 66 backend tests, 97.58% coverage, and the isolation gate from update 1 finally closed.

Starting Turmarium: proving one school can't read another's data

Update 017 min

Update 1 of a new build log. Turmarium is a multi-tenant B2B school-management SaaS, built English-first with pt-BR first-class, where the academic core is a stable spine and every future capability plugs in as a module through contracts and domain events, never by importing the core. This first slice is the foundation: a Django 6 + Ninja modular monolith, JWT auth with org/role claims, and tenant isolation proven at the database with Postgres row-level security. 19 tests, coverage 82%, and zero tenant-data endpoints yet, on purpose.