Skip to main content

Auth, Tenant, and RBAC

SalesArck separates identity verification from authorization.

  • Identity: verified by Supabase JWT validation.
  • Authorization: resolved from SalesArck-owned user/tenant/permission data.

Authentication Entry Points

Strict auth middleware

apps/api/src/middleware/auth.ts is used on protected routes.

Behavior:

  1. Require Authorization: Bearer <token>.
  2. Verify token via Supabase service-role client.
  3. Resolve or create users row (resolveOrCreateUserRow).
  4. Enforce admin email domain for role admin (@doshidhruv.com).
  5. Enforce user.status === "active".
  6. Load tenant memberships from tenant_users.
  7. Attach auth context { userId, role, tenantIds }.

Profile auth middleware

apps/api/src/middleware/auth-profile.ts is used only on GET /api/v1/auth/me.

It allows valid JWT users to receive profile state even when app access is blocked.

Response includes:

  • userId, email, phone
  • role, status
  • canUseApp
  • tenantIds
  • needsMerchantOnboarding
  • optional code (PENDING_APPROVAL, SUSPENDED, ADMIN_EMAIL_REQUIRED)

Role and Status Derivation

First-login role assignment is performed in apps/api/src/lib/resolve-user.ts.

requested_role in Supabase user_metadata influences initial role:

  • client or merchant -> role=client, status=pending_approval
  • admin + approved domain (@doshidhruv.com) -> role=admin, status=active
  • admin + other domain -> role=consumer, status=active
  • otherwise -> role=consumer, status=active

Existing user rows are not re-derived on subsequent requests.

Tenant Isolation

Tenant access is enforced by apps/api/src/middleware/tenant.ts using @salesarc/tenant.

Rules:

  • Tenant ID is read from URL path (:tenantId), never request body/query.
  • For non-admin users, requested tenant must exist in auth.tenantIds.
  • Admin has global tenant access in resolver logic.

Violation throws TENANT_NOT_MEMBER.

RBAC Permission Model

Permissions are declared in packages/rbac/src/permissions.ts and checked via requirePermission(...) middleware.

Consumer

  • wallet:view:own
  • wallet:history:own
  • wallet:redeem

POS operator

  • redemption:verify

Client

  • tenant:view
  • tenant:profile:update
  • tenant:reward:config
  • tenant:pos:connect
  • tenant:analytics:view
  • tenant:consumer:view
  • tenant:team:invite

Admin

  • All client permissions
  • admin:tenants:view:all
  • admin:wallet:freeze
  • admin:wallet:adjust
  • admin:audit:logs:view
  • admin:user:search

Access Pattern in Routes

Common pattern:

  1. router.use(authMiddleware)
  2. optional router.use("/tenants/:tenantId/*", tenantMiddleware)
  3. per-endpoint requirePermission(Permission.X)
  4. business handler

This gives defense-in-depth:

  • authenticated identity,
  • tenant membership validation,
  • permission-level gate,
  • resource ownership checks in SQL clauses.

Admin Approval Workflow

Merchants are not auto-approved. Admin must explicitly approve via:

  • PATCH /api/v1/admin/users/:userId/approve

On approval:

  1. User status set to active
  2. Audit log written
  3. If role is client: auto-provision merchant workspace via ensureClientHasMerchantWorkspace()
  4. Optional provisioning audit log when tenant is newly created

Admin Role Change

  • PATCH /api/v1/admin/users/:userId/role
  • If new role is client: status set to pending_approval
  • Otherwise: status set to active
  • Audit log written with before/after state

Security Invariants

Non-negotiable invariants in current code:

  1. JWT is always validated server-side before auth context is built.
  2. Tenant identity is never trusted from request body/query.
  3. Admin role alone is insufficient without approved email domain.
  4. Pending/suspended users cannot use strict protected routes.
  5. UI route guards are convenience only; API middleware is the real security boundary.
  6. OAuth state is HMAC-signed and time-limited, preventing CSRF.
  7. All admin actions generate immutable audit logs.
Written byDhruv Doshi