Tenant Manager
The tenant manager is a console for the people who run the platform — not for workspace users. It lists every workspace (tenant) with its plan, limits and usage, lets you change a workspace's plan and limits, suspend, reactivate or delete it, and shows the background job queues across all workspaces. Before it existed, workspaces could only be managed with SQL on the database server.
Open it at /platform/login on the web app (for example https://crm.example.com/platform/login).
Platform admins
Platform admins are not CRM users. They live in master.platform_admins (master migration m002_platform_admins), outside every workspace, and there is no sign-up for them. Create and manage them from the server with the CLI:
| Task | Command (from apps/api) |
|---|---|
| Create an admin | npm run platform:admin -- create --email you@company.com --name "Your Name" |
| Reset the password | npm run platform:admin -- reset-password --email you@company.com |
| Reset the authenticator (re-enrol at next sign-in) | npm run platform:admin -- reset-mfa --email you@company.com |
| Disable / enable an admin | npm run platform:admin -- disable --email … / enable --email … |
| List admins | npm run platform:admin -- list |
In production use npm run platform:admin:prod inside the api container — see Production Topology → Operational commands. create and reset-password print a generated password once; it is never stored in plain text. Every CLI action is written to the platform activity log.
m002_platform_admins lives in apps/api/src/scripts/master-migrations.ts. In production the app's database user can't create tables in the master schema, so it must be applied as postgres — see Master-schema migrations.
Signing in
- Enter the email and password.
- First sign-in: scan the QR code with an authenticator app (Google Authenticator, Microsoft Authenticator, 1Password…) and enter the 6-digit code. Afterwards: enter the code from the app.
Details:
- Codes are standard TOTP (RFC 6238). A code that was already used can't be used again.
- A session lasts 4 hours with no refresh; sign in again after that.
- Platform tokens are signed with their own key (
PLATFORM_JWT_SECRET, or a key derived fromJWT_SECRET) and carry their own token type, so a workspace token never opens a platform route and a platform token never opens a workspace route. - In the browser the platform session is kept per tab and never touches the workspace session, so you can be signed in to a workspace and to the tenant manager at the same time.
- Sign-in endpoints are rate limited. Every sign-in, failed sign-in and wrong code is recorded in the activity log.
Workspaces
Workspaces lists every workspace with its status, plan and usage against its limits (users, records, storage, API calls today). Search by name or slug. Click a workspace to open it.
The workspace page shows:
- Plan and limits — choose a plan; for each limit (users, records, storage MB, API calls per day, import rows) the plan's value, an optional override and the effective value. Leave an override blank to use the plan's value. Limits are measured, not enforced yet.
- Daily usage (last 30 days) — from the nightly usage snapshot (00:10 UTC).
- Activity — the tenant-manager actions taken on this workspace.
- Suspend / Reactivate workspace. Suspending signs every user out at their next request, blocks sign-in and makes scheduled jobs skip the workspace. Its data is kept.
- Delete workspace — only for a suspended workspace, after typing its slug to confirm. The workspace row and its database schema are removed in one transaction; stored files can optionally be removed too. This can't be undone.
Background jobs
Background jobs (/platform/jobs) shows the queued work across all workspaces and refreshes every 15 seconds. There is a card per queue with the number of live workers and the jobs waiting, running, retrying and failed:
| Queue | Holds | Retry from here |
|---|---|---|
Scheduled jobs (tenant-jobs) | Per-workspace scheduled work: SLA scans, reminders, mailbox and calendar sync, reports… | Yes |
Notification delivery (notification-delivery) | Notification emails, SMS, WhatsApp and chat messages | Yes |
Workflow runs (workflow-runs) | Workflow executions | No — retry from the failed step on the workflow's Runs tab in the workspace |
| Lead / account / opportunity / product imports | Import files | No — run the import again in the workspace |
| Account enrichment | Company data lookups | No — run enrichment again on the account |
Open a queue to list its jobs by state, optionally for one workspace. For failed jobs you can retry one or retry all failed (where retry is allowed) and discard a job so it never runs again. Workflow runs and imports can't be retried here because repeating them blindly could create duplicate records.
Job payloads are never shown — they can hold message bodies and personal data. Only the workspace, job type, attempts, timings and error are listed. Every retry and discard is recorded in the activity log.
When no worker is running, jobs pile up under waiting; see Process roles.
Activity
Activity is the platform audit log: sign-ins and failures, plan / limit / status changes (with the before and after values), deletions, CLI actions on platform admins, and job retries and discards. It is append-only for the application's database user.
API
All routes are under /api/platform and require a platform token (PlatformAuthGuard), except sign-in.
| Method | Path | Purpose |
|---|---|---|
| POST | /platform/auth/login | Email + password → { step: 'enroll', otpauthUrl, mfaToken } the first time, then { step: 'verify', mfaToken } |
| POST | /platform/auth/mfa | mfaToken + 6-digit code → { accessToken } |
| GET | /platform/me | The signed-in platform admin |
| GET | /platform/plans | Plans and their limits |
| GET | /platform/tenants · /platform/tenants/:id | Workspaces with plan, limits and usage; one workspace with daily usage and activity |
| PATCH | /platform/tenants/:id | { plan?, status?: 'active' | 'suspended', limitsOverride? } — a null override removes it |
| DELETE | /platform/tenants/:id | { confirmSlug, deleteFiles? } — suspended workspaces only |
| GET | /platform/jobs · /platform/jobs/:queue | Queue overview; jobs in one state (?state=failed&workspace=&limit=50 — states waiting, active, delayed, failed, completed) |
| POST | /platform/jobs/:queue/retry-failed · /platform/jobs/:queue/:id/retry | Retry failed jobs (scheduled jobs and notification delivery only) |
| DELETE | /platform/jobs/:queue/:id | Discard a failed job |
| GET | /platform/audit | Activity log |
Code: apps/api/src/modules/platform/ (API) and apps/web/src/features/platform/ (screens). Suspension is enforced in JwtStrategy, which now also requires the workspace to be active — this covers deleted workspaces too.