SaaSyBase
SaaSyBase

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

Non-technical shortcut: you do not need to understand all three providers before you start. Pick one lane and stay there. Choose Clerk for the fastest hosted setup, Better Auth for the preferred self-hosted setup, or NextAuth when you want the simplest self-hosted baseline and do not need Better Auth's organization features yet.

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.

.env.local
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 wantChooseWhy
The fastest managed-auth launchClerkClerk hosts the auth layer and gives you the least setup work inside your own app.
The preferred self-hosted auth laneBetter AuthYou keep auth in your own stack and still get richer organization support than the simpler self-hosted lane.
A simpler self-hosted baselineNextAuthIt is easy to understand, works with the shared Prisma lane, and is a reasonable starting point when you want fewer moving parts.

Note

The .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.

SwitchUser data migration needed?Main caveat
NextAuth ↔ Better AuthUsually noThey share the repo's self-hosted Prisma auth lane, but users may still need to sign in again after a switch.
Clerk ↔ NextAuthYesClerk identities live in Clerk's hosted system, not just in your local Prisma auth rows.
Clerk ↔ Better AuthYesClerk is still a separate identity lane even when Better Auth is enabled.

Note

The current repo keeps NextAuth and Better Auth on a shared self-hosted data lane with compatibility normalization for user rows, credential hashes, verification state, and OAuth account mappings. Clerk remains separate.

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

Choose Better Auth when you want a self-hosted auth stack with stronger built-in organization primitives than NextAuth, but without depending on Clerk.

Official docs: Better Auth docs.

Required environment variables

.env.local
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:

MethodRequirements
Email + passwordWorks immediately. No extra setup beyond the Better Auth base variables.
GitHub OAuthSet GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET
Google OAuthSet GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
Magic linkRequires 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

1

Create OAuth App

Go to GitHub Developer Settings → OAuth Apps → New OAuth App.

2

Set Homepage URL

Set the Homepage URL to your application's base URL.

3

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.

4

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

1

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.

2

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.

3

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").

4

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.

5

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.

6

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, and NEXT_PUBLIC_APP_URL aligned 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_ORIGINS and 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

Better Auth and NextAuth share the same GitHub and Google env var names in this repo. What changes is which provider handles the /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

Choose Clerk when you want the fastest managed-auth path and you are comfortable depending on an external auth platform.

Official docs: Clerk docs.

Required environment variables

.env.local
AUTH_PROVIDER="clerk"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
CLERK_SECRET_KEY=""
CLERK_WEBHOOK_SECRET=""   # Required for webhook-driven user init and welcome emails

How 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 ClerkProvider wraps the app with automatic dark mode theming via a MutationObserver on the <html> class.
  • Organization primitives are powered by Clerk and synced to the local database via webhooks.

Webhook setup (production)

Clerk Webhook Configuration

1

Add Webhook Endpoint

Go to Clerk Dashboard → Webhooks → Add endpoint.

2

Set Endpoint URL

Set the URL to https://yourdomain.com/api/webhooks/clerk (using your actual production domain).

3

Enable Events

Select the following events: user.created, user.updated, organization.*, organizationMembership.*, and organizationInvitation.*.

4

Copy Signing Secret

Once created, copy the endpoint's Signing Secret and set it as CLERK_WEBHOOK_SECRET in your production environment.

Warning

A common first-time mistake is enabling Clerk in the UI and forgetting the webhook. Without the webhook, local app records can fall out of sync with Clerk user and organization changes.

Clerk troubleshooting

ProblemUsual causeFix
ClerkProvider not mounted or missing publishable key errorsAUTH_PROVIDER is set to clerk but NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is not actually present at runtimeVerify the value is in your env or secrets provider setup and restart the app
Failed to load Clerk JSThe publishable key resolved, but the browser cannot fetch Clerk-hosted assetsCheck 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 CloudflareA strict Content Security Policy is blocking Clerk or Cloudflare Turnstile assetsLeave CSP disabled by default, or if you enable it set the required Clerk and CAPTCHA domains explicitly

Note

SaaSyBase ships with CSP disabled by default because auth, payments, analytics, and CAPTCHA providers often need extra origins. If you want a stricter policy later, enable it intentionally and maintain the allowlist for the providers you use.
Optional CSP flag
ENABLE_CSP=true

NextAuth (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

Choose NextAuth when you want the most traditional self-hosted Auth.js lane and do not need provider-backed organization primitives.

Official docs: Auth.js docs.

Note

In the current SaaSyBase schema, NextAuth and Better Auth intentionally coexist on the same self-hosted Prisma auth lane. That means switching between those two providers does not require exporting/importing users, though active sessions can still be recreated.

Required environment variables

.env.local
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:

MethodRequirements
Email + password (credentials)Works immediately. No additional setup.
GitHub OAuthSet GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET
Google OAuthSet 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

1

Create OAuth App

Go to GitHub Developer Settings → OAuth Apps → New OAuth App.

2

Set Homepage URL

Set the Homepage URL to your application's base URL.

3

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.

4

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

1

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.

2

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.

3

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").

4

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.

5

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.

6

Copy Keys

Click "Create". A modal will appear with your Client ID and Client Secret. Copy these into your .env.local file.

Note

If you replace 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

AspectNextAuthClerk
User storageYour own database (Prisma)Clerk-hosted (synced to local DB via webhooks)
Organization primitivesManaged by the app layerBuilt into Clerk, synced via webhooks
Email verificationIn-app pending-change flowClerk handles it natively
Third-party dependencyNone (self-contained)Requires Clerk account
Offline developmentWorks fully offlineRequires internet for Clerk API

When to choose NextAuth vs Better Auth

AspectNextAuthBetter Auth
Session/auth modelAuth.js providers with Prisma adapterBetter Auth server with Better Auth Prisma adapter
OrganizationsManaged primarily by the app layerProvider-backed organization support plus app data model
Local-first setupVery strongVery strong
Best fitClassic Auth.js lane with familiar OAuth patternsSelf-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:

How to check auth in your code
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

Never import @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.

StateBehavior
Temporary suspensionThe user is blocked from signing in and is told to contact support to regain access.
Permanent suspensionThe 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

Suspension is an application-level access control. It does not delete the user or remove their historical billing, subscription, or audit data.