The bottleneck nobody budgets for
Selling software to schools and colleges means selling to a lot of small institutions rather than a few large ones. That’s a fine business — right up to the point where onboarding each one takes a person half a day. Then your growth ceiling is your ops headcount, and it’s an unusually expensive ceiling because the work is boring.
The goal was blunt: an institution signs up, picks a name, and a minute later has a working, branded, isolated instance at their own subdomain. No ticket, no engineer, no waiting.
Constraints that shaped the design
Isolation is a one-shot decision. Shared schema with a tenant column, schema-per-tenant, database-per-tenant — each has a different cost curve, and migrating between them once you have live customers is a project, not a refactor.
Provisioning touches things that fail independently. DNS propagates on its own schedule. Certificate issuance depends on a third party. Schema seeding depends on your own database. A script that runs these in sequence and dies halfway leaves a tenant that exists in some systems and not others — the worst possible state.
Everything ran on infrastructure I also operated. Gunicorn behind Nginx, Supervisor for process control, Celery and Redis for async, cron for scheduling, on AWS. Provisioning had to fit that reality rather than assume a platform that would do it for me.
Decisions and trade-offs
Provisioning as a resumable state machine
The critical reframe. Not “run these eight steps” but “this tenant is in state awaiting_dns; advance it.” Every step is idempotent, every step records its outcome, and the whole thing can be re-driven from any point without duplicating work.
This costs noticeably more to build than a script. It buys the thing that actually matters: a partial failure is a tenant that is behind, not a tenant that is broken. Retry becomes safe, which means retry can be automatic, which means most failures resolve without anyone finding out.
Shared fleet, per-request tenant resolution
No per-tenant deployments, ever. One application fleet, and the host header resolves to a tenant at the start of every request, with tenant scope applied from that point on. The alternative — an app instance per customer — is seductive because it feels isolated, and it means every deploy, every patch and every migration is multiplied by your customer count.
The trade-off is that tenant scoping must be enforced structurally, at the data-access layer, not by remembering to add a filter. One forgotten WHERE tenant_id = ... in a multi-tenant system is a cross-customer data leak. That’s the price of the shared fleet and it has to be paid in architecture, not in code review.
Verify the tenant works before declaring it ready
The final provisioning step wasn’t “mark active” — it was an automated check that actually exercised the new tenant end to end, and only then flipped the registry. It’s a small amount of extra work that converts a class of silent failures into loud ones, before a customer discovers them.
Automate DNS and certificates as part of the same machine
Subdomain creation and certificate issuance were steps in the state machine, not manual prerequisites. This is the piece most “one-click provisioning” implementations quietly omit, and it’s exactly the piece that requires a human at 9am on a Monday.
Boring async infrastructure, used consistently
Celery with Redis for asynchronous work, Supervisor keeping processes alive, cron for scheduled jobs, gunicorn behind Nginx. None of it novel. All of it understood, debuggable at 3am, and applied the same way everywhere so that a new service didn’t come with a new operational vocabulary.
What I’d do differently in 2026
Declaratively. Today I’d express the desired tenant state as data and run a reconciliation loop that drives reality toward it continuously, rather than a state machine that advances once at creation. The state-machine version handles provisioning well but has nothing to say about drift — a tenant whose config was hand-edited months later, or whose certificate quietly failed to renew.
I’d also make the isolation boundary configurable per tenant from the start. The shared-fleet, scoped-data model is right for the long tail of small institutions, but the one large customer who needs a dedicated boundary shouldn’t require a different product.