Consider a B2B SaaS where one customer signs up at night with no human involved, and another is closed by a sales call the next morning. Both need a working tenant, and if your provisioning code treats them as two separate systems, they will drift apart. Your first action: open the endpoint that creates an organization and check whether running it twice with the same request creates two tenants. If it does, fix that before anything else in this post.
In B2B SaaS, the same provisioning function should serve both entry points. Self-serve signups punish slow or failing provisioning with abandonment. Sales-assisted deals punish manual provisioning with engineering hours that belong on the product. The entry points differ, the mechanism should not.
What Does Multi-Tenant Provisioning Actually Create?
Tenant provisioning creates the minimum a new organization needs to start working: an organization record, an admin user linked to it, a role assignment, and whatever isolation your multi-tenant architecture uses.
That isolation model sets the cost. With a shared schema, provisioning is a handful of database writes. With schema-per-tenant you add a migration step, and with database-per-tenant you add infrastructure work that takes noticeably longer. I have not measured these on a common stack, so time your own path. The trade-offs between the models are covered in database-per-tenant vs shared schema.
One rule holds for every model: provisioning must be idempotent. If a network retry runs it twice, the second run returns the first result instead of creating a duplicate. Stripe's API documents the pattern well. Its idempotency docs say the server saves the status code and body of the first request for a given key and returns the same result for repeats, that keys can be up to 255 characters, and that keys may be removed once they are at least 24 hours old. Copy that design for your own endpoint: a client-generated key, a unique constraint, and a stored result.
What Must Be Automatic in Self-Serve Signup?
Self-serve provisioning must finish without a human, and the user should see progress within seconds. Nielsen Norman Group's response time limits, first published in 1993 and updated in 2014, name 10 seconds as the limit for keeping a user's attention on the dialogue, and advise progress indicators beyond it.
Treat that number as a ceiling for the wait, not a target. It is a usability finding about attention, not a measured abandonment rate for signup flows.
The standard sequence:
- The user submits credentials, or an OAuth callback completes.
- The auth handler creates the user record.
- Provisioning creates the organization, links the user as owner, assigns the owner role and writes the billing record.
- The welcome email fires asynchronously, off the critical path.
- The user lands in onboarding.
Steps 1 to 3 block the user, so keep them short. If step 3 needs external calls, such as billing setup or subdomain DNS, run them asynchronously and show a pending state. The onboarding flow that activates starts after this, so it should not wait on slow infrastructure.
Invites and Pending Users
An invitation creates a pending user with the organization ID already attached. When the invitee signs up, provisioning has nothing to do because the organization exists. Put a limit on pending invites per plan tier, because unlimited pending invites on a free trial is an easy route to abuse. That is a design recommendation, not a measured incident.
Domain Capture
Domain capture belongs in the self-serve path after the MVP is stable. An admin registers the company domain, your system issues a DNS TXT record challenge, and once verified, matching signups route to that tenant. DNS propagation delays are the UX trap, so build the happy path first and the retry-and-pending state second.
How Does Sales-Assisted Provisioning Differ From Self-Serve?
Sales-assisted provisioning calls the same function from an internal surface that accepts inputs the public form does not. The mechanism is identical and the configuration differs, as in the table below.
| Field | Self-serve default | Sales-assisted value |
|---|---|---|
| Plan tier | Matches signup form selection | Matches signed contract |
| Seat cap | Plan default | Contracted seat count |
| Trial end date | Calculated from signup timestamp | Contract go-live date |
| Initial admin | The person who typed the email | Named customer contact |
| Custom subdomain | Optional, self-configured | Often pre-configured at deal close |
The AWS guidance on tenant onboarding (September 2023) describes both a self-service path and an internal operations path, and recommends managing the onboarding process through a single mechanism that processes all phases. When the two paths are separate code paths, one usually gets features the other misses, such as domain capture on self-serve but not in the admin tool. That is an inference from the AWS recommendation, not something the article measures.
Does Self-Serve Work for Enterprise Deals?
Self-serve lets an enterprise prospect evaluate the product, but a buying committee usually needs a human at some point. I could not find a primary-source figure I trust for conversion rates by motion, so I am not quoting one. What I can say from the architecture side: give evaluators a working trial tenant, and give your sales team a separate trigger for production tenants, such as an internal admin page or an authenticated API call, so a signed contract does not become an engineering ticket.
A related pricing question is when to add a sales layer at all. The SaaS free trial vs freemium billing post covers the trial side of that decision.
Migrations Belong Behind Your API
Replacement deals often need data migrated from the old tool. Run that migration through your application API with a service credential, not through direct database inserts. Direct writes bypass your validation layer, and a tenant can look correct in the database while the application cannot render its data. Consider a migration of appointment records where foreign keys point at rows that do not exist: the import succeeds, the screens break. This is a hypothetical, and validation at the API is the cheap defence.
What State Machine Does a Trial Tenant Need?
A trial tenant needs at least six states with server-enforced rules: trial, trial_expired, active, past_due, suspended and cancelled. The transitions matter more than the names.
When the trial clock runs out, a scheduled job should move the tenant to trial_expired, block writes on the server through API middleware or database rules, keep reads available, and send the conversion message on the state change. Client route guards are a UI convenience, not enforcement.
The Callidus case study shows this design in a real build. Callidus is a multi-tenant SaaS for UK aesthetic clinics built with React, Firebase and Stripe Connect. It runs a 14-day trial with a 3-day reminder, a 1-day reminder, expiry with a 3-day grace window, and then suspension. During grace, pages stay viewable while mutations are blocked at both the route layer and the Firestore rules. A second scheduled function escalates tenants whose payment failed. Six roles sit on top of that, with JWT claims carrying tenantId and role.
The past_due state comes from billing. Stripe's webhook documentation says a past_due subscription continues to create invoices, that customer.subscription.trial_will_end is sent 3 days before a trial ends, and that you should revoke access when a subscription becomes canceled or unpaid. The grace period before that is your retry window. Stripe's Smart Retries page says the recommended default is 8 tries within 2 weeks, configurable up to 2 months, and that you choose whether the subscription is then cancelled, marked unpaid or left past due. My recommendation: warn in the UI straight away, block writes after the first failed retry, and block reads only at suspension. The dunning and failed payment recovery post goes deeper on the billing side.
What Breaks Most Often
Three failure modes are worth guarding against first: concurrent signups, half-finished provisioning and client-only limits. The order is my judgment, not measured data.
- Concurrent signup requests. A double-click sends two provisioning calls. Without an idempotency key you get two organizations and one orphan. A unique constraint on the key makes the second call return the first result.
- Incomplete provisioning. The function creates the organization, then fails at billing setup. The user retries and hits "account already exists". Either wrap the sequence in a transaction, which is hard with external calls, or run a cleanup job that removes records stuck in an
initializingstate past a cutoff you choose. - Limits enforced only in the UI. A direct API call bypasses a seat or unit cap. The BookBed case study lists a plan of up to twenty units per tenant, and BookBed (Flutter, Firebase and Stripe) is the kind of product where that limit must be checked server-side against the subscription record.
Build Self-Serve First, Then Add the Sales Trigger
Build the idempotent function first, wire the self-serve trigger, and add the sales trigger when a deal needs contract-specific configuration, pre-go-live data migration or SSO setup by the customer's IT admin. Before then, a sales rep can create a trial through the self-serve flow and hand over the credentials. It is manual, and it keeps sales close to the onboarding friction customers feel. The SaaS MVP stack page shows where provisioning sits in the build order.
Does your provisioning endpoint accept an idempotency key today? If not, what would it take to add one this week?



