The entire authentication flow for my SaaS starter

SaaS Starter

Github Repo

A lightweight SaaS starter that shows how I’d architect a multi-tenant app with organizational structure. Fully functional but intentionally unpolished—meant to be cloned, tested, and extended.

It includes authentication, post-signup user management, RBAC org setup, and billing logic. While designed for multi-tenant SaaS, the auth flow works for most apps.

Architecture

Models

Whenever possible, I use dates instead of booleans for state. “Email verified on Jan 6” beats a true/false toggle for both clarity and UX.

Authentication

Some say never build auth from scratch. I disagree. Auth just means verifying identity—and done well, it’s simple, secure, and customizable.

At its core:

From there, you can layer on passkeys, phone sign-in, OAuth, etc.—but you’re still playing by the same basic rules.

Sign Up

There are 3 sign-up methods:

Users can later add a second method. The first two are for org owners and include onboarding. Invitations are for team members—email+password only, no OTP needed.

Email+password

Start with just the email. Confirm it via OTP, then collect the password during onboarding. Cleaner UX and better separation of steps.

Yes, this opens the door to enumeration attacks. A generic “Something went wrong” message protects user data, even if it slightly dents UX.

SSO

The heavy lifting happens on Google’s end. We just:

Organization Invitation

Like standard email+password, but initiated by an org owner. We skip OTP—email link = instant verification + onboarding redirect.

Account Verification

OTP is just another form of password verification. You hash the code and compare it to the input. Simple.

For smoother UX:

User Creation

At it's most simple, your user is going to just be an email. At this point, you might have more information than that, such as from an SSO, but your user model needs to be flexible enough to subsist on an email alone in the signup.

Sessions

Sessions are simple, but critical. A cookie connects user to device. Store minimal info, invalidate regularly. That’s most of it.

Where sessions shine is how you use them—tracking access, managing devices, enabling persistent login without compromising security.

Onboarding

Onboarding isn't technically auth—but it’s the best time to gather info.

Built as a recursive flow, so it’s:

This lets you update onboarding any time (e.g., new compliance needs), and users complete only what’s missing.

Email Login

Classic email+password login—two fields, one comparison. If it matches, start a session.

Again, avoid exposing whether an email exists. Vague errors protect your users. Lock out or alert after multiple failed attempts.

Forgot Password

Same idea: collect email, send OTP, validate, reset password.

Security-wise, it follows the same principles. Don’t confirm if the user exists in your response. Push clarity inside the app, not through the endpoint.

Logout

Logout = session kill. Label the reason, cleanly expire it, and redirect. Handles most cases of unauthenticated access.

User Accesses A Protected Route

If a user hits a protected route without a session, they likely know the app. Redirect them to log in and preserve their flow where possible.

User Can Access The App

Once signed up and onboarded, users are fully authenticated and dropped into the app.

Billing

Billing gating isn’t included in the base repo so you can test it locally without Stripe keys or sandboxing. But billing information is ready to be collected once Stripe is setup. The starter assumes paid access only—no freemium or trial logic.

To add billing gating:

If the sub lapses, let the session stand but gate access. Redirect them to update payment before re-entering.