A customer signs up. Or your sales rep closes a deal. Either way, something has to happen in your database. That something is tenant provisioning, and in B2B SaaS, getting it wrong costs you in two different ways depending on how the customer arrived.
Self-serve signups punish slow provisioning with abandonment. Sales-assisted deals punish manual provisioning with engineering hours that belong on the product. Different entry points, same root problem. Here is how to design a provisioning system that handles both cleanly: same core mechanism under the hood, different triggers on top — and what breaks when you treat them as separate systems.
What Does Tenant Provisioning Actually Create?

Tenant provisioning creates the minimum resources a new organization needs to start using your product. At minimum: an organization record, an admin user record linked to it, a role assignment, and whatever data isolation your multi-tenant architecture requires — a row-level tenant ID, a separate schema, or a separate database.
The exact scope depends on your isolation model. Shared schema: provisioning is a handful of database writes and completes in milliseconds. Schema-per-tenant: add a migration step. Database-per-tenant: add infrastructure provisioning — realistically 5-30 seconds depending on your stack. That gap matters because self-serve users are watching a spinner.
One rule applies to every isolation model: your provisioning path must be idempotent. If it runs twice from a network retry, it produces the same result. Not a duplicate tenant. This is non-negotiable and almost always under-specified on the first pass.
In B2B products, the provisioning surface is your first impression. A tenant that takes 30 seconds to create, or errors on the second signup attempt, sets a tone that's hard to recover from during an evaluation window.
Self-Serve Signup: What Has to Be Automatic?

Self-serve provisioning must complete with no human involvement, start to finish, in under 10 seconds. That is the ceiling above which users abandon and you have no recovery path.
The standard flow:
- User submits email + password (or OAuth callback completes)
- Auth handler creates the user record
- Provisioning creates the org, links the user as owner, assigns the owner role, writes the billing record
- Welcome email fires async — off the critical path
- User lands on the onboarding flow
Every synchronous step is user-blocking. Steps 1-3 need to complete in low hundreds of milliseconds. If step 3 involves external API calls (billing setup, subdomain DNS), run those async with a follow-up state check — or accept the latency and load-test before you open signups.
The invite model (where the admin brings teammates in) is a surface on top of provisioning, not part of it. An invitation creates a pending user record with the org ID already baked in; when the invitee signs up, provisioning is a no-op because the org already exists. Set a hard limit on pending invites per plan tier. Unlimited pending invites on a free trial is a spam vector you will discover at the worst moment.
Domain capture belongs in the self-serve path too, but typically added after the MVP is stable. The mechanic: an admin registers their company domain, your system issues a DNS TXT record challenge, verification marks the domain as owned, and any subsequent matching signup is routed to that tenant without showing a password field. DNS propagation is the UX trap — it can take anywhere from seconds to 48 hours, and users expect instant. Build the happy path first. Build the retry-and-pending state second. In that order.
Does Self-Serve Signup Work for Enterprise Deals?

