02 — User Personas (Organizations Module)
- P1 — Super Admin (platform)
- P2 — Org Admin
- P3 — Invited Admin
- P4 — Platform Ops (secondary)
- Persona × permission × surface matrix
Roles that touch the Organizations surface. Permissions are exact from
permissions.constants.ts+role.schema.ts. Note: noorganization.createpermission exists — provisioning is a platform-admin responsibility by role design (OQ-7 in 01). The client hides/denies surfaces per permission; server remains authoritative.
P1 — Super Admin (platform)
| Attribute | Value |
|---|---|
| Real-world role | Platform operator / SaaS vendor operations |
| System role | platform_admin (role.schema.ts:9-16) — priority 1000, permissions: [], operates via isPlatformAdmin bypass (jwt-auth.guard.ts:54; base.repository.ts:21-23) |
| Frequency | Daily (onboarding queue), bursts during migration/ops |
| Entry | /admin/tenants (platform surface, hidden from tenant admins) |
| Permissions (exact) | No explicit organization.* needed — platform bypass; ALL_PERMISSIONS available if role grants (not granted by default) |
Goals
- Provision a school in one call: name + optional slug/domain/contact/address/timezone/currency/plan/branding/metadata (
create-organization.dto.ts:57-112). - Oversee the whole tenant fleet: search by name/slug (
organizations.service.ts:78-83), sort, paginate. - Suspend problem tenants (
PATCH :id→status: suspended,update-organization.dto.ts:130-133) and offboard departed customers (DELETE :id, soft delete).
Pain points
- OQ-1: no self-org endpoint exists, but platform admin does have full
_idvisibility — pain is on the tenant side, not theirs. - Offboarding looks final but data survives 30 days (worker retention) — must communicate to customer.
- Slug/domain conflicts surface as raw 409 messages ("Organization with slug X already exists." —
organizations.service.ts:41-43).
Needs / restrictions
- Needs create/update/delete + list; must not operate tenant data from this surface.
- Restrictions: none enforced in code today for this controller (OQ-4) — client still gates by
isPlatformAdmin.
Mental model: "I open tenants, I create a tenant, I hand the slug to the customer for their admin registration." Expected behaviour: every lifecycle action immediate + reversible (soft delete); nothing hard-deleted for 30 days.
P2 — Org Admin
| Attribute | Value |
|---|---|
| Real-world role | Principal / Director / IT admin of one institution |
| System role | org_admin — priority 100, permissions: ALL_PERMISSIONS (role.schema.ts:17-24), including all organization.*, settings.*, feature-flags.* |
| Frequency | Daily (settings rarely; feature flags occasionally; profile quarterly) |
| Entry | /organization (self surface) |
Permissions (exact subset relevant to this module) — permissions.constants.ts:
| Permission | Line | Screens |
|---|---|---|
organization.read | :2 | Org Overview, Branding read |
organization.update | :3 | Org Edit, Branding, Academic year, Subscription |
organization.delete | :4 | Offboarding (self-delete — destructive, gated) |
organization.settings.update | :5 | Settings tabs (embedded settings path) |
settings.read / settings.update / settings.delete | :75-77 | Settings tabs (standalone settings collection path) |
feature-flags.read / feature-flags.update / feature-flags.delete | :78-80 | Feature flags screen |
user.create | :8 | Invite admin (user creation) |
rbac.member.create / rbac.member.read | :15-16 | Invite admin (membership) |
Goals
- Understand at a glance what the institution is configured as (timezone, currency, academic year, plan, status).
- Fix a mistake fast: wrong timezone, wrong working days, wrong late threshold — before the next attendance run.
- Make the app "feel like the school": logo + brand colors.
- Turn on/off features (e.g., biometric, WhatsApp) as contracts/priorities change.
Pain points
- OQ-6: settings exist in two places (embedded +
settingscollection) — risk of seeing stale/duplicated config. - OQ-1: no "my organization" endpoint — today the app cannot fetch the org profile without the org
_id. - Full-replace semantics: saving a partial settings/flag form silently wipes sibling groups/keys (
organizations.service.ts:134,150). PATCH :id/feature-flagsacceptsRecord<string,boolean>— no key whitelist, no label; the UI must maintain its own flag catalog.
Needs / restrictions
- Needs full read+write on self profile; delete is destructive → typed confirm.
- Must not see other tenants; must not see platform surface.
- Timezone/currency pickers over free text (server only validates
IsString—create-organization.dto.ts:86-94).
Mental model: "This is my school's identity page — like a settings app." Expected: save → instantly reflected app-wide (theme color, logo). Feature flags = light switches that affect whole modules.
P3 — Invited Admin
| Attribute | Value |
|---|---|
| Real-world role | Second administrator (e.g., vice principal, office manager) |
| System role | org_admin again, or a custom role with a permission subset (rbac.controller.ts:39-43) |
| Frequency | Daily; configure rarely |
| Entry | Invitation link/email → POST /auth/register (register.dto.ts:4-29) or login |
| Permissions | Whatever the org admin granted at invite (roles), evaluated server-side via getPermissionsForUser (rbac.service.ts:44-73) |
Goals
- Get in with zero friction: invite email → register (first/last name, email, phone, password ≥ 8) → usable session (register returns access+refresh pair —
auth.service.ts:95-99). - Be able to act on the org surface only where granted; see the same screens as P2 minus denied actions.
Pain points
- No invitation token mechanism exists in code (register is open; OQ-2) — invite = create user + add member role, then hand over credentials/registration.
- If registered with a mistyped tenant slug, they land in the wrong/no tenant silently.
Needs / restrictions
- Needs register form with tenant slug; needs
user.create+rbac.member.createfrom the inviting admin. - Restricted by their role's permission set — the client mirrors it and re-checks on 403.
Mental model: "The boss invited me; I just register and everything is already set up for me." Expected: onboarding state visible on first login if org is still onboarding.
P4 — Platform Ops (secondary)
| Attribute | Value |
|---|---|
| Real-world role | Support/DevOps |
| System role | Any platform role or direct DB/queue access |
| Frequency | Incident-driven |
| Entry | BullMQ dashboard (/admin/queues, main.ts:65-70) + tenant detail |
| Permissions | n/a (observability) |
Goals
- Verify purge timing: worker deletes only
isDeleted && deletedAt < now-30d(tenant-purge.worker.ts:32-42). - Replay
OrganizationCreatednotifications from thein-appqueue (event-queue-map.ts:13). - Diagnose "tenant disappeared" — soft-deleted orgs are invisible to every query (
organizations.repository.ts:15-19).
Persona × permission × surface matrix
| Surface | P1 Super Admin | P2 Org Admin | P3 Invited Admin |
|---|---|---|---|
Tenants list (/admin/tenants) | ✅ (platform flag) | ❌ hidden | ❌ hidden |
| Tenant detail + edit + suspend | ✅ | ❌ (own org only) | ❌ |
| Org Overview (self) | ✅ (via list) | ✅ organization.read | ✅ if role has it |
| Org Edit / Branding | ✅ | ✅ organization.update | role-gated |
| Settings tabs | ✅ (any tenant via :id) | ✅ organization.settings.update + settings.* | role-gated |
| Feature flags | ✅ | ✅ feature-flags.* | role-gated |
| Invite admin | ✅ | ✅ user.create + rbac.member.create | ❌ (unless custom) |
| Offboard (delete) | ✅ | ✅ organization.delete (typed confirm) | ❌ |
Server remains authoritative: 403 PERMISSION_DENIED / 404 RESOURCE_NOT_FOUND handling per 00-shared/06 §5.