Skip to content
Software architecture6 min read

Self-service tenant provisioning that does not need an engineer

A product where onboarding a customer requires manual database work is not yet a product — it is a service with a login page. Provisioning is the boundary: everything a new tenant needs must be created automatically, atomically enough to be safe, and recoverably enough that a half-finished signup is not a support ticket.

Part ofSaaS & PaaS Product Engineering

The first few customers of any SaaS product get set up by hand, and that is fine. The problem is that manual provisioning scales linearly with customers while revenue per customer does not, and it quietly caps how fast the business can grow — not because demand is missing, but because someone has to run a script.

Everything a tenant actually needs

Provisioning is easy to under-scope because the obvious part — insert a row in the tenants table — is the smallest part. The full list is longer than it looks, and each missing item becomes a bug that only appears for brand-new customers.

CategoryCreated at provisioning
IdentityTenant record, first admin user, role assignments, invitation tokens
ConfigurationDefault settings, feature flags for the chosen plan, locale and timezone
BillingCustomer record at the payment provider, subscription, trial end date
Reference dataDefault categories, statuses, templates — whatever the product is unusable without
IsolationSchema or row-scope depending on the tenancy model, storage prefix, search index scope
ObservabilityTenant registered in logging and metrics dimensions so support can find them

The failure to design for is partial provisioning

Provisioning touches your database, your payment provider, possibly an email service and an object store. Those cannot participate in a single transaction. So the real question is not how to make provisioning atomic — it is what happens when step four of seven fails.

  1. Do all local database work inside one transaction, so the tenant either exists completely or not at all.
  2. Perform external calls after that transaction commits, driven by a durable record of work still to do.
  3. Make every external step idempotent, keyed on the tenant id, so a retry cannot create two billing customers.
  4. Track provisioning state on the tenant record itself, so a half-finished tenant is queryable rather than invisible.
  5. Let the tenant into the product as soon as the local state is complete — do not block first login on a billing API.
sql
-- Provisioning state lives on the tenant, so a stuck signup is findable.
ALTER TABLE tenants ADD COLUMN provisioning_state text NOT NULL DEFAULT 'pending';
-- 'pending' | 'core_ready' | 'complete' | 'failed'

ALTER TABLE tenants ADD COLUMN provisioning_error text;
ALTER TABLE tenants ADD COLUMN provisioned_at timestamptz;

-- The query support actually needs at 9am on a Monday.
SELECT id, name, provisioning_state, provisioning_error, created_at
FROM   tenants
WHERE  provisioning_state <> 'complete'
  AND  created_at < now() - INTERVAL '10 minutes'
ORDER  BY created_at;

That last query is the whole point. Without an explicit state column, a tenant whose billing setup failed looks identical to a healthy one until the customer complains.

Idempotency across external systems

Every external call needs a key derived from the tenant, not from the attempt. Payment providers support this directly; for anything that does not, check-then-create guarded by a local record of what you already did.

ts
// The key is derived from the tenant, so every retry collapses onto
// the same provider-side object instead of creating duplicates.
await stripe.customers.create(
  { name: tenant.name, metadata: { tenant_id: tenant.id } },
  { idempotencyKey: `tenant-customer-${tenant.id}` },
);

// Record what succeeded before attempting the next step, so a crash
// between steps resumes rather than restarts.
await db.tenants.update(tenant.id, { provisioning_state: "core_ready" });

Trials, plans and the entitlement boundary

Provisioning is where entitlements are first applied, and the design decision that matters is whether feature access is read from the plan or copied onto the tenant. Copying is tempting and wrong: a plan change then has to find and rewrite every copy.

  • Store the tenant's plan; derive entitlements from the plan at read time
  • Allow per-tenant overrides as an explicit, auditable exception, not as the normal mechanism
  • Record trial end as a timestamp on the tenant, and treat expiry as a state transition with its own handling
  • Decide what an expired trial can still do — read-only access and data export is usually kinder, and better for winbacks, than a hard lockout

The half nobody builds

Provisioning gets built because signups need it. Deprovisioning gets skipped because churn is unwelcome — and then a departing customer's deletion request arrives and there is no path for it.

StageWhat happens
Subscription endsAccess restricted; data retained; export remains available
Grace periodDefined window — commonly 30 days — during which reactivation restores everything
Data exportTenant can retrieve their own data without contacting support
DeletionData removed across database, object storage, search indexes, caches and backups per policy
Provider cleanupBilling customer and subscription closed at the payment provider

The metric that tells you it works

One number: engineering minutes required to onboard one new tenant. If it is not zero, provisioning is not finished. Track it, and treat any manual intervention as a defect with a root cause rather than as routine operational work.

Questions

Should a new tenant be able to use the product before provisioning completes?

Yes, as soon as the local database state is complete. Blocking first login on an external billing call means a provider outage becomes a signup outage. Let them in once the tenant, admin user and defaults exist, and finish billing setup in the background — with monitoring that catches it if that background work fails.

How do you provision when each tenant gets its own schema or database?

The same sequence applies, with the schema or database creation as an additional step that must be idempotent and recorded. The important difference is that it is slower and more failure-prone, so it belongs in background work with explicit state rather than inside the signup request — and your migration tooling must then handle a tenant that exists but is mid-provision.

What is the most common provisioning bug?

Missing reference data. The tenant record, user and billing all succeed, so provisioning reports success, but the product requires a default category or template that was never seeded. The new customer's first session dead-ends and they conclude the product is broken — which, for them, it is.

Want this applied to your systems?

Most of what is described here begins as an assessment: a fixed-scope review producing a prioritised written report you can act on with any supplier.

Prefer email? hello@kornspire.com