Authentication
Tonnex uses Supabase Auth for all authentication needs with enterprise-grade optimizations for performance and security.Architecture
| Component | Client | Purpose |
|---|---|---|
apps/web | @supabase/ssr (Anon Key) | Login, signup, session management |
apps/api | jose + @supabase/supabase-js | Local JWT verification, admin operations |
Web App Auth Flow (Next.js 16)
- User enters credentials on
/loginor creates account on/signup - Upon success, user is redirected to
/home @supabase/ssrhandles Supabase auth call- Session stored as HTTP-only cookies
proxy.tsrefreshes session cookie on every request and callsgetUser()for JWT verificationgetAuthContext()reads fromgetSession()(local cookie, ~0ms) — safe because middleware already verified the token- Server actions and components use
getAuthContext()for token + org context
⚠️ Performance Note:getAuthContext()usesgetSession()(local, ~0ms) instead ofgetUser()(network, ~150-300ms). The middleware already verifies the JWT, so re-verification in server actions is unnecessary.getAuthContext()is deduplicated viaReact.cache().
API Auth Flow
- Web app includes JWT in
Authorization: Bearer <token>header SupabaseGuardextracts token and verifies it locally usingjose+SUPABASE_JWT_SECRET(~0.1ms)- Falls back to Supabase Auth API if local verification fails
- Authenticated user attached to
request.user - All downstream services can access the verified user + JWT claims
🔒 Security: Swagger/API docs are only available in development (NODE_ENV !== 'production'). CORS requiresWEB_URLto be set in production.
User Logout
- Click the Sign out button in the dashboard sidebar.
- The
logout()server action (inapps/web/actions/auth.ts) is triggered. - Supabase session is revoked.
- 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:
| Claim | Source | Purpose |
|---|---|---|
app_metadata.org_id | org_memberships | Tenant isolation |
app_metadata.role | org_memberships | Admin fast-path |
app_metadata.membership_id | org_memberships | RBAC lookups |
app_metadata.location_id | org_memberships | Scope context |
app_metadata.warehouse_id | org_memberships | Scope context |
app_metadata.onboarding_stage | organizations | Onboarding redirects |
RLS Functions
| Function | JWT Fast-Path | Fallback |
|---|---|---|
is_org_member(org_id) | ✅ Checks app_metadata.org_id | DB lookup on org_memberships |
is_org_admin(org_id) | ✅ Checks app_metadata.org_id + role | DB lookup on org_memberships |
RBAC Default Behavior
TheRbacProvider defaults to deny-by-default — if a component is rendered outside the provider, hasPermission() returns false. This prevents accidental permission bypasses.
Key Files
| File | Purpose |
|---|---|
apps/web/lib/auth-context.ts | Centralized auth context (getSession + JWT decode) |
apps/web/lib/supabase/client.ts | Browser Supabase client |
apps/web/lib/supabase/server.ts | Server-side Supabase client |
apps/web/lib/supabase/session.ts | Session refresh logic (middleware) |
apps/web/lib/get-sidebar-data.ts | Sidebar data (uses getAuthContext) |
apps/web/providers/rbac-provider.tsx | Client RBAC context (deny-by-default) |
apps/api/src/auth/supabase.service.ts | Local JWT verification + Supabase clients |
apps/api/src/auth/supabase.guard.ts | JWT verification guard |
UI Components
| Component | Location | usage |
|---|---|---|
LoginForm | @tonnex/ui | Used in /login page |
SignupForm | @tonnex/ui | Used in /signup page |