Skip to main content

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.com hint, 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/ssr with two factories — createServerSupabase() (cookies, RLS) for Server Components/actions/route handlers, and createBrowserSupabase() for the client login button. Session refresh happens in the route gate.
  • Profile bootstrap: on first allowed sign-in, a profiles row is created; the first admin for a company becomes owner, the rest staff.
  • Session control surface: the sessions table records each login (with the JWT session_id). The owner force-logout flips revoked; per-role timeout is the row's issued_at age vs profiles.session_timeout_minutes.
  • The gate: src/proxy.ts (Next.js "proxy" convention — the renamed middleware) runs on /admin and /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 reads sessions/profiles through 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 via requireAdmin()/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.ts follows Next.js 16's renamed convention (was middleware.ts); the export is proxy.