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.
| Step | Operation | Required | Rollback |
|---|---|---|---|
| 1 | Create user record in auth system | Yes | Delete user if subsequent steps fail |
| 2 | Create account/tenant record in DB | Yes | Delete account record |
| 3 | Create Stripe customer | Yes | Delete Stripe customer; store ID on account |
| 4 | Assign default plan / trial | Yes | No subscription created = no rollback needed |
| 5 | Initialize default workspace | Yes | Delete workspace record |
| 6 | Write audit log entry | Yes | No rollback; audit entries are immutable |
| 7 | Send welcome email | No | No rollback; idempotency via email provider dedup key |
| 8 | Trigger onboarding checklist state | No | Delete 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:
- → Database inserts: Use
INSERT ... ON CONFLICT DO NOTHINGwith a unique constraint on the tenant identifier. If the record exists, the step is a no-op. - → Stripe customer creation: Store the Stripe customer ID on your account record immediately after creation. Before creating, check if
stripe_customer_idis already set; if yes, skip creation. - → Email sending: Pass a deterministic idempotency key to your email provider (e.g.,
welcome-{account_id}). The provider deduplicates on its side. - → External API calls: Check for an existing resource before creating. Most APIs support a
GETby external identifier beforePOST.
📊 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 stepUpdate 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.