Authentication
SaaSyBase ships with three fully implemented auth providers that you can swap with a single environment variable. This guide explains how the system works, how to configure each provider, and how to integrate against the auth abstraction safely.
Note
How the Auth Provider System Works
Instead of locking you into one auth provider, SaaSyBase uses an abstraction layer that sits between your app code and the actual auth provider. The rest of the codebase never imports Clerk, Better Auth, or NextAuth directly — it always goes through lib/auth-provider/.
This means switching providers is a configuration change, not a code rewrite. The abstraction defines a full AuthProvider interface covering sessions, user management, organizations, webhooks, and middleware. All three providers implement this interface completely.
AUTH_PROVIDER="betterauth" # Options: "clerk", "nextauth", "betterauth"The build system automatically exposes this as NEXT_PUBLIC_AUTH_PROVIDER to the client bundle so that unused provider code is eliminated at build time (dead-code elimination).
| If you want | Choose | Why |
|---|---|---|
| The fastest managed-auth launch | Clerk | Clerk hosts the auth layer and gives you the least setup work inside your own app. |
| The preferred self-hosted auth lane | Better Auth | You keep auth in your own stack and still get richer organization support than the simpler self-hosted lane. |
| A simpler self-hosted baseline | NextAuth | It is easy to understand, works with the shared Prisma lane, and is a reasonable starting point when you want fewer moving parts. |
Note
.env.example template ships with AUTH_PROVIDER="betterauth" so you can start locally without any third-party accounts. If you remove that variable entirely, the runtime also falls back to betterauth. In practice, new setups start on Better Auth by default, and you should keep that variable explicit in every environment.What provider switching actually means
Switching AUTH_PROVIDER changes which auth runtime owns the sign-in UI, session handling, and /api/auth behavior. It does not mean every provider shares the same identity store.
| Switch | User data migration needed? | Main caveat |
|---|---|---|
| NextAuth ↔ Better Auth | Usually no | They share the repo's self-hosted Prisma auth lane, but users may still need to sign in again after a switch. |
| Clerk ↔ NextAuth | Yes | Clerk identities live in Clerk's hosted system, not just in your local Prisma auth rows. |
| Clerk ↔ Better Auth | Yes | Clerk is still a separate identity lane even when Better Auth is enabled. |
Note
Better Auth
Better Auth is the preferred self-hosted auth lane when you want provider-managed sessions plus app-managed organizations without depending on Clerk. It runs locally against your own database and still participates in the shared auth abstraction.
Tip
Official docs: Better Auth docs.
Required environment variables
AUTH_PROVIDER="betterauth"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
BETTER_AUTH_SECRET="" # Generate with: npx auth secret
AUTH_SECRET="" # Keep aligned with BETTER_AUTH_SECRET
NEXTAUTH_SECRET="" # Optional compatibility fallback
# Optional only when local dev uses a custom host or tunnel:
# ALLOWED_DEV_ORIGINS="app.localhost.test,*.ngrok-free.dev"
# Optional OAuth providers:
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""For plain email-and-password sign-in, the first five values are the important ones. GitHub, Google, and magic-link flows are optional extras you add only when you are ready to support them.
How Better Auth works in SaaSyBase
- Supports local credentials, magic links, and app-managed organizations through the Better Auth server.
- Uses the same
lib/auth-provider/abstraction surface as Clerk and NextAuth. - Does not require inbound auth webhooks in the local self-hosted lane.
- Uses the shared workspace switcher and active-organization flow exposed by the app.
- Shares the repo's Prisma-backed self-hosted auth lane with NextAuth so user records do not need a separate migration just to switch between those two providers.
Supported sign-in methods
Better Auth now supports these user-facing methods in the shipped SaaSyBase UI:
| Method | Requirements |
|---|---|
| Email + password | Works immediately. No extra setup beyond the Better Auth base variables. |
| GitHub OAuth | Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET |
| Google OAuth | Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET |
| Magic link | Requires SMTP or another configured email provider |
The Better Auth sign-in and sign-up forms only show GitHub and Google buttons when both env vars for that provider are configured.
GitHub OAuth setup
Official setup reference: GitHub OAuth app docs.
GitHub OAuth Configuration
Create OAuth App
Go to GitHub Developer Settings → OAuth Apps → New OAuth App.
Set Homepage URL
Set the Homepage URL to your application's base URL.
Set Authorization Callback URL
For local development, use http://localhost:3000/api/auth/callback/github. For production, use https://your-domain.com/api/auth/callback/github.
Copy Credentials
Copy the generated Client ID and Client Secret into your .env.local file or your platform's environment settings.
Google OAuth setup
Official setup reference: Google OAuth docs.
Google OAuth Configuration
Select or Create a Project
Go to the Google Cloud Console. Click the project dropdown in the top navigation bar and select an existing project or click "New Project" to create one dedicated to your application.
Configure OAuth Consent Screen
Navigate to "APIs & Services" → "OAuth consent screen" (or "Google Auth Platform"). Choose "External" user type, then fill in your App Name, support email, and developer contact information. Complete the setup wizard.
Create OAuth Client
From the Google Auth Platform Overview, click "Create OAuth client" (or navigate to "Clients" on the left menu and click Create). Choose "Web application" as the Application type and give it a name (e.g., "SaaSyBase Login").
Add Authorized JavaScript Origins
Under "Authorized JavaScript origins", click "Add URI". For local development, add http://localhost:3000. For production, add your domain like https://your-domain.com.
Add Authorized Redirect URIs
Under "Authorized redirect URIs", click "Add URI". For local development, add http://localhost:3000/api/auth/callback/google. For production, add https://your-domain.com/api/auth/callback/google.
Copy Keys
Click "Create". A modal will appear with your Client ID and Client Secret. Copy these into your .env.local file.
Better Auth OAuth notes
- Keep
AUTH_PROVIDER="betterauth". - Keep
BETTER_AUTH_URL,NEXT_PUBLIC_BETTER_AUTH_URL, andNEXT_PUBLIC_APP_URLaligned with the exact base URL you registered with GitHub and Google. - If that local base URL is a custom hostname or tunnel instead of plain localhost, add it to
ALLOWED_DEV_ORIGINSand restart the dev server so Next.js dev assets are not blocked. - The external callback path is
/api/auth/callback/<provider>, routed through the active auth provider. - Leaving GitHub or Google env vars blank disables that provider cleanly without affecting credentials or magic-link auth.
- If you use the optional Infisical or Doppler bootstrap, store the OAuth client secret values there and keep only non-secret auth config in normal env settings.
Tip
/api/auth routes at runtime.Clerk
Clerk is a hosted auth service that handles user management, sign-in/sign-up UI, and organization primitives. Choose Clerk if you want a fully managed auth solution with minimal server-side complexity.
Tip
Official docs: Clerk docs.
Required environment variables
AUTH_PROVIDER="clerk"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
CLERK_SECRET_KEY=""
CLERK_WEBHOOK_SECRET="" # Required for webhook-driven user init and welcome emailsHow Clerk works in SaaSyBase
- UI components (
<AuthSignIn>,<AuthSignUp>, etc.) are re-exported from the abstraction layer and switch automatically based on the provider. - Clerk's
ClerkProviderwraps the app with automatic dark mode theming via aMutationObserveron the<html>class. - Organization primitives are powered by Clerk and synced to the local database via webhooks.
Webhook setup (production)
Clerk Webhook Configuration
Add Webhook Endpoint
Go to Clerk Dashboard → Webhooks → Add endpoint.
Set Endpoint URL
Set the URL to https://yourdomain.com/api/webhooks/clerk (using your actual production domain).
Enable Events
Select the following events: user.created, user.updated, organization.*, organizationMembership.*, and organizationInvitation.*.
Copy Signing Secret
Once created, copy the endpoint's Signing Secret and set it as CLERK_WEBHOOK_SECRET in your production environment.
Warning
Clerk troubleshooting
| Problem | Usual cause | Fix |
|---|---|---|
| ClerkProvider not mounted or missing publishable key errors | AUTH_PROVIDER is set to clerk but NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is not actually present at runtime | Verify the value is in your env or secrets provider setup and restart the app |
| Failed to load Clerk JS | The publishable key resolved, but the browser cannot fetch Clerk-hosted assets | Check the Network tab, confirm the publishable key maps to the expected Clerk instance, and rule out privacy blockers or browser shields |
| Failed to load the CAPTCHA script from Cloudflare | A strict Content Security Policy is blocking Clerk or Cloudflare Turnstile assets | Leave CSP disabled by default, or if you enable it set the required Clerk and CAPTCHA domains explicitly |
Note
ENABLE_CSP=trueNextAuth (Auth.js v5)
NextAuth stores your local user records in your own database through the Prisma adapter. Choose NextAuth if you want full control over user data, zero third-party dependencies, and the ability to run offline.
For OAuth sign-in, the credential still originates from the upstream provider such as GitHub or Google, and the local user record is created or updated on first successful login. That means your app state stays in your database even though the external identity proof comes from the OAuth provider.
Tip
Official docs: Auth.js docs.
Note
Required environment variables
AUTH_PROVIDER="nextauth"
AUTH_SECRET="" # Generate with: npx auth secret
# Optional only when local dev uses a custom host or tunnel:
# ALLOWED_DEV_ORIGINS="app.localhost.test,*.ngrok-free.dev"
# Optional OAuth providers:
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""Supported sign-in methods
NextAuth supports multiple methods out of the box — enable the ones you need:
| Method | Requirements |
|---|---|
| Email + password (credentials) | Works immediately. No additional setup. |
| GitHub OAuth | Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET |
| Google OAuth | Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET |
| Magic link (passwordless email) | Requires SMTP configuration (see Email & Notifications) |
The sign-in and sign-up UI automatically shows buttons only for the providers that are actually configured. Leaving OAuth variables blank simply hides those options without breaking anything.
GitHub OAuth setup
Official setup reference: GitHub OAuth app docs.
GitHub OAuth Configuration
Create OAuth App
Go to GitHub Developer Settings → OAuth Apps → New OAuth App.
Set Homepage URL
Set the Homepage URL to your application's base URL.
Set Authorization Callback URL
For local development, use http://localhost:3000/api/auth/callback/github. For production, use https://your-domain.com/api/auth/callback/github.
Copy Credentials
Copy the generated Client ID and Client Secret into your .env.local file.
Google OAuth setup
Official setup reference: Google OAuth docs.
Google OAuth Configuration
Select or Create a Project
Go to the Google Cloud Console. Click the project dropdown in the top navigation bar and select an existing project or click "New Project" to create one dedicated to your application.
Configure OAuth Consent Screen
Navigate to "APIs & Services" → "OAuth consent screen" (or "Google Auth Platform"). Choose "External" user type, then fill in your App Name, support email, and developer contact information. Complete the setup wizard.
Create OAuth Client
From the Google Auth Platform Overview, click "Create OAuth client" (or navigate to "Clients" on the left menu and click Create). Choose "Web application" as the Application type and give it a name (e.g., "SaaSyBase Login").
Add Authorized JavaScript Origins
Under "Authorized JavaScript origins", click "Add URI". For local development, add http://localhost:3000. For production, add your domain like https://your-domain.com.
Add Authorized Redirect URIs
Under "Authorized redirect URIs", click "Add URI". For local development, add http://localhost:3000/api/auth/callback/google. For production, add https://your-domain.com/api/auth/callback/google.
Copy Keys
Click "Create". A modal will appear with your Client ID and Client Secret. Copy these into your .env.local file.
Note
http://localhost:3000 with a custom local hostname or tunnel during development, keep your auth URL env vars aligned with that host and add it to ALLOWED_DEV_ORIGINS before restarting the dev server.Key differences from Clerk
| Aspect | NextAuth | Clerk |
|---|---|---|
| User storage | Your own database (Prisma) | Clerk-hosted (synced to local DB via webhooks) |
| Organization primitives | Managed by the app layer | Built into Clerk, synced via webhooks |
| Email verification | In-app pending-change flow | Clerk handles it natively |
| Third-party dependency | None (self-contained) | Requires Clerk account |
| Offline development | Works fully offline | Requires internet for Clerk API |
When to choose NextAuth vs Better Auth
| Aspect | NextAuth | Better Auth |
|---|---|---|
| Session/auth model | Auth.js providers with Prisma adapter | Better Auth server with Better Auth Prisma adapter |
| Organizations | Managed primarily by the app layer | Provider-backed organization support plus app data model |
| Local-first setup | Very strong | Very strong |
| Best fit | Classic Auth.js lane with familiar OAuth patterns | Self-hosted lane with stronger built-in organization primitives |
The Auth Abstraction Layer
If you're building new features that need to check who is signed in, always import from the abstraction layer — never from Clerk, Better Auth, or NextAuth directly:
import { authService } from '@/lib/auth-provider/service';
// In an API route or server component:
const { userId, orgId } = await authService.getSession();
// To require authentication (throws if not signed in):
const userId = await authService.requireUserId();If you still need to create your first administrator, use the dedicated Admin Setup page rather than mixing auth configuration with role promotion steps. If you are still deciding how to store secrets for OAuth credentials, pair this guide with Secrets & Providers.
Warning
@clerk/nextjs, better-auth, or next-auth directly in your business logic. Always use the lib/auth-provider/ abstraction. This ensures your code works across all supported providers.User suspensions
User suspension is enforced in the app database rather than delegated to a provider-specific feature. That keeps the behavior consistent across Clerk, Better Auth, and NextAuth.
| State | Behavior |
|---|---|
| Temporary suspension | The user is blocked from signing in and is told to contact support to regain access. |
| Permanent suspension | The user is blocked from signing in and sees a permanent-suspension message on the sign-in flow. |
Admins control this from /admin/users. Credentials sign-in, OAuth completion, magic-link sign-in, and provider-backed session resolution all read the same suspension state.
- Admins choose whether a suspension is temporary or permanent when they apply it.
- The admin user list and edit modal expose the suspension type, timestamp, and reason so support staff can see why access was removed.
- When a suspended user attempts to sign in, the app surfaces a suspension-specific message instead of a generic auth failure.
Note

