Skip to main content

Frontend Architecture

Application Model

SalesArck uses a single frontend application with one login flow. Access is controlled by RBAC:

SurfaceRoute PrefixWho Uses ItAccess Rule
Consumer UI/wallet, /history, /redeemEnd consumersconsumer role
Merchant UI/dashboard, /transactions, /pos, /rewardsMerchant team membersclient or admin role
Admin UI/admin/*SalesArck internal teamadmin role

Tech Stack

Core

  • React 18 — component model, concurrent features
  • TypeScript — strict mode, no any in 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