All documents

MERIDIAN · DOC 07 / 25

Tenant Management

Atomic provisioning, slug rules, lifecycle matrix, list querying, password reset, and admin console.

IMPLEMENTED IN PHASE 4. Everything the Platform Admin can do with tenants, and the guarantees the system makes while doing it. Tenant Admins have no tenant-management capability — the backend rejects them (403 FORBIDDEN) before any handler logic runs.


1 · The tenant concept

A tenant is one logistics company: identity (companyName, slug, contact) and lifecycle status in tenants; presentation config in website_configs; operations team of exactly one TENANT_ADMIN user. Nothing else lives on the tenant document (locked separation).

2 · Lifecycle

Exactly three statuses, with a drawn transition matrix:

ACTIVE ⇄ SUSPENDED
ACTIVE  → ARCHIVED
ARCHIVED→ ACTIVE
TransitionEndpointEffect
ACTIVE → SUSPENDEDsuspendlogin/API blocked, live sessions destroyed, data kept
SUSPENDED → ACTIVErestorestatus change only — nothing recreated
ACTIVE → ARCHIVEDarchivelike suspend + hidden from normal lists, data kept
ARCHIVED → ACTIVErestorestatus change only
anything else400 INVALID_TENANT_STATUS with the allowed set

There is no hard-delete operation anywhere in the platform.

Behavior when suspended/archived: public site + tracking disabled (the Phase 4 suspension page and Phase 5 tracking gate read the same status), tenant-admin login/API blocked (Phase 3 login gate; 403 TENANT_SUSPENDED per request), platform admin unaffected. Archived tenants are excluded from the default ALL list filter and surface under the dedicated ARCHIVED filter.

3 · Creation (atomic provisioning)

POST /api/v1/platform/tenants
  → zod boundary (createTenantWithAdminSchema)
  → slug: normalize → pattern → reserved-list → uniqueness pre-check
  → admin email uniqueness pre-check
  → password: provided or server-generated temporary (policy-checked)
  → Argon2id hash
  → TRANSACTION:
        1. tenants.insert            (status ACTIVE)
        2. users.insert              (TENANT_ADMIN, tenantId, ACTIVE)
        3. website_configs.insert    (default config + company contact)
  → 201 { tenant, admin, temporaryPassword? }

Failures roll back everything. On replica-set/Atlas deployments this is a real MongoDB transaction (withTransaction). On a standalone mongod — which does not support transactions — the service falls back to sequential creation with explicit compensating deletes (documented in src/server/services/tenant.service.ts); the invariant is identical: no tenant without its admin, no tenant without its WebsiteConfig, no orphans of either kind. The transaction test suite proves rollback (tests/tenant.service.test.ts).

Local-dev requirement for the transactional path: run mongod as a single-node replica set (mongod --replSet rs0 then rs.initiate()) or use Atlas; without it the service logs and uses the rollback fallback.

Password handling

Temporary passwords are generated with crypto-randomness in xxxx-xxxx-xxxx form, shown exactly once in the creation response (only when generated), never stored, never logged, never retrievable. The admin's stored value is always the Argon2id hash.

4 · Tenant admin — the one-admin invariant

Each tenant has exactly one TENANT_ADMIN user, enforced twice:

  1. Application: provisioning is the only creation path.
  2. Database: the partial unique index { tenantId: 1 } where role = TENANT_ADMIN (Phase 2) rejects duplicates even under races; the service maps 11000 on that index to 409 TENANT_ADMIN_ALREADY_EXISTS.

There is no staff/team/invitation model.

5 · WebsiteConfig initialization

Created atomically with the tenant: defaults from the model (hero + tracking sections enabled, services/about disabled, empty navigation and social links) plus the company contact carried into the website contact. One config per tenant is enforced by the unique tenantId index (test-verified). Branding/customization is Platform-Admin work in Phases 7/9 — the management surface here only verifies websiteConfigured.

6 · Slug rules

No DNS configuration and no custom domains happen in V1 (locked).

