Auth Flows
Ozeaon delegates identity to Supabase Auth. Sign-in, sign-up, password reset, and email verification are all server actions in src/lib/supabase/actions.ts. Route gating happens in server component layouts using getAuthUserOrRedirect(). There is no custom session cookie and no client-side redirect logic.
Overview
Section titled “Overview”All auth routes live in the (auth) route group under src/app/(auth)/. The layout renders AuthMarketingPanel alongside the form panel. The pages are:
/login—LoginPageFormcallsuseAuth()to submit theloginserver action./signup— requires anACCESS_TOKENgate; creates the Supabase auth user only (profile is created on email OTP verification)./forgot-password— callsforgotPassword; sends a Supabase reset email./reset-password— callsresetPasswordafter OTP or link verification./verify-email— shows the in-app OTP form after signup.
AuthFormPanel is a presentational shell. LoginPageForm holds the submit logic and reads session state via useAuth().
Google sign-in exists as src/components/auth/GoogleSignIn.tsx but is not rendered anywhere — it is not a live sign-in option.
Architecture
Section titled “Architecture”Server Actions
Section titled “Server Actions”All auth flows are server actions. The key ones:
login—signInWithPassword, then reads the profile and returns{ user, activeAccount }directly soSessionProvidercan hydrate without a second round-trip.signup— checksACCESS_TOKENbefore callingsupabase.auth.signUp. The Supabase email OTP is sent at this point; the profile row is not created untilverifyEmailOtpsucceeds.verifyEmailOtp— verifies the OTP, generates a unique username fromdisplay_name, and insertsuser_profilesanduser_settingsrows via the admin client (to bypass RLS on first-time profile creation).forgotPassword— callssupabase.auth.resetPasswordForEmail. Supabase sends a PKCE link; in-app OTP recovery is handled byverifyOtpForRecovery.verifyOtpForRecovery— verifies the recovery OTP and setsuser_metadata.password_reset_pending = trueto force a password reset before any other navigation.resetPassword— callssupabase.auth.updateUserwith the new password and clearspassword_reset_pending.resendVerificationEmail— callssupabase.auth.resendforsignuporemail_changetypes.signOut— clearspassword_reset_pendingif set, signs out, callsclearActiveAccount, and redirects to/.
/auth/confirm Callback
Section titled “/auth/confirm Callback”src/app/(auth)/auth/confirm/route.ts is the GET route that Supabase links point to when email links are used (as opposed to in-app OTP). It handles three cases in order:
- Error params — if
erroris present, redirects to/auth/errorwith the encodederror_description. - PKCE code — if
codeis present, callsexchangeCodeForSession. Password reset links carrynext=/reset-password. Any othernextvalue from the link is honoured; the default is/. - Legacy OTP hash — if
token_hashandtypeare present, callssupabase.auth.verifyOtp. Recovery types redirect to/reset-password; others redirect tonextor/.
flowchart TD
A["/auth/confirm?..."] --> B{error param?}
B -->|yes| C["/auth/error?message=..."]
B -->|no| D{code param?}
D -->|yes| E[exchangeCodeForSession]
E -->|ok| F["redirect next or /"]
E -->|fail| C
D -->|no| G{token_hash + type?}
G -->|yes| H[verifyOtp]
H -->|recovery| I["/reset-password"]
H -->|other| F
H -->|fail| C
G -->|no| C
/auth/error Page
Section titled “/auth/error Page”src/app/auth/error/page.tsx reads only the message search param and shows a static “Link is invalid or has expired” heading with a default fallback message. It lives outside the (auth) route group. An AuthError component on (main)/error handles runtime errors and reads code; these are separate.
Route Gating
Section titled “Route Gating”authorizeUser() in src/lib/supabase/auth.ts calls supabase.auth.getUser() and redirects to /login. It exists in the codebase but has no callers — it is dead code.
The real guard is getAuthUserOrRedirect() in src/lib/supabase/queries/auth.ts. It calls getAuthUser() (React-cached) and redirects to /login if there is no user. The (dashboard) layout calls it for all settings and org routes. Individual pages in that group also call it to get typed user and activeAccount values.
Feed, reader, and profile routes in (main)/(feed)/(public) and (main)/(profile) are public — they do not call getAuthUserOrRedirect. Some inner pages in (feed)/(private) call it directly.
Password-reset Guard
Section titled “Password-reset Guard”The middleware (src/middleware.ts) runs getClaims() on every request. If user_metadata.password_reset_pending is set, the middleware calls getUser() and redirects any route except /reset-password and /auth/signout to /reset-password. This prevents a user who requested a password reset from navigating away before completing it.
Failure Modes & Edge Cases
Section titled “Failure Modes & Edge Cases”- Unconfirmed email on login —
loginchecksemail_confirmed_atand signs the session back out, returning an error rather than leaving an unconfirmed session active. - Email already exists on signup —
signupdetectsidentities.length === 0and returns a “log in instead” message, avoiding a Supabase error leaking internal state. - Double confirm changes —
verifyEmailChangechecks thatdata.user.emailactually moved to the new address after OTP verification, because GoTrue may silently wait for a second confirmation underdouble_confirm_changes. If the email hasn’t moved, an explanatory error is returned. - Expired /auth/confirm link — the route redirects to
/auth/errorwith the Supabase error message; the page shows a “Reset password again” link. password_reset_pendingon sign-out —signOutclears the flag before signing out, so a re-login is not immediately redirected to/reset-password.
Operational Notes
Section titled “Operational Notes”getAuthUser is wrapped in React’s cache(), so it resolves at most once per server render across all layouts and pages that call it in the same request tree. withAuthUser (used by API routes) also calls getAuthUser and injects the user into the route handler context via withContext for structured logging.
Supabase auth emails (verification, password reset) are sent by Supabase directly. The only application-sent auth-adjacent emails are the email-change security notice and the account-deletion confirmation (see User Settings & Account Management).
Related Links
Section titled “Related Links”- Account Switching & Active Account — org mode and the active account cookie
- User Settings & Account Management — email/password change dialogs and account deletion
- Middleware & Sessions — the password-reset guard and session cookie mechanics