5. Authentication, session control, and the auth gate
Date: 2026-06-06
Status
Accepted
Context
Admins sign in with Google SSO and must be restricted to the agency's Workspace (nightingalepm.com). The spec also requires a per-role session timeout and the ability for the owner to force-logout any admin. Supabase Auth issues stateless JWTs, so revocation and absolute timeout cannot be enforced by the token alone — the app needs its own control surface. (Contractor email/password auth and facility magic links come in later steps.)
Decision
- Provider: Supabase Auth with Google. The login button sends an
hd=nightingalepm.comhint, but the authoritative domain check is server-side in the OAuth callback (src/server/auth/domain.ts); a disallowed email is signed out immediately. Consent screen is Internal as a second barrier. - SSR session handling:
@supabase/ssrwith two factories —createServerSupabase()(cookies, RLS) for Server Components/actions/route handlers, andcreateBrowserSupabase()for the client login button. Session refresh happens in the route gate. - Profile bootstrap: on first allowed sign-in, a
profilesrow is created; the first admin for a company becomesowner, the reststaff. - Session control surface: the
sessionstable records each login (with the JWTsession_id). The owner force-logout flipsrevoked; per-role timeout is the row'sissued_atage vsprofiles.session_timeout_minutes. - The gate:
src/proxy.ts(Next.js "proxy" convention — the renamed middleware) runs on/adminand/login. It refreshes the session, redirects unauthenticated users to/login, redirects authenticated users away from/login, and signs out + bounces any request whose session is revoked or timed out. It readssessions/profilesthrough the anon+RLS client (a user sees only their own rows), so no service-role secret enters the edge bundle. Server actions/components re-verify viarequireAdmin()/requireOwner()(src/server/auth/guards.ts).
Consequences
- Force-logout and timeout are enforced at the app layer immediately (the next request is blocked), independent of Supabase JWT expiry.
- The gate adds a Supabase
getUser()call plus one or two RLS reads per matched request — acceptable for a 2–5 admin internal tool; can be optimized later if needed. - Google Cloud + Supabase dashboard configuration is required and documented in
docs/setup/google-sso.md; the OAuth flow can only be tested end-to-end once those credentials are in place. proxy.tsfollows Next.js 16's renamed convention (wasmiddleware.ts); the export isproxy.