
SaaS Starter
Github RepoA 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
- Frontend/Backend: Next.js
- Database: Postgres with Prisma
- Components: Shadcn
- Styling: Tailwind
-
Services:
- Stripe: Billing
- Google: For SSO
- IPinfo: For extended IP information to add to sessions

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.
- User: Basic info + status + auth/security fields
- Onboarding: Recursive wizard-style form
- Session: Standard plus IPinfo-enriched data
- Address: Overkill for most apps, but useful for SEO and localized flows

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:
- Collect an email + password
- Verify ownership (OTP or magic link)
- Store credentials securely
- Handle session and device tracking
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:
- Email+password
- SSO (Google)
- Organization Invitation
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:
- Check if the user exists
- If connected → create session
- If not → confirm intent and connect

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:
- Send a link to auto-verify where possible
- Limit attempts, notify on failures
- Redirect based on flow: sign-up → onboarding, forgot password → reset
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:
- Familiar
- Extensible
- Asynchronous
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:
- Collect plan + payment during onboarding
- Send to Stripe
- Store on the organization model
- On each load, check subscription validity before granting access
If the sub lapses, let the session stand but gate access. Redirect them to update payment before re-entering.