What actually happens when somebody signs up
On paper it is one action. In practice, approving a tenant means creating a schema, running every migration against it, seeding catalogues and defaults, writing the subscription and its entitlements, applying branding, creating the first administrator, sending an invitation, and recording that all of it happened.
That is eight things, across at least three systems, any of which can fail. The interesting question is not how to make them succeed. It is what the system looks like when number six fails at three in the morning.
The version everybody builds first
The first version is a request handler. It does the eight things in order, wrapped in whatever transaction it can reach, and returns success.
When step six fails, you get a tenant that exists in the database, has a schema, has no administrator, and cannot be signed into. The customer has been told they are approved. Nobody on your side knows the state is broken until they write in. The fix is an engineer with a database console and a good memory.
And the retry is worse than the failure. Run it again and you have two subscription rows, or a second invitation, or a migration applied twice.
Making it durable
We rebuilt provisioning as a saga. Each step is recorded before it runs and marked when it completes. The whole thing is resumable, and every step is idempotent: running it twice produces the same state as running it once.
That changes the failure conversation entirely. Step six failing at three in the morning is now a row that says step six failed at three in the morning. The run resumes from there, either automatically or when an operator presses a button, and it does not redo the five steps that already landed.
Two axes matter here. What the tenant is (their lifecycle state) and how far provisioning has got (the run state) are different facts, and conflating them is how you end up with a tenant that looks active and is not usable.
Idempotency is the real work
Most of the effort is not the state machine. It is making each step safe to repeat.
Creating a schema becomes create if absent. Seeding becomes upsert against a stable key. Sending an invitation becomes send unless one is already outstanding, which means invitations need identity of their own rather than being a side effect. Every one of those is a small design decision that only looks necessary once you have accepted that steps will be retried.
What it gives back
Operators stop being a recovery mechanism. When something stalls, the platform surface shows exactly which step, with the error, and a resume that is safe to press.
Support conversations change shape. Instead of asking a customer to describe symptoms, you look at the run.
And the customer experience holds. Onboarding either completes or resumes. It does not silently strand somebody in a half created workspace with a login that does not work.
The wider point
Any process that crosses more than two systems and gets triggered by a human decision deserves this treatment. Approvals. Cancellations. Bulk imports. Anywhere the middle of the process is a real place a system can be, the middle needs to be a state you can see and act on, not an accident.
