Skip to main content

Authentication

Tonnex uses Supabase Auth for all authentication needs with enterprise-grade optimizations for performance and security.

Architecture

ComponentClientPurpose
apps/web@supabase/ssr (Anon Key)Login, signup, session management
apps/apijose + @supabase/supabase-jsLocal JWT verification, admin operations

Web App Auth Flow (Next.js 16)

  1. User enters credentials on /login or creates account on /signup
  2. Upon success, user is redirected to /home
  3. @supabase/ssr handles Supabase auth call
  4. Session stored as HTTP-only cookies
  5. proxy.ts refreshes session cookie on every request and calls getUser() for JWT verification
  6. getAuthContext() reads from getSession() (local cookie, ~0ms) — safe because middleware already verified the token
  7. Server actions and components use getAuthContext() for token + org context
⚠️ Performance Note: getAuthContext() uses getSession() (local, ~0ms) instead of getUser() (network, ~150-300ms). The middleware already verifies the JWT, so re-verification in server actions is unnecessary. getAuthContext() is deduplicated via React.cache().

API Auth Flow

  1. Web app includes JWT in Authorization: Bearer <token> header
  2. SupabaseGuard extracts token and verifies it locally using jose + SUPABASE_JWT_SECRET (~0.1ms)
  3. Falls back to Supabase Auth API if local verification fails
  4. Authenticated user attached to request.user
  5. All downstream services can access the verified user + JWT claims
🔒 Security: Swagger/API docs are only available in development (NODE_ENV !== 'production'). CORS requires WEB_URL to be set in production.

User Logout

  1. Click the Sign out button in the dashboard sidebar.
  2. The logout() server action (in apps/web/actions/auth.ts) is triggered.
  3. Supabase session is revoked.
  4. User is redirected to /login.

JWT Claims (Custom Access Token Hook)

The Supabase custom access token hook (supabase/migrations/0001_custom_access_token_hook_onboarding.sql) enriches the JWT with:
ClaimSourcePurpose
app_metadata.org_idorg_membershipsTenant isolation
app_metadata.roleorg_membershipsAdmin fast-path
app_metadata.membership_idorg_membershipsRBAC lookups
app_metadata.location_idorg_membershipsScope context
app_metadata.warehouse_idorg_membershipsScope context
app_metadata.onboarding_stageorganizationsOnboarding redirects

RLS Functions

FunctionJWT Fast-PathFallback
is_org_member(org_id)✅ Checks app_metadata.org_idDB lookup on org_memberships
is_org_admin(org_id)✅ Checks app_metadata.org_id + roleDB lookup on org_memberships

RBAC Default Behavior

The RbacProvider defaults to deny-by-default — if a component is rendered outside the provider, hasPermission() returns false. This prevents accidental permission bypasses.

Key Files

FilePurpose
apps/web/lib/auth-context.tsCentralized auth context (getSession + JWT decode)
apps/web/lib/supabase/client.tsBrowser Supabase client
apps/web/lib/supabase/server.tsServer-side Supabase client
apps/web/lib/supabase/session.tsSession refresh logic (middleware)
apps/web/lib/get-sidebar-data.tsSidebar data (uses getAuthContext)
apps/web/providers/rbac-provider.tsxClient RBAC context (deny-by-default)
apps/api/src/auth/supabase.service.tsLocal JWT verification + Supabase clients
apps/api/src/auth/supabase.guard.tsJWT verification guard

UI Components

ComponentLocationusage
LoginForm@tonnex/uiUsed in /login page
SignupForm@tonnex/uiUsed in /signup page