Frontend Architecture
Application Model
SalesArck uses a single frontend application with one login flow. Access is controlled by RBAC:
| Surface | Route Prefix | Who Uses It | Access Rule |
|---|---|---|---|
| Consumer UI | /wallet, /history, /redeem | End consumers | consumer role |
| Merchant UI | /dashboard, /transactions, /pos, /rewards | Merchant team members | client or admin role |
| Admin UI | /admin/* | SalesArck internal team | admin role |
Tech Stack
Core
- React 18 — component model, concurrent features
- TypeScript — strict mode, no
anyin domain code - Vite — fast dev server + production bundler
State Management
- React Query (TanStack Query) — server state, caching, background refetch
- Zustand — lightweight client state (e.g., drawer open/closed, selected tenant)
- Avoid Redux — it's overkill for this scale
Forms & Validation
- React Hook Form — performant uncontrolled forms
- Zod — schema validation shared with backend types where possible
Styling
- Tailwind CSS + shadcn/ui component primitives
- Design tokens for SalesArck brand colors
HTTP Client
- Strongly-typed API client generated from OpenAPI spec (or hand-written with Zod validation)
- Axios or ky — with interceptors for token refresh
Application Structure
apps/web/
src/
pages/ ← Route-level components (all roles)
components/ ← UI components (wallet card, history list, nav shell, etc.)
hooks/ ← useWallet, useRedemption, useAuth, etc.
api/ ← Typed API client functions
store/ ← Zustand state slices (auth with role)
lib/ ← Utilities, formatters, Supabase client
types/ ← Shared TypeScript types
Unified App Routes
/login ← Supabase OTP sign-in form
/wallet ← Consumer wallet
/history ← Consumer transaction history
/redeem ← Consumer redemption preview + confirm
/dashboard ← Merchant overview metrics
/transactions ← Merchant transaction log
/pos ← Merchant POS connection management
/rewards ← Merchant reward configuration
/admin/tenants ← Tenant list + search
/admin/users ← User search
/admin/audit-logs ← Immutable audit log viewer
Authentication Handling
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
);
// The Supabase client keeps the session fresh automatically.
// App data requests still go through the typed API client with the access token attached.
Access tokens in memory, not localStorage
Access tokens should not be placed in localStorage. Let the Supabase client manage the session, and pass the current access token to API requests only when needed.
Role-Based Route Guards (UI-Level)
// Route guard — UX only, backend remains source of truth
function RequireRole({ role, children }: { role: UserRole; children: ReactNode }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
if (user.role !== role) return <Navigate to="/unauthorized" />;
return <>{children}</>;
}
// Usage
<RequireRole role="admin">
<AdminDashboard />
</RequireRole>
Frontend role checks are UX only
These guards improve UX by not rendering unauthorized UI. They are not a security boundary — the API enforces all permissions server-side. Never skip the backend check.
Key Frontend Rules
□ No business logic in UI components — move to hooks or api layer
□ No direct fetch() calls — all HTTP goes through the typed API client
□ No hardcoded tenant IDs — always from auth context
□ No duplicating backend validation — Zod schemas shared from types package
□ TypeScript strict mode — no `any`, no `!` non-null assertions without comment
□ React Query for all server state — no useState for data that comes from API
□ Error boundaries on every page-level component
□ Loading + error states handled for every async operation
Written byDhruv Doshi