Frontend Architecture
The web application (apps/web) is a single React app serving multiple personas:
- consumer
- client (merchant)
- pos_operator
- admin
Runtime Stack
- React 18 + Vite 5
- React Router 6
- TanStack Query 5 (configured, used lightly)
- Zustand 4 (single auth store)
- Supabase JS client with PKCE flow
- Tailwind CSS 3.4
Bootstrap Sequence
apps/web/src/main.tsx wraps app with:
QueryClientProviderBrowserRouter- top-level
App
App then initializes profile loading via fetchAndApplyProfile(true).
Auth and Session Model
Supabase client
lib/supabase.ts config:
flowType: "pkce"detectSessionInUrl: truepersistSession: trueautoRefreshToken: true
Auth Store
store/auth.ts (Zustand) keeps:
userId,role,tenantIds,email,phonestatus(active,pending_approval,suspended)canUseApp,needsMerchantOnboardingblockCode(ADMIN_EMAIL_REQUIRED, etc.)isLoading,profileLoadError
Actions: setProfile(), setProfileLoadFailed(), setProfileLoading(), clearAuth()
Profile fetch reliability
lib/auth-profile.ts includes:
- In-flight request coalescing (prevents duplicate calls)
- Retry/backoff for transient errors (max 5 attempts, 400ms -> 12s)
- Special handling for
429withRetry-After - Forced sign-out on 401 profile rejection
Login Flow
- User selects email or phone on
LoginPage - Email: user picks role intent (consumer, client, admin)
- OTP sent via
signInWithOtp()withdata: { requested_role } - User enters 6-8 digit code
verifyOtp()exchanges for sessionfetchAndApplyProfile()loads profile fromGET /api/v1/auth/menavigateAfterLogin()routes based on profile state
PKCE Callback
- Supabase returns to site URL with
?code=... PkceRedirectLayoutintercepts and redirects to/auth/callback?code=...AuthCallbackPagecallsexchangeCodeForSession()to complete auth- Handles legacy hash fragment fallback
Route Guarding Strategy
App.tsx composes layered guards:
PkceRedirectLayout(preserve?codecallback flow)AuthRedirector(global route decisions)ProtectedRoute(session required — checksuserId)CanUseAppRoute(status and block checks —canUseAppmust be true)RoleRoute(persona-based route access — checksrole)
This gives explicit, readable, and testable route policy composition.
Route Map
Public Routes
| Path | Component | Description |
|---|---|---|
/login | LoginPage | Email/phone OTP login with role selection |
/auth/callback | AuthCallbackPage | PKCE code exchange |
Consumer Routes
| Path | Component | Role | Description |
|---|---|---|---|
/wallet | WalletPage | consumer | Multi-merchant wallet cards with deterministic colors |
/history | HistoryPage | consumer | Transaction ledger for selected merchant |
/redeem | RedeemPage | consumer, pos_operator | Multi-step redemption (select -> preview -> code) |
Client (Merchant) Routes
| Path | Component | Role | Description |
|---|---|---|---|
/onboarding | OnboardingPage | client | Workspace bootstrap + business setup wizard |
/dashboard | DashboardPage | client, admin | Role-aware dashboard view |
/rewards | RewardConfigPage | client | Reward rule configuration |
/pos | PosSettingsPage | client | Square/Clover connection management |
/transactions | TransactionsPage | client | Merchant transaction feed |
Admin Routes
| Path | Component | Role | Description |
|---|---|---|---|
/admin/tenants | TenantsPage | admin | Merchant list with detail panel |
/admin/users | UsersPage | admin | User search and approval management |
/admin/audit-logs | AuditLogsPage | admin | Immutable action log |
Status Routes
| Path | Component | Description |
|---|---|---|
/pending-approval | PendingApprovalPage | Merchant awaiting admin approval |
/account-blocked | AccountBlockedPage | Account suspended or invalid admin email |
/profile-error | ProfileErrorPage | Session exists but profile API unreachable |
/profile | ProfilePage | Role-based profile view |
Home Redirect
/ redirects based on role:
- consumer ->
/wallet - client ->
/dashboard - admin ->
/dashboard - pos_operator ->
/redeem
Navigation Model
AppShell builds nav links dynamically by role and onboarding state.
Notable behavior:
- Clients with onboarding incomplete see restricted menu
- Admin-only routes are isolated (
/admin/*) - Mobile navigation uses explicit open/close state and body scroll lock
- Home button routes based on role via
getDefaultPath(role)
Mobile-First Design
min-h-screen min-h-[100dvh]for mobile address bar handling- Safe area insets:
env(safe-area-inset-top),env(safe-area-inset-bottom) - Touch targets >= 44px
touch-manipulationon interactive elements
API Client Contract
lib/api.ts defines apiFetch<T>(path, init):
- Derives API base from
VITE_API_BASE_URL - Attaches Supabase bearer token automatically
- Parses API errors and exposes
ApiRequestError - Captures
Retry-Afterfor backoff callers - Exponential backoff (400ms -> 12s) for 429, 502/503, network errors
Styling Approach
- 100% Tailwind utility classes (no component library)
- Color system: Slate (neutral), Indigo (primary), Emerald (success), Amber (warning), Red (error)
- Deterministic merchant colors: hash-based palette (8 colors) ensures same tenant gets same color
- Responsive:
sm:(640px) andmd:(768px) breakpoints - No dark mode
State Management Pattern
- Data fetching:
useEffect+useState(directapiFetch()calls in components) - TanStack Query configured but not heavily used for data fetching
- Global auth state in Zustand store
- Tenant context via
tenantIds[0](single-tenant per merchant currently)
Design Characteristics
- API-backed pages avoid hardcoded stubs in current implementation.
- Role policy is duplicated in UI for UX only; backend remains security authority.
- Merchant onboarding progression is enforced before broader client functionality.
- All pages render real data from backend API endpoints.
Written byDhruv Doshi