Core
Auth
Rbac

RBAC — Roles & Tenancy

Three roles (OWNER, STAFF, SUPER_ADMIN), enforced by backend middleware. Multi-tenancy is a single self-referencing FK: STAFF.ownerId → OWNER.id. SUPER_ADMIN bypasses tenancy.

1. Overview

Every authenticated request passes through authenticateToken (JWT verify) and then a role-specific guard. OWNER is the tenant root — each OWNER has exactly one Store. STAFF references an OWNER via User.ownerId and operates under that OWNER's Store. SUPER_ADMIN is the platform operator — the admin panel at /admin/* is gated by requireSuperAdmin. Frontend guards (AdminGuard.tsx) are cosmetic; real enforcement is backend-only.

2. Architecture

3. Data model

See schema.prisma:L14-L18 (UserRole enum), L31 (User.ownerId self-ref), L77 (Store.ownerId @unique — enforces 1:1).

4. Key flows

4.1 Access check

4.2 Effective owner resolution

Handlers that mutate store-scoped data call getEffectiveOwnerId(user):

  • OWNERuser.id
  • STAFFuser.ownerId
  • SUPER_ADMIN → impersonation target or null

All downstream Prisma queries filter by this ownerId.

5. Permission matrix (high-level)

CapabilityOWNERSTAFFSUPER_ADMIN
View/edit own products, orders, customers✓ (any)
Invite/remove team members
Manage billing & subscription
Store branding, catalog settings
Admin panel (/admin/*)
Impersonate another owner(deferred)
Deactivate a store(deferred)

6. Key files

48 occurrences of requireRole/requireOwner*/requireSuperAdmin across 8 backend route files.

7. Env vars & config

None specific. Inherits JWT_SECRET. See authentication.md.

8. Gotchas & troubleshooting

  • Impersonation is deferred → SUPER_ADMIN cannot "log in as" an OWNER yet. Planned (memory: project_admin_pending). When building, also gate against SubscriptionGate so impersonators don't silently bypass plan limits.
  • Store deactivation is deferred → OWNER of a deactivated store can still hit most routes. Deferred per the same memory.
  • STAFF sees ALL of the OWNER's data → No per-role granularity. STAFF reads every order, customer, product. Read-only staff = a new role.
  • SUPER_ADMIN bypasses tenancygetEffectiveOwnerId may return null; passing null to Prisma where = returns everything. Handlers must guard explicitly.
  • Frontend guards are not securityAdminGuard.tsx only redirects. A stolen SUPER_ADMIN token still works via curl.
  • Role change requires token refresh → Old tokens (1m–24h) carry old role claims until expiry. Force re-login when changing roles in sensitive ops.

9. Extension points

  • New role → extend UserRole enum + migration, add guard helper, update handlers + Permissions.
  • Granular permissions → add permissions: string[] on User; check via Permissions.X. Keep roles as coarse buckets.
  • Row-level ACLs → extend canAccessResource for per-row checks.

10. Related docs