Codapult uses the adapter pattern for authentication. Two providers are supported out of the box — switch between them with a single environment variable, no code changes required.
| Provider | Type | Key features | | ------------------------- | ----------- | -------------------------------------------------------------------------- | | Better-Auth (default) | Self-hosted | Email/password, OAuth, magic links, TOTP 2FA, passkeys, user impersonation | | Kinde | Hosted | Managed auth pages, OAuth, SSO, built-in user management |
Choosing a provider
Set AUTH_PROVIDER in your .env.local:
# Better-Auth (default — omit to use this)
AUTH_PROVIDER="better-auth"
# Kinde
AUTH_PROVIDER="kinde"
# Disable auth entirely (public-only app, no sign-in)
AUTH_PROVIDER="none"
Both providers produce the same AppSession object used throughout the app, so all downstream code (guards, server actions, API routes) works identically regardless of which provider is active.
Better-Auth setup
Better-Auth is the default provider. It stores sessions and accounts in your database (Turso or PostgreSQL).
Required variables
BETTER_AUTH_SECRET="your-secret-here" # openssl rand -base64 32
BETTER_AUTH_URL="http://localhost:3000" # your app URL
Sign-in methods
Better-Auth supports multiple sign-in methods, all configurable in src/config/app.ts:
auth: {
oauthProviders: ['google', 'github'],
magicLink: true,
passkeys: true,
twoFactor: true,
},
Disable any method by removing it from the array or setting the boolean to false. The sign-in page automatically adjusts to show only enabled methods.
Kinde setup
Kinde provides fully managed, hosted authentication pages.
Required variables
AUTH_PROVIDER="kinde"
KINDE_CLIENT_ID="your-client-id"
KINDE_CLIENT_SECRET="your-client-secret"
KINDE_ISSUER_URL="https://your-app.kinde.com"
KINDE_SITE_URL="http://localhost:3000"
KINDE_POST_LOGOUT_REDIRECT_URL="http://localhost:3000"
KINDE_POST_LOGIN_REDIRECT_URL="http://localhost:3000/dashboard"
Create an application in the Kinde dashboard, copy the credentials, and add the callback URLs.
OAuth providers
OAuth buttons appear on the sign-in page when the corresponding credentials are configured. Codapult supports Google, GitHub, Apple, Discord, Twitter, and Microsoft.
- Go to the Google Cloud Console
- Create an OAuth 2.0 Client ID (Web application)
- Add authorized redirect URI:
https://your-app.com/api/auth/callback/google - Copy the credentials to
.env.local:
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="GOCSPX-your-secret"
GitHub
- Go to GitHub Developer Settings → OAuth Apps
- Create a new OAuth App
- Set the authorization callback URL:
https://your-app.com/api/auth/callback/github - Copy the credentials to
.env.local:
GITHUB_CLIENT_ID="your-client-id"
GITHUB_CLIENT_SECRET="your-client-secret"
Adding more providers
Add the provider name to the oauthProviders array in src/config/app.ts, then add the corresponding environment variables. Supported values: 'google', 'github', 'apple', 'discord', 'twitter', 'microsoft'.
Magic links
Magic links provide passwordless email sign-in. When a user enters their email, they receive a link that signs them in directly — no password required.
Prerequisites
RESEND_API_KEYmust be set (magic links are sent via Resend)magicLink: trueinsrc/config/app.ts
The magic link expires after 10 minutes. If the user doesn't have an account, one is created automatically.
Two-factor authentication (2FA)
Codapult supports TOTP-based two-factor authentication using authenticator apps (Google Authenticator, Authy, 1Password, etc.).
Setup flow for end-users
- Navigate to Settings → Two-Factor Authentication
- Click Enable 2FA
- Scan the QR code with an authenticator app
- Enter the 6-digit verification code to confirm
- Save the backup codes in a secure location
Once enabled, users are prompted for a TOTP code after entering their password on sign-in.
Configuration
Set twoFactor: true in src/config/app.ts to enable the 2FA option in user settings. The TOTP issuer name (shown in authenticator apps) is read from appConfig.brand.name.
Passkeys (WebAuthn)
Passkeys let users sign in with biometrics (fingerprint, face ID) or hardware security keys instead of passwords.
Prerequisites
passkeys: trueinsrc/config/app.ts- HTTPS in production (WebAuthn requires a secure origin)
The passkey relying party ID and origin are derived from NEXT_PUBLIC_APP_URL automatically.
Enterprise SSO (SAML)
Codapult includes enterprise-grade SSO via BoxyHQ Jackson, supporting SAML 2.0 identity providers (Okta, Azure AD, Google Workspace, OneLogin, etc.).
Admin setup
- Go to Admin → Enterprise SSO
- Click Add Connection
- Enter the tenant slug (matches the organization) and paste the IdP metadata XML or URL
- Share the following with the customer's IT team:
- ACS URL:
https://your-app.com/api/auth/sso/callback - Entity ID / Audience:
https://your-app.com/api/auth/sso
- ACS URL:
Environment variables
SSO_PROVIDER="jackson"
SSO_PRODUCT="your-product-name"
# Production: use Postgres for durable storage
SSO_DB_ENGINE="sql"
SSO_DB_TYPE="postgres"
SSO_DB_URL="postgres://user:password@host:5432/jackson"
For development, Jackson defaults to in-memory storage (no database configuration needed). For production, configure a PostgreSQL database for persistent SSO connections.
SSO sign-in flow
- User enters their email on the sign-in page
- Codapult detects the SSO connection by email domain
- User is redirected to their company's identity provider
- After IdP authentication, the user is redirected back and signed in
Auth guards
Codapult provides server-side guard functions for protecting API routes, server actions, and pages.
getAppSession()
Returns the current session or null. Use in API routes and server components:
import { getAppSession } from '@/lib/auth';
export async function GET() {
const session = await getAppSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// session.user.id, session.user.email, session.user.role
}
requireAuth()
Throws if the user is not authenticated. Returns an AuthContext with the session:
import { requireAuth } from '@/lib/guards';
export async function someAction() {
const { session } = await requireAuth();
// session is guaranteed to exist
}
Organization guards
| Guard | Description |
| ----------------------------------------- | -------------------------------------------------- |
| requireOrgMembership(orgId) | User must be a member of the organization |
| requireOrgAdmin(orgId) | User must be an owner or admin of the organization |
| requireOrgPermission(orgId, permission) | User must have a specific RBAC permission |
All org guards return an OrgAuthContext with the session, orgId, and the user's role. Global admins bypass org permission checks automatically.
import { requireOrgPermission } from '@/lib/guards';
export async function updateProject(orgId: string, data: unknown) {
const { session, role } = await requireOrgPermission(orgId, 'project:update');
// proceed with the update
}
Session shape
Both providers produce the same AppSession interface:
interface AppSession {
user: {
id: string;
name: string;
email: string;
image?: string | null;
role: UserRole; // 'user' | 'admin'
};
impersonatedBy?: string; // set when an admin is impersonating
}
Auth API endpoints
These endpoints are available at /api/auth/ when using Better-Auth:
| Endpoint | Method | Description |
| -------------------------------- | ------ | --------------------------------------------------- |
| /api/auth/sign-up/email | POST | Create a new account with email and password |
| /api/auth/sign-in/email | POST | Sign in with email and password |
| /api/auth/sign-in/social | POST | Initiate OAuth sign-in (provider specified in body) |
| /api/auth/sign-out | POST | Sign out and clear the session cookie |
| /api/auth/get-session | GET | Get the current session |
| /api/auth/magic-link/send | POST | Send a magic link email |
| /api/auth/two-factor/enable | POST | Enable TOTP 2FA for the current user |
| /api/auth/two-factor/verify | POST | Verify a TOTP code during sign-in |
| /api/auth/passkey/register | POST | Register a new passkey |
| /api/auth/passkey/authenticate | POST | Authenticate with a passkey |
| /api/auth/sso/callback | POST | SAML SSO assertion consumer service (ACS) |
Sessions are stored as HTTP-only cookies with a 5-minute cache for performance. The cookie is automatically refreshed on each request.