Tenant Provisioning System Design at Seed Stage

Tenant provisioning is the sequence of operations that runs between "user clicks sign up" and "user has a fully functional account." At pre-seed, this might be three database inserts and a welcome email. At seed stage, it expands: Stripe customer creation, default workspace initialization, onboarding state setup, billing plan assignment, welcome email with dynamic content, and audit log entry — all of which must succeed atomically or be reliably rolled back.

This guide covers the provisioning system architecture that seed-stage teams need: a reliable sequence, idempotent steps, rollback handling, and an observable provisioning status.

📐 The Provisioning Sequence

Model tenant provisioning as an ordered sequence of idempotent steps. Each step either succeeds fully, fails cleanly with a recoverable error, or is skipped because it already ran. This model makes provisioning safe to retry and easy to resume from a failed step.

StepOperationRequiredRollback
1Create user record in auth systemYesDelete user if subsequent steps fail
2Create account/tenant record in DBYesDelete account record
3Create Stripe customerYesDelete Stripe customer; store ID on account
4Assign default plan / trialYesNo subscription created = no rollback needed
5Initialize default workspaceYesDelete workspace record
6Write audit log entryYesNo rollback; audit entries are immutable
7Send welcome emailNoNo rollback; idempotency via email provider dedup key
8Trigger onboarding checklist stateNoDelete checklist state record

🔧 Idempotent Provisioning Steps

Each step must be safe to run multiple times. If provisioning fails at step 5 and is retried, steps 1–4 must not create duplicate records or charge the user twice.

Idempotency patterns per step type:

📊 Provisioning Status Tracking

At seed stage, you will have provisioning failures. Without status tracking, you have no way to identify which accounts are in a broken state or retry the failed step. Add a provisioning status to every account record.

ALTER TABLE accounts ADD COLUMN provisioning_status TEXT NOT NULL DEFAULT 'pending';
-- Values: 'pending' | 'in_progress' | 'completed' | 'failed'
ALTER TABLE accounts ADD COLUMN provisioning_step TEXT;
-- Last completed step name for resumable retry
ALTER TABLE accounts ADD COLUMN provisioning_error TEXT;
-- Error message from the failed step

Update status and step after each successful operation. When provisioning completes, set provisioning_status = 'completed'. When it fails, set provisioning_status = 'failed' and store the error. A background job or admin tool can then query for status = 'failed' and retry from the last successful step.

🔄 Handling Rollbacks

Full automatic rollback is complex to implement correctly. At seed stage, a pragmatic approach: complete forward-pass provisioning with retries for transient failures, and handle genuine failures with an alert and manual or semi-automated repair.

What to automate: Retry logic for transient errors (network timeouts, rate limits) with exponential backoff up to 3 attempts per step.

What to handle manually: Partial provisioning states where the user record exists but the Stripe customer or workspace does not. These are rare but require review to avoid creating duplicate Stripe customers.

Build an internal admin endpoint that shows all accounts in provisioning_status = 'failed' and allows one-click retry from the failed step. This tool is worth more than an automated rollback system at seed stage.

What to Do Next

If your provisioning is a single synchronous function with no status tracking: add the provisioning_status and provisioning_step columns this week and start logging step completion. If you have provisioning failures in production that you cannot identify: build the admin endpoint to query failed accounts before fixing the failure cause — visibility comes first. If you are pre-product: design the sequence table above before writing a line of provisioning code; retrofitting idempotency is significantly harder than building it in.