Skip to content

Architecture overview

QA Sync is a single-page React application backed directly by Supabase. There is no application server of its own: the browser talks to Supabase Auth, the PostgREST API, database functions (RPCs) and Storage, and Postgres Row Level Security enforces every data boundary.

┌──────────────────────────┐
│ Browser: React 19 SPA │
│ (Vite build on Vercel) │
└──────┬─────────┬─────────┘
│ │
▼ ▼
Supabase Auth Supabase PostgREST / RPC ──▶ Postgres (RLS)
Supabase Storage ──────────▶ private buckets
Layer Technology
UI React 19, TypeScript (~5.8), Tailwind CSS 4, lucide-react icons
Build Vite 6
Auth Supabase Auth, email and password
Data Supabase Postgres via @supabase/supabase-js, with RPCs for multi-step workflows
Files Supabase Storage, private buckets with signed URLs
Authorisation Postgres Row Level Security and SECURITY DEFINER helper functions
Hosting Vercel (static SPA with a rewrite to /index.html)
apps/web/src/
api/ Supabase data access and mutations (the only data-access layer)
components/ Reusable shared UI only; layout/ holds the app shell
config/ Runtime feature and environment gates
constants/ Shared workflow and status constants
hooks/ Workspace data and actions: admin/, app/, developer/, tester/, analytics/
lib/ Low-level helpers
routes/ The in-house router
screens/ Route-level screens and role-owned workspace UI
utils/ Domain-specific pure helpers
types.ts Shared domain types (roles, statuses, entities)
main.tsx Entry point
App.tsx Thin route shell
supabase/
migrations/ Schema and policy history
seed.sql Local seed data
config.toml Local ports and auth settings

QA Sync uses a small in-house history router in apps/web/src/routes/index.ts, not react-router-dom. The route set is fixed:

The app is served under /app (Vite base), so in the browser each route below is prefixed: /app/login, /app/admin, and so on. The router strips the prefix, so route names stay the same in code.

Route Screen
/login Login
/access-pending Access Pending
/admin Admin workspace (Admin and Super Admin)
/product-manager Product Release Workspace
/developer Developer Workspace
/tester Tester workspace
/workspace Stakeholder workspace (UI Designer, Client, Viewer)

apps/web/src/hooks/app/useAppSession.ts restores the Supabase session, resolves the profile, and redirects to the right route. apps/web/src/screens/RoleWorkspaceScreen.tsx wraps the role dashboard in the shared AppShell and lazy-loads it.

  1. Supabase Auth signs the user in.
  2. apps/web/src/api/profile-access.ts reads the user’s row in public.profiles.
  3. If there’s no row, it calls ensure_current_user_profile(), which creates or repairs the profile from the admin-managed public.access_directory.
  4. An active profile with a role goes to its workspace; anything else goes to /access-pending.

See Database and Row Level Security.

All table, RPC and storage access lives in apps/web/src/api. Screens, hooks and components call apps/web/src/api functions; they never call Supabase directly. The single exception is Supabase Auth session calls (sign-in, sign-out, auth-state listener), which LoginScreen.tsx and useAppSession.ts make on the shared client exported from apps/web/src/api/supabase.ts.

API functions follow load* / save* / delete* naming and return a result envelope with source and message instead of throwing.

Role Screen Data hook(s)
Admin, Super Admin screens/admin/AdminDashboard.tsx hooks/admin/useAdminDashboardData.ts, useAdminDashboardActions.ts, useAdminDashboardUiState.ts
Product Manager screens/product-manager/ProductManagerDashboard.tsx hooks/analytics/useAnalyticsDashboard.ts
Developer screens/developer/DevDashboard.tsx hooks/developer/useDeveloperDashboardData.ts
Tester screens/tester/TesterDashboard.tsx hooks/tester/useTesterDashboardData.ts, useTesterDeviceProfile.ts
UI Designer, Client, Viewer screens/stakeholder/StakeholderDashboard.tsx hooks/analytics/useAnalyticsDashboard.ts

Shared panels used across workspaces live in apps/web/src/components: the release board (release/ReleaseControlBoard.tsx), notification centre, comments, history timelines, hierarchy tree and analytics panels.

  • Role dashboards, admin tabs and admin modals are lazy-loaded.
  • The admin route loads a lightweight overview aggregate RPC first and fetches heavier tab data on demand.
  • Reference data, admin lists, task requests, analytics and attachment signed URLs use in-memory caches with explicit force-refresh after mutations.
  • The build emits react-vendor, supabase-vendor and ui-vendor chunks so the entry bundle stays small.
  • Charts are native SVG; there is no charting dependency.
  • Local development doesn’t mount React StrictMode, avoiding duplicate effect-driven fetches, and keeps the resolved auth snapshot across Fast Refresh.

Architecture decisions are recorded one per file in docs/decisions/ in the repository. Accepted decisions are binding; read the ones that apply before changing an area. Highlights:

Decision Summary
0001 Use the in-house router, not react-router-dom.
0002 Admin charts are native SVG, not recharts.
0003 apps/web/src/api is the only frontend data-access boundary.
0004 Public self-sign-up stays disabled.
0006 RLS policies use SECURITY DEFINER helpers, not recursive self-queries.
0008 Shared UI in apps/web/src/components, role-owned UI in apps/web/src/screens.
0009 npm is the package manager.
0014 Delivery lifecycle and QA execution stay separate.
  • The router has no nested route composition.
  • There is no automated test suite; npm run lint (type-check) and the validation scripts are the checks.