Admin: Workspaces
/admin/workspaces lists every workspace (organization) and hosts the admin write surface: suspend and reactivate. It reads app-owned organization_profiles joined with Better Auth organization data.
/admin/workspaces lists every workspace (organization) and hosts the admin
write surface: suspend and reactivate. It reads app-owned
organization_profiles joined with Better Auth organization data.
Using it
- Paginated list with a status filter (all / active / suspended / deleted).
- Manage opens a drawer with the workspace's owner, type, active plans, and members, plus the status action.
Suspend / reactivate
These are the only admin writes. They flip
organization_profiles.status between active and suspended. Product
permission checks already treat non-active organizations as unavailable, so
suspending a workspace immediately blocks its members from using it.
Each write requires two independent checks, in order:
- Super-admin access (
resolveSuperAdminAccess). A non-admin attempt is rejected and recorded asadmin.access_denied. The mutation returns a structured{ ok: false, code: "FORBIDDEN" }(rather than throwing) so that denial audit commits — a throwing mutation would roll the audit row back. - Sensitive-action step-up bound to the target workspace. The confirm dialog runs the existing verification ceremony (password or email code); the step-up grant is single-use and level 4, so one verification maps to exactly one write. See Sensitive Action Protection.
Every attempt produces an audit event: organization.suspended /
organization.reactivated with result = success or failure.
Deleted workspaces cannot be suspended or reactivated.
Why step-up is bound to the target workspace
A platform admin is usually not a member of the workspace they act on. The sensitive-action grant is therefore bound to the target organization id (passed by the confirm dialog), not the admin's active workspace. This forces a fresh verification per target and keeps the grant scope aligned with enforcement.
Backend
admin.organizations.list / .get / .suspend / .reactivate in
convex/admin/organizations.ts.
The sensitive actions admin.organizationSuspend /
admin.organizationReactivate are defined in
convex/security/sensitiveActions.ts
with requiresSuperAdmin: true.
Extending the write surface
Keep new admin writes small and explainable. For each one: add a sensitive
action id (with requiresSuperAdmin: true), gate the mutation with
requireSuperAdminInMutation then requireSensitiveActionInMutation, and emit
audit events for success/failure. Avoid impersonation, account deletion, and
arbitrary billing edits.
Removing it
Admin: Users
/admin/users lets a super admin answer "who is this user, and which workspaces do they belong to?" It reads the app-owned users table (kept in sync with Better Auth by the auth triggers), so no membership-scoped API is needed.
Admin: Billing and Grants
The admin panel surfaces just enough billing context to answer basic support questions without opening your provider's dashboard. It does not edit billing — for refunds, plan changes, or customer edits, use the provider dashboard (Polar/Stripe/Lemon Squeezy) or the Convex dashboard.