Self-serve lets enterprise prospects evaluate your product, but it rarely closes enterprise deals without human involvement at some point. The gap is trust and buying-committee coordination, not provisioning speed.
A 2025 analysis of B2B SaaS deal data found that enterprise demos compress evaluation cycles from 40+ days to 3-6 days, while pure self-serve trial-to-paid conversion at enterprise ACV runs 10-15%. Demos-first deals close at 55-75%. At $10K ACV, demo-acquired cohorts produce roughly $1,900 more lifetime value per customer than trial cohorts, and their annual churn rate (3.5%) is less than half of trial-acquired customers (7.5%). That is not a product quality gap. It's a coordination problem no onboarding wizard eliminates.
What this means for provisioning design: the self-serve path still needs to work for enterprise prospects during evaluation — give them a functional trial tenant. But your sales team needs a separate trigger for production tenant creation: an internal admin portal or a direct API call, so that when the contract is signed, sales can provision the environment immediately without filing an engineering ticket.
Let me back up. The 36.3% of B2B SaaS companies that report zero self-serve revenue despite investing in product-led growth (same analysis) mostly share this failure mode: they built self-serve for SMB evaluation but never built the sales-triggered provisioning path. Enterprise deals stall waiting for environments, or sales manually creates accounts through the self-serve flow — which works until the trial clock runs out mid-negotiation on a $50K contract.
How Sales-Assisted Provisioning Differs
Sales-assisted provisioning calls the same provisioning function, but from an internal surface with additional inputs the public signup form does not capture. The mechanism is identical. The configuration is not.
| 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 data migration case is a separate workflow that provisioning must precede. Almost every enterprise replacement deal involves migrating data from the old tool. That migration writes into a correctly provisioned tenant, and critically, it should flow through your application API with a service role token, not via direct database inserts. Direct writes bypass your validation layer. Tuesday afternoon at a client's go-live: a direct-write migration that skipped validation introduced 3,200 appointment records with malformed foreign key references. The tenant looked correct in the database. The application couldn't render any of it. Fix was a three-hour emergency migration rerun, with the client watching. Your application API has validation for a reason: use it, even for migrations.
Trial Tenant Lifecycle: What State Machine Do You Need?
You need at minimum six states, and getting the transitions wrong is the most common provisioning gap I see in production codebases.
States: trial → trial_expired → active → past_due → suspended → cancelled.
When the trial clock hits zero (a nightly background job against trial tenant records past their expiry timestamp):
- Block writes immediately, server-side: via API middleware or Firestore rules, not client-side route guards. Client guards are UI convenience, not enforcement.
- Keep reads available: users who got value during the trial need to see their data to understand what they'd lose. That visibility is your conversion mechanism.
- Fire the conversion sequence: trial expiry is the highest-signal moment in the funnel. Trigger the email on the state change, not on the next marketing batch run.
The past_due state arrives differently — via Stripe webhook when a subscription payment fails. Stripe's Smart Retries default runs 4-8 days depending on billing interval, which is your grace period before you have to decide what to block. I prefer: warn in the UI immediately, block writes on first retry failure, block reads at suspension. Enterprise customers will escalate a billing failure to finance if given time. Give them the time.
On Callidus, a multi-tenant clinic SaaS for UK aesthetic practices built in 10 weeks on React, TypeScript, and Firebase, this state machine drove the full access control layer. Six roles (super admin, owner, admin, manager, practitioner, receptionist), each with distinct write permissions. Billing state was a multiplier: past-due tenants had all write operations blocked at the Firestore rules layer regardless of role. Client-side showed warning banners. Rules enforced the block. No edge case where a practitioner created a patient record during a billing failure by manually clearing the URL.
What Breaks Most Often
Three failure modes, in order of frequency.
Race conditions on concurrent signup requests. A user double-clicks submit on a slow connection. Two concurrent provisioning requests hit your endpoint. No idempotency key. Two org records created for one user, one orphaned with no user attached. The user retries signup and hits "account already exists" for an account they cannot access. The fix: a unique constraint on the provisioning call's idempotency key, making the second concurrent call return the first call's result rather than creating a duplicate. Two lines of code. One production incident prevented.
Incomplete provisioning records with no cleanup. Your provisioning function creates the org record, then fails at step 3 (billing setup call to Stripe). The database has an org with no billing config. The user retries signup and hits "account already exists." Either wrap the full provisioning sequence in a transaction (hard when calling external APIs mid-flow) or run a background cleanup job that detects records in an initializing state older than 30 minutes and removes them. Pick one and ship it before opening public signups.
Seat caps enforced only on the client. You've felt this: a developer calls the invite API directly, bypassing the UI enforcement, and seats exceed the plan tier. Revenue leak at minimum, compliance breach if the contract caps usage. Seat cap enforcement belongs at the API layer, checking against the subscription record in your billing system. BookBed, a property management SaaS built on Flutter and Firebase, had this exact gap in the invite-a-property-manager flow during the build. The UI had the cap. The API didn't. A code review caught it before production. The API check shipped the same day.
Build Self-Serve First
Self-serve provisioning comes first because retrofitting it onto a manually-provisioned system is harder than adding a sales admin portal to a self-serve one. The provisioning function is the same. The caller is different.
The inflection point where a formal sales-assisted flow pays its own way: ACV exceeds $5K-$10K (Refiner's analysis puts the inside-sales LTV floor here) and you are running active demos, you have a signed enterprise deal requiring data migration before go-live, or an IT admin needs to configure SSO before their team can access the product.
Before that inflection point, sales-assisted provisioning can literally be the sales rep creating a trial account through the self-serve flow and forwarding the credentials to the customer champion. Manual. Slightly embarrassing. But it uses the same system, and it means sales feels the onboarding friction customers feel.
AWS's SaaS architecture guidance describes both self-serve and admin-managed paths funneling into the same central provisioning orchestration. When they're separate code paths, they drift: self-serve gets domain capture, admin portal doesn't. Six months later you have two classes of tenant configuration in production depending on how a customer arrived, and debugging becomes archaeology.
One provisioning function. Two callers. The SaaS MVP stack sequencing question for provisioning: build the idempotent function first, wire up the self-serve trigger, add the sales trigger when deal size demands it.
If you have not yet added an idempotency key to your provisioning endpoint, that is the next task. What's blocking you from shipping that this week?
