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:
- Require
Authorization: Bearer <token>. - Verify token via Supabase service-role client.
- Resolve or create
usersrow (resolveOrCreateUserRow). - Enforce admin email domain for role
admin(@doshidhruv.com). - Enforce
user.status === "active". - Load tenant memberships from
tenant_users. - Attach
authcontext{ 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,phonerole,statuscanUseApptenantIdsneedsMerchantOnboarding- 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:
clientormerchant->role=client,status=pending_approvaladmin+ approved domain (@doshidhruv.com) ->role=admin,status=activeadmin+ 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:ownwallet:history:ownwallet:redeem
POS operator
redemption:verify
Client
tenant:viewtenant:profile:updatetenant:reward:configtenant:pos:connecttenant:analytics:viewtenant:consumer:viewtenant:team:invite
Admin
- All client permissions
admin:tenants:view:alladmin:wallet:freezeadmin:wallet:adjustadmin:audit:logs:viewadmin:user:search
Access Pattern in Routes
Common pattern:
router.use(authMiddleware)- optional
router.use("/tenants/:tenantId/*", tenantMiddleware) - per-endpoint
requirePermission(Permission.X) - 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:
- User status set to
active - Audit log written
- If role is
client: auto-provision merchant workspace viaensureClientHasMerchantWorkspace() - 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 topending_approval - Otherwise: status set to
active - Audit log written with before/after state
Security Invariants
Non-negotiable invariants in current code:
- JWT is always validated server-side before auth context is built.
- Tenant identity is never trusted from request body/query.
- Admin role alone is insufficient without approved email domain.
- Pending/suspended users cannot use strict protected routes.
- UI route guards are convenience only; API middleware is the real security boundary.
- OAuth state is HMAC-signed and time-limited, preventing CSRF.
- All admin actions generate immutable audit logs.