Authentication
Google OAuth and email sign-in, plus session management.
routess supports two sign-in methods, Google OAuth (one-tap) and email + password, both ending in the same JWT session cookie on the API.
Google flow
- The web app loads the Google Identity Services SDK and renders the sign-in button.
- On success, Google returns an ID token to the browser.
- Web posts the ID token to
POST /api/v1/auth/google. - The API verifies the token's signature and audience against Google's JWKS.
- The API upserts the user (by Google
sub), issues its own JWT, and sets it as anHttpOnly,Secure,SameSite=Laxcookie. - Subsequent API calls include the cookie automatically.
Email flow
POST /api/v1/auth/signup-emailregisters the account (passwords are at least 12 characters, hashed at rest) and sends a verification link, valid for 24 hours.POST /api/v1/auth/verify-emailconsumes the link's token and signs the user in.POST /api/v1/auth/login-emailsigns in on later visits and issues the same session cookie as the Google flow.POST /api/v1/auth/request-password-resetemails a reset link valid for 30 minutes;POST /api/v1/auth/reset-passwordconsumes it.
Email delivery goes through Resend when RESEND_API_KEY is set; otherwise verification and reset URLs are logged to stdout (the dev/self-host console transport).
A Google-created account can add a password later (POST /api/v1/users/me/password), so both methods work for the same user.
Why a server-issued JWT?
- Lets us add app-specific claims (user role, default unit)
- Decouples session lifetime from Google's token
- We don't need to call Google on every request
Sign-out
POST /api/v1/auth/logout clears the cookie. The web app also clears Zustand auth state.
Session refresh
JWTs are short-lived (15 minutes). The API refreshes silently on each authenticated request, returning a fresh cookie. If the user is gone for too long, the cookie expires and they re-sign-in via Google's silent re-auth (no user interaction in most cases).