ProjectR Docs
Dashboard

Auth & Access

Better Auth client, role resolution, and the permission matrix.

Session plumbing

  • src/lib/auth-client.ts — createAuthClient with convexClient() + crossDomainClient(). Sessions live in localStorage under better-auth.session_token, sent via the Better-Auth-Cookie header — 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.tsx wraps the app in ConvexBetterAuthProvider (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:

Featureviewcreate/updatedelete
orders / products / content / loyalty / vouchers / raya / notifications / ratingsdashboardadminsuper_admin (varies)
usersadmincreate: super_admin; update: admin (role change: super_admin)super_admin
webhooksadminsuper_adminsuper_admin
auditadmin——

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 (by feature+operation or minRole) renders skeleton-while-loading and the AccessDenied page on failure; wrappers AdminOnly, SuperAdminOnly, CanCreate/CanUpdate/CanDelete/CanExport exist 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).

On this page