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.
Sign-In Methods
Beyond email/password, Codapult supports several additional authentication methods:
| Method | Description | Docs |
|---|---|---|
| OAuth (6 providers) | Google, GitHub, Apple, Discord, Twitter, Microsoft | OAuth Providers → |
| Magic links | Passwordless email sign-in | 2FA & Passwordless → |
| Two-factor (TOTP) | Authenticator app verification | 2FA & Passwordless → |
| Passkeys (WebAuthn) | Biometric / hardware key sign-in | 2FA & Passwordless → |
| Enterprise SSO (SAML) | Okta, Azure AD, Google Workspace, OneLogin | Enterprise SSO → |
All methods are configured in src/config/app.ts under the auth key.
Testing Auth Locally
Seed the database to create a test user for local development:
pnpm db:seed
This creates a test user you can sign in with. Check src/lib/db/seed.ts for the seeded credentials.
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 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 session cookie |
/api/auth/get-session | GET | Get the current session |
For magic link, 2FA, passkey, and SSO endpoints, see the respective documentation pages linked above.
Sessions are stored as HTTP-only cookies with a 5-minute cache for performance.