Skip to main content

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:

  1. QueryClientProvider
  2. BrowserRouter
  3. top-level App

App then initializes profile loading via fetchAndApplyProfile(true).

Auth and Session Model

Supabase client

lib/supabase.ts config:

  • flowType: "pkce"
  • detectSessionInUrl: true
  • persistSession: true
  • autoRefreshToken: true

Auth Store

store/auth.ts (Zustand) keeps:

  • userId, role, tenantIds, email, phone
  • status (active, pending_approval, suspended)
  • canUseApp, needsMerchantOnboarding
  • blockCode (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 429 with Retry-After
  • Forced sign-out on 401 profile rejection

Login Flow

  1. User selects email or phone on LoginPage
  2. Email: user picks role intent (consumer, client, admin)
  3. OTP sent via signInWithOtp() with data: { requested_role }
  4. User enters 6-8 digit code
  5. verifyOtp() exchanges for session
  6. fetchAndApplyProfile() loads profile from GET /api/v1/auth/me
  7. navigateAfterLogin() routes based on profile state

PKCE Callback

  • Supabase returns to site URL with ?code=...
  • PkceRedirectLayout intercepts and redirects to /auth/callback?code=...
  • AuthCallbackPage calls exchangeCodeForSession() to complete auth
  • Handles legacy hash fragment fallback

Route Guarding Strategy

App.tsx composes layered guards:

  1. PkceRedirectLayout (preserve ?code callback flow)
  2. AuthRedirector (global route decisions)
  3. ProtectedRoute (session required — checks userId)
  4. CanUseAppRoute (status and block checks — canUseApp must be true)
  5. RoleRoute (persona-based route access — checks role)

This gives explicit, readable, and testable route policy composition.

Route Map

Public Routes

PathComponentDescription
/loginLoginPageEmail/phone OTP login with role selection
/auth/callbackAuthCallbackPagePKCE code exchange

Consumer Routes

PathComponentRoleDescription
/walletWalletPageconsumerMulti-merchant wallet cards with deterministic colors
/historyHistoryPageconsumerTransaction ledger for selected merchant
/redeemRedeemPageconsumer, pos_operatorMulti-step redemption (select -> preview -> code)

Client (Merchant) Routes

PathComponentRoleDescription
/onboardingOnboardingPageclientWorkspace bootstrap + business setup wizard
/dashboardDashboardPageclient, adminRole-aware dashboard view
/rewardsRewardConfigPageclientReward rule configuration
/posPosSettingsPageclientSquare/Clover connection management
/transactionsTransactionsPageclientMerchant transaction feed

Admin Routes

PathComponentRoleDescription
/admin/tenantsTenantsPageadminMerchant list with detail panel
/admin/usersUsersPageadminUser search and approval management
/admin/audit-logsAuditLogsPageadminImmutable action log

Status Routes

PathComponentDescription
/pending-approvalPendingApprovalPageMerchant awaiting admin approval
/account-blockedAccountBlockedPageAccount suspended or invalid admin email
/profile-errorProfileErrorPageSession exists but profile API unreachable
/profileProfilePageRole-based profile view

Home Redirect

/ redirects based on role:

  • consumer -> /wallet
  • client -> /dashboard
  • admin -> /dashboard
  • pos_operator -> /redeem

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-manipulation on 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-After for 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) and md: (768px) breakpoints
  • No dark mode

State Management Pattern

  • Data fetching: useEffect + useState (direct apiFetch() 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