Authentication
Perfxcel uses Supabase Auth for the admin dashboard. The public-facing site (perfxcel.com) has no authentication — all public routes are open.
Two-Tier Auth Model
| Tier | Surface | Auth Required | Mechanism |
|---|---|---|---|
| Public | perfxcel.com |
No | Open access; rate-limited |
| Admin | admin.perfxcel.com |
Yes | Supabase JWT + perfxcel tenant check |
Admin Login Flow
sequenceDiagram
participant A as admin.perfxcel.com
participant SB as Supabase Auth
participant API as api.perfxcel.com
A->>SB: supabase.auth.signInWithPassword(email, password)
SB-->>A: Session (JWT)
A->>API: GET /api/v1/metrics (Bearer JWT) — liveness check
API->>SB: supabase.auth.getUser(token)
SB-->>API: { user }
API->>API: requirePerfxcelTenant — DB lookup to verify perfxcel membership
API-->>A: 200 OK
A->>A: setSession — show dashboard
On session load, perfxcel-admin calls GET /api/v1/metrics as a liveness check. If this call fails (e.g. revoked session), the admin is signed out immediately.
requireAuth Middleware
Validates the Authorization: Bearer <token> header using supabase.auth.getUser(token) and attaches req.user.
- Returns
401for missing or malformed headers. - Returns
403for invalid or expired tokens. - Dev bypass: when
SUPABASE_URLorSUPABASE_SERVICE_ROLE_KEYare unset, auth is skipped with a warning log. Never remove this guard.
requirePerfxcelTenant Middleware
After requireAuth, this middleware performs a DB lookup to confirm the authenticated user is a member of the perfxcel tenant.
- Reads the user's membership from the
publicschema via a separateportalSupabaseclient. - Sets
req.tenantIdon success. - Throws
AuthorizationErrorif the tenant or membership is not found.
sequenceDiagram
participant M as requirePerfxcelTenant
participant PS as portalSupabase (public schema)
M->>PS: SELECT tenants.id FROM tenants JOIN user_tenant_roles WHERE user_id = req.user.id AND slug = 'perfxcel'
PS-->>M: { tenantId }
M->>M: req.tenantId = tenantId
M->>M: next()
SSO Support
The admin dashboard supports SSO via Supabase Auth providers (configured at the Supabase project level). No additional configuration is required in the application code.
Session Management
Session state is managed in App.tsx via:
supabase.auth.getSession(); // initial load
supabase.auth.onAuthStateChange(); // ongoing session changes
There is no token refresh logic in the admin app — Supabase handles refresh automatically.
Bot Protection (Public Routes)
Interest registration (POST /api/v1/courses/:id/interest), training plan requests, and contact form submissions are protected by Cloudflare Turnstile:
- The public site renders a Turnstile widget (
@marsidev/react-turnstile). - On form submit the token is included in the request body as
turnstileToken. - The API verifies the token against
https://challenges.cloudflare.com/turnstile/v0/siteverifybefore inserting any record. - Allowed hostnames are controlled by
VITE_PERFXCEL_TURNSTILE_HOSTNAMES(comma-separated).