Tenants
Tenants
Section titled “Tenants”The Tenants panel (/tenants in super-admin) is where the platform operator provisions new customer stores, suspends ones in arrears, restores after payment, and — rarely — deletes.
Access requires the tenant_provisioning license feature (Enterprise tier and above).
What a tenant is
Section titled “What a tenant is”A tenant is one row in the tenants table + every record that filters by its tenantId in middleware. Tenants are isolated at the DB query level — every apps/api route that touches data filters by req.tenantId set in the resolveTenant middleware. See Architecture for the full isolation model.
A tenant has:
- A unique slug (
acme) that becomes the subdomain (acme.mystore.cloud) under SaaS hosting, or the host header (X-Ecommus-Tenant) under B2B integrations - A primary niche (one of
services/real-estate/travel/ecommerce) - A theme assignment (one of the niche’s licensed themes)
- A set of enabled plugins (subset of the customer’s licensed plugins)
- A status:
active/suspended/archived - A subscription pointer (which plan + when it renews)
Lifecycle
Section titled “Lifecycle”[create] ↓active ──────► suspended ──────► active (recoverable) │ │ ↓ ↓archived ◄──── archived (terminal — data retained per GDPR window)Create
Section titled “Create”POST /api/super-admin/tenants — invoked from /tenants/new:
{ "slug": "acme", // unique, lowercase, [a-z0-9-] "name": "Acme Wellness", "niche": "services", "theme": "theme-services", "plan_id": "<uuid>", // from /plans "owner_email": "ana@acme.ro", // initial admin user "send_welcome_email": true}Server-side flow:
- Validate slug uniqueness + niche/theme/plan licence compatibility
- Insert
tenantsrow withstatus='active' - Run
seed-tenant.ts— creates a default storefront homepage, a settings row with sane defaults, the tenant-side admin user (random password, email-reset link) - Subscribe to license-server for the renewal cycle (Stripe customer + subscription created)
- Send welcome email to
owner_emailwith login link + temp password - Audit log:
tenant.create
The first request to the new subdomain takes ~5 s longer than subsequent ones — the resolveTenant cache is cold.
Suspend
Section titled “Suspend”POST /api/super-admin/tenants/:id/suspend — invoked from the tenant detail view.
When a tenant is suspended:
- All
/api/storefront/*routes return503 tenant_suspendedfor that host - Tenant admin login fails with
403 tenant_suspended - Background workers skip jobs where
tenant_id = <suspended> - The license server still considers the install part of the customer’s count for billing — suspension is a hold, not a refund
Common reasons: payment failure, T&C violation, customer request for a planned outage.
Restore
Section titled “Restore”POST /api/super-admin/tenants/:id/restore — flips status back to active. Workers resume on the next tick. Storefront unblocks immediately (no cache invalidation needed — resolveTenant re-reads on every request when the tenant is suspended-flagged).
Archive (terminal)
Section titled “Archive (terminal)”POST /api/super-admin/tenants/:id/archive — moves to archived. Different from suspend:
- Customer data is retained per the GDPR retention window (default 30 days, configurable in
/settings) - The slug is released — another tenant can claim it
- Storefront returns
410 goneinstead of503 - Subscription is cancelled at the license-server (no further charges)
- After the retention window, a cron drops the data permanently
Archive is the customer-facing “delete account” path.
Detail view
Section titled “Detail view”/tenants/:id shows:
- Status + recent transitions (audit-log scoped to this tenant)
- Plan + renewal date + payment status (
active/past_due/canceled) - Theme + enabled plugins
- Counts — products, orders (last 30 d), customers, monthly revenue
- Owner — email, last login, 2FA status
- License — pointer to the JWT issued for this tenant (links to
/licenses/:id) - Custom domain — if
white_labelfeature is on, the operator can mapmystore.ro→ this tenant - Quick actions — suspend, restore, archive, “log in as tenant” (creates a short-lived impersonation token + audit-logs it)
“Log in as tenant”
Section titled ““Log in as tenant””The most powerful action in the panel. Creates a 5-minute impersonation token, logs the operator into the tenant’s apps/admin as a synthetic admin user whose actions are flagged impersonated_by=<operator-id> in every write.
When NOT to use:
- Customer hasn’t asked for support. The audit log makes the access visible — they will see it.
- Modifying customer data without their explicit permission. Read-only support is fine; mutation requires a ticket reference in the impersonation modal.
The impersonation event is also surfaced in the tenant’s own admin audit log — they see it from their side too.
Multi-tenant gotchas
Section titled “Multi-tenant gotchas”- Slug collisions across archive→create: if
acmeis archived and someone tries to create a newacmewithin the retention window, the create fails with409 slug_in_retention. Force release withPOST /api/super-admin/tenants/:id/release-slug(audit-logged). - Cross-tenant query bugs: any new admin route that misses
WHERE tenantId = req.tenantIdis a CLAUDE.md §4 violation. Thetenant-isolation-checkeragent runs on every PR to catch this. Don’t disable it. - Subdomain mode + custom domains: when a tenant has a
white_labelcustom domain,resolveTenantmatches by Host header first; the subdomain still works as a fallback. The custom domain TLS cert is provisioned via Caddy / Let’s Encrypt on the platform-side reverse proxy. - Plan downgrade: moving a tenant from a higher plan to a lower one may strip plugins. The UI flags this before the change; operators see the diff and confirm explicitly. Stripped plugins keep running for the 30-day grace window then deactivate.
See also
Section titled “See also”- Overview — full panel inventory
- Plans — tier matrix + white-label flag
- Licenses — JWT issuance, tied to tenant lifecycle
- Customer Onboarding — playbook the operator runs alongside this panel
- Architecture — multi-tenant isolation model