Auth & Access
Better Auth client, role resolution, and the permission matrix.
Session plumbing
src/lib/auth-client.ts—createAuthClientwithconvexClient()+crossDomainClient(). Sessions live in localStorage underbetter-auth.session_token, sent via theBetter-Auth-Cookieheader — not browser cookies. Consequence: auth breakage never shows in static page renders; smoke a real sign-in after any auth-cluster bump.src/app/api/auth/[...all]/route.ts— GET delegates to the Convex Better Auth handler; POST is a hand-rolled forward to the Convex site URL with forwarded-host headers (avoids CORS).ConvexClientProvider.tsxwraps the app inConvexBetterAuthProvider(with a documented cast — the 0.12.x exported type is degenerate).
Role resolution chain
useSession() → useAuth() (merges session user with the Convex userProfiles row) → use-current-user-role.ts (convexProfile?.role || … || 'apps') → use-permissions.ts. The role lives in Convex userProfiles, never in the Better Auth session. normalizeRole() (src/types/ui-config.ts) maps any casing to the lowercase union and falls back to 'apps' (no access).
The permission matrix
src/lib/access-control.ts: 15 feature areas × 5 operations (view/create/update/delete/export), each cell a minimum role on the hierarchy apps(1) < dashboard(2) < admin(3) < super_admin(4). Highlights:
| Feature | view | create/update | delete |
|---|---|---|---|
| orders / products / content / loyalty / vouchers / raya / notifications / ratings | dashboard | admin | super_admin (varies) |
| users | admin | create: super_admin; update: admin (role change: super_admin) | super_admin |
| webhooks | admin | super_admin | super_admin |
| audit | admin | — | — |
ROUTE_FEATURE_MAP + canAccessRoute enforce per-route; the sidebar hides items by requiredRole; use-permissions.ts exposes memoized canAccess/canCreate/… plus useRequireAdmin/useRequireSuperAdmin.
Guards
AuthGuard(components/auth/auth-guard.tsx) does a synchronous localStorage token check so it never redirects while a session is still loading; redirects to/auth/sign-in?callbackUrl=….ProtectedRoute(byfeature+operationorminRole) renders skeleton-while-loading and theAccessDeniedpage on failure; wrappersAdminOnly,SuperAdminOnly,CanCreate/CanUpdate/CanDelete/CanExportexist for inline gating.
CSP note
Any new client-facing domain must be added to connect-src in next.config.ts or its fetches fail silently (current allowlist covers *.convex.cloud/.site, R2, Sentry, *.theraw.com.my).