7 · Listing: search · filters · sort · pagination

GET /api/v1/platform/tenants — single aggregation pipeline:

ParameterValuesDefault
searchfree text — company name, slug, admin name, admin email
statusALL (ACTIVE+SUSPENDED) · ACTIVE · SUSPENDED · ARCHIVEDALL
sortnewest · oldest · most_packages · company_namenewest
page≥ 11
limit1–50 (server clamps)20

most_packages computes live from the packages collection in the same pipeline. Response: { items: TenantListItem[], page, limit, total, totalPages } where each item carries company, slug, status, admin summary, package count, and timestamps — never hashes/sessions/internals.

8 · Endpoints (all PLATFORM_ADMIN-gated)

MethodPathPurpose
GET/api/v1/platform/tenantslist/search/filter/sort/paginate
POST/api/v1/platform/tenantsatomic provisioning (201)
GET/api/v1/platform/tenants/:tenantIddetails (identity + admin + counts)
PATCH/api/v1/platform/tenants/:tenantIdedit company info / slug
POST/api/v1/platform/tenants/:tenantId/suspendACTIVE → SUSPENDED
POST/api/v1/platform/tenants/:tenantId/archiveACTIVE → ARCHIVED
POST/api/v1/platform/tenants/:tenantId/restoreSUSPENDED/ARCHIVED → ACTIVE
POST/api/v1/platform/tenants/:tenantId/admin/reset-passwordreset admin password (one-time reveal)

Full request/response/error detail: docs/api.md §platform.

9 · Password reset

Generates a new temporary password → Argon2id hash → replaces passwordHashdestroys every session of that admin. The old password is unretrievable by design; the new one is returned exactly once. The platform admin never sees any existing password.

10 · Platform Admin cross-tenant access

Privileged but never invisible: every platform endpoint runs behind the explicit PLATFORM_ADMIN gate, mutations are pino-logged (tenantId, action, session invalidations), and there is no if admin → skip checks" path. An "open tenant dashboard" flow for Phase 8 will ride on the same server-authorized mechanism — no client-side impersonation, no browser-set tenantId`.

11 · Tenant isolation (unchanged)

This feature set is platform-scoped and therefore cross-tenant by intention. Nothing here leaks into tenant-scoped surfaces: tenant admins keep 403 on every /platform route, and Phase 5 services keep deriving tenantId exclusively from the session.

12 · Error handling

Mapped codes in use: TENANT_NOT_FOUND (404) · TENANT_SLUG_ALREADY_EXISTS (409) · TENANT_SLUG_RESERVED (400) · TENANT_ADMIN_ALREADY_EXISTS (409) · EMAIL_ALREADY_EXISTS (409) · INVALID_TENANT_STATUS (400) · plus the standard VALIDATION_ERROR, FORBIDDEN, UNAUTHORIZED, RATE_LIMITED, INTERNAL_ERROR envelopes. Mongo duplicate-key errors (11000) are translated, never exposed.

12b · Phase 8 additions

The console gained a full tenant detail page (Overview/Website/Branding/ Admin/Packages/Settings), platform overview metrics, website + branding editors, authenticated preview, and a server-authorized read-only view of a tenant's packages. Lifecycle rules, slug rules, the one-admin invariant, provisioning atomicity, and "no hard delete" are unchanged — see docs/platform-admin.md and docs/website-configuration.md.

13 · Admin console (this phase's UI)

/admin — SSR-guarded platform console (wrong role → /login redirect; the API remains the boundary): overview stats, tenant manager (search/filter/sort/pagination/lifecycle/details/reset), the 3-step provisioning wizard with one-time credential display, confirm-pattern danger buttons. Desktop-first, responsive, keyboard-usable.

14 · Future extension points (not built)

Tenant details "Website/Branding" tabs (Phase 7/9 consume website_configs directly) · packages tab (Phase 8 reads the same packageCount pipeline stage) · tenant-scoped "open dashboard" flow (server-authorized, Phase 8) · audit log (deferred per scope).