User Personas & Permissions
The Four Roles
SalesArck has four user roles. Every authenticated user occupies exactly one role at the platform level, though consumers and clients live in different tenant contexts.
1. Consumer
The end customer who shops at a merchants that use SalesArck. Consumers earn points passively (via POS transactions) and can actively redeem them through the consumer app.
Characteristics:
- May not even know SalesArck exists — the experience is branded per merchant
- Can have wallet relationships with multiple tenants simultaneously
- Authenticates globally but transacts within tenant scope
- Primary identity: Supabase user account with verified phone or email contact method
2. Client (Merchant)
A team member of a merchant business. May be the owner or an employee with delegated access. Manages their loyalty program: connects POS, configures rules, views analytics.
Characteristics:
- Operates within a single tenant (their business)
- Invited by an existing tenant owner or self-registered
- Cannot see other tenants' data — ever
- Manages POS OAuth tokens and reward rules for their tenant
- New client accounts start with
status: pending_approvaland require admin approval before access is granted
3. POS Operator
A cashier or front-line employee at a merchant location. Has limited access scoped to a single task: verifying and confirming customer redemption codes at the register. Does not have access to analytics, reward configuration, POS settings, or any other merchant portal feature.
Characteristics:
- Operates within a single tenant (their employer's business)
- Invited by a Client (merchant owner/manager) — cannot self-register
- Access is restricted to the Redemption Verification Page only
- Cannot view transaction history, analytics, reward rules, or POS settings
- Designed for high employee turnover — easy to provision and revoke
Redemption Verification Page:
The POS Operator sees a single-purpose interface:
┌────────────────── ────────────────────────┐
│ SalesArck — Verify Redemption │
│ │
│ Enter code: [ AXK47R ] [Search] │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Customer: Jane D. │ │
│ │ Discount: $1.00 (100 points) │ │
│ │ Expires: 3 min 22 sec remaining │ │
│ │ │ │
│ │ [ ✅ Confirm Redemption ] │ │
│ └──────────────────────────────────────┘ │
│ │
│ ⓘ Apply $1.00 discount at the register │
│ before confirming. │
└──────────────────────────────────────────┘
- Search box: Operator enters the 6-character code from the customer
- Result displays: Customer first name + last initial, discount dollar amount, time remaining
- Confirm button: Grayed out / disabled if the code is expired or otherwise invalid
- Estimated verification time: ~30 seconds per transaction (acceptable for counter service)
4. Admin
A SalesArck internal employee with platform-wide access. Performs support actions, fraud interventions, and platform operations. Every admin action is immutably logged.
Characteristics:
- Global access across all tenants
- Can perform actions consumers and clients cannot (manual wallet adjustments, freezing, etc.)
- Every action creates an
admin_audit_logwith before/after state and reason - Admin bootstrap seeded in Phase 0 via environment config
- Admin signup restricted to
@doshidhruv.comemail addresses only (enforced server-side) - Admins can approve/reject client accounts and assign roles via
PATCH /api/v1/admin/users/:id/approve
Permission Matrix
| Capability | Consumer | Client | POS Operator | Admin |
|---|---|---|---|---|
| View own profile | ✅ | ✅ | ✅ | ✅ |
| Update own profile | ✅ | ✅ | ❌ | ✅ |
| View own wallet balance | ✅ | ❌ | ❌ | ✅ |
| View own transaction history | ✅ | ❌ | ❌ | ✅ |
| Redeem points | ✅ | ❌ | ❌ | ✅ |
| Verify redemption code | ❌ | ✅ (own tenant) | ✅ (own tenant) | ✅ |
| Confirm redemption | ❌ | ✅ (own tenant) | ✅ (own tenant) | ✅ |
| View tenant transactions | ❌ | ✅ (own tenant) | ❌ | ✅ (all) |
| View tenant analytics | ❌ | ✅ (own tenant) | ❌ | ✅ (all) |
| Configure reward rules | ❌ | ✅ (own tenant) | ❌ | ✅ |
| Connect/disconnect POS | ❌ | ✅ (own tenant) | ❌ | ✅ |
| Revoke POS token | ❌ | ✅ (own tenant) | ❌ | ✅ |
| Invite team members | ❌ | ✅ (own tenant, owner role) | ❌ | ✅ |
| Invite POS operators | ❌ | ✅ (own tenant) | ❌ | ✅ |
| Manual wallet adjustments | ❌ | ❌ (post-MVP) | ❌ | ✅ |
| Freeze/suspend wallets | ❌ | ❌ | ❌ | ✅ |
| View all tenants | ❌ | ❌ | ❌ | ✅ |
| Change tenant status | ❌ | ❌ | ❌ | ✅ |
| View admin audit logs | ❌ | ❌ | ❌ | ✅ |
| View fraud alerts | ❌ | ❌ | ❌ | ✅ |
Identity Constraints
mobileis globally unique — it is the primary login identifieremailis optional but when present must be globally unique- A consumer can exist in multiple tenant contexts (has a separate wallet per tenant)
- Client users are restricted to the tenants they're explicitly members of — enforced by
tenant_usersand verified server-side on every request - Admin users have no
tenant_idclaim — they access all tenants via elevated permission checks
JWT Token Claims
The Supabase session JWT identifies the user, and SalesArck resolves the rest of the authorization context server-side:
{
"sub": "supabase-user-uuid",
"role": "consumer | client | pos_operator | admin",
"tenant_ids": ["tenant-uuid-1"]
}
Notes:
subcomes from Supabase and is the stable identity key for SalesArcktenant_idsis resolved by SalesArck after verifying the Supabase JWT- Role and tenant access are enforced by SalesArck middleware, not by the client
- Session refresh and token rotation are handled by Supabase Auth
Multi-Tenancy Enforcement Pattern
Request arrives
↓
JWT decoded → userId, role, tenant_ids
↓
tenantMiddleware() runs:
if role = client:
assert requestedTenantId ∈ jwt.tenant_ids
if role = pos_operator:
assert requestedTenantId ∈ jwt.tenant_ids
assert route ∈ OPERATOR_ALLOWED_ROUTES
if role = consumer:
assert consumer owns wallet for requestedTenantId
if role = admin:
allow any tenantId
↓
Handler receives validated (userId, tenantId) context
↓
All DB queries include WHERE tenant_id = :tenantId
It is not sufficient to validate the tenant only at the route level. Every database query that operates on tenant-scoped data must include a tenant_id filter, even if the route already has a middleware check. Defense in depth.