Authentication
Authentication is handled by BetterAuth integrated with Convex via the @convex-dev/better-auth adapter. All configuration is dynamic — stored in the Convex config table and applied per request, so changes take effect without redeployment.
Auth flows
Section titled “Auth flows”Email & password
Section titled “Email & password”The primary authentication method. Supports sign-up, sign-in, password reset, and email verification.
import { authClient } from "@/lib/core/auth-client"
// Sign upawait authClient.signUp.email({ name, email, password })
// Sign inawait authClient.signIn.email({ email, password })
// Request password resetawait authClient.forgetPassword({ email })
// Sign outawait authClient.signOut()OAuth providers
Section titled “OAuth providers”Google, GitHub, Apple, and Microsoft can be enabled dynamically via the admin config panel. Each provider requires a clientId and clientSecret set through the socialProviders.* config keys.
await authClient.signIn.social({ provider: "google" })Passkeys (WebAuthn)
Section titled “Passkeys (WebAuthn)”When auth.passkeyEnabled is true, users can register FIDO2 passkeys. A custom passkeyEnforce plugin ensures that once a user has a registered passkey, password sign-in creates then immediately revokes the session, returning { passkeyRequired: true } to trigger the WebAuthn flow instead.
// Register a passkeyawait authClient.passkey.addPasskey()
// Sign in with passkeyawait authClient.signIn.passkey()Two-factor authentication (TOTP)
Section titled “Two-factor authentication (TOTP)”Users can enable TOTP-based 2FA. The mfaOrgGuard plugin enforces MFA at the organization level — if an org’s metadata includes require2FA, members must have TOTP or passkeys configured before they can activate that organization.
// Enable 2FA (returns TOTP URI + backup codes)await authClient.twoFactor.enable({ password })
// Verify during sign-inawait authClient.twoFactor.verifyTotp({ code })Email verification
Section titled “Email verification”Sent automatically on sign-up when auth.requireEmailVerification is true. Users who sign up through an organization invitation skip verification — the invitationVerify plugin auto-verifies their email.
Organizations
Section titled “Organizations”Multi-tenant organization support is built in. Each user can belong to multiple organizations with role-based access.
Organization roles
Section titled “Organization roles”| Role | Scope |
|---|---|
| owner | Full control — update, delete org, manage members, all resources |
| admin | Update org, manage members, all resources (cannot delete org) |
| member | Read access, create own resources |
// Create an organizationawait authClient.organization.create({ name, slug })
// Switch active organizationawait authClient.organization.setActive({ organizationId })
// Invite a memberawait authClient.organization.inviteMember({ email, role: "member" })Organization deletion is protected — all non-owner members must be removed first.
Invitations
Section titled “Invitations”Invitations are sent by email with a link to /accept-invitation/{id}. Both authenticated and unauthenticated users can accept. Unauthenticated users are guided through sign-up with automatic email verification.
Authorization
Section titled “Authorization”A two-tier role system separates global and organization-scoped permissions.
Global roles
Section titled “Global roles”| Role | Purpose |
|---|---|
| admin | System administrator — bypasses all org-level permission checks |
| member | Regular user (default on sign-up) |
Permission enforcement
Section titled “Permission enforcement”Backend Convex functions declare their required permissions at definition time:
import { authenticatedQuery } from "../../../functions"
export const listBooks = authenticatedQuery({ args: {}, permissions: ["book", ["read"]], handler: async (ctx) => { // Only runs if the user's org role grants book:read },})The requirePermission function checks in order:
- Global admin → always allowed
- API key → checks explicit permission scopes
- Org role → checks role-based access control matrix
Function builders
Section titled “Function builders”| Builder | Auth | Permissions |
|---|---|---|
publicQuery / publicMutation | None | None |
authenticatedQuery / authenticatedMutation | Required | Required |
zAuthenticatedQuery / zAuthenticatedMutation | Required | Required + Zod validation |
adminQuery / adminMutation | Required | Global admin only |
API keys
Section titled “API keys”API keys provide programmatic access with fine-grained permissions. Keys are prefixed with ak_ and scoped to a specific organization.
// Create an API key (frontend admin)await authClient.apiKey.create({ name: "My Integration", permissions: { book: ["read", "create"] },})API keys authenticate via token exchange: POST /api/v1/auth/token with the API key returns a short-lived JWT. Permissions are checked per-request against the key’s explicit scopes — not role-based.
Sessions
Section titled “Sessions”Sessions are stored in the Convex database via the BetterAuth adapter. Key settings:
- Duration: configurable via
security.sessionDuration(default: 24 hours) - Multi-session: up to 5 concurrent sessions per user
- Secure cookies: HTTPS-only in production
- Active organization: tracked per session, resolved on each request
// List device sessionsawait authClient.multiSession.listDeviceSessions()
// Revoke a specific sessionawait authClient.multiSession.revoke({ sessionToken })Admin capabilities
Section titled “Admin capabilities”Global admins have access to user management through the admin plugin:
- List, search, and filter users
- Ban/unban users (with optional expiration)
- Impersonate users (max 1 hour, cannot impersonate other admins)
- Revoke all sessions for a user
- Reset user passwords
- Delete user accounts
Plugin chain
Section titled “Plugin chain”Auth behavior is extended through a chain of BetterAuth plugins, applied in order:
convex— Core Convex database bindingpasskeyEnforce— Enforce passkey when registeredmfaOrgGuard— Block org activation without MFAtwoFactor— TOTP 2FA managementpasskey— WebAuthn support (conditional)multiSession— Device session managementadmin— User management and impersonationorganization— Multi-tenant organizationsapiKey— API key generation and validationwaitlistGate— Sign-up token validation (conditional)invitationVerify— Auto-verify invited user emailsbillingProvision— Provision billing on org creationauditTrail— Log all auth eventscrossDomain— Cross-origin auth (must be last)
Configuration
Section titled “Configuration”All auth settings are managed through the config table and editable from the admin panel:
| Key | Default | Description |
|---|---|---|
auth.allowSelfSignup | true | Allow public registration |
auth.requireEmailVerification | true | Require email verification on sign-up |
auth.allowOrgCreation | true | Allow users to create organizations |
auth.passkeyEnabled | true | Enable WebAuthn passkey support |
security.sessionDuration | 86400 | Session lifetime in seconds |
security.rateLimitWindow | 60 | Rate limit window in seconds |
security.rateLimitMax | 10 | Max requests per window |
security.passwordMinLength | 10 | Minimum password length |
organization.membershipLimit | 50 | Max members per organization |
organization.invitationExpiration | 604800 | Invitation TTL in seconds (7 days) |
Key files
Section titled “Key files”| File | Purpose |
|---|---|
packages/backend/src/convex/lib/core/auth/convex_auth.ts | Main BetterAuth setup and plugin chain |
packages/backend/src/convex/lib/core/auth/auth.ts | requirePermission enforcement |
packages/backend/src/convex/lib/core/permissions.ts | Role and access control definitions |
packages/backend/src/convex/functions.ts | Authenticated function builders |
packages/web-core/src/lib/auth-client.ts | Frontend BetterAuth client |
apps/web/src/app/routes/auth/sign-in.tsx | Sign-in / sign-up / password reset UI |