Skip to content

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.

The primary authentication method. Supports sign-up, sign-in, password reset, and email verification.

import { authClient } from "@/lib/core/auth-client"
// Sign up
await authClient.signUp.email({ name, email, password })
// Sign in
await authClient.signIn.email({ email, password })
// Request password reset
await authClient.forgetPassword({ email })
// Sign out
await authClient.signOut()

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

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 passkey
await authClient.passkey.addPasskey()
// Sign in with passkey
await authClient.signIn.passkey()

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-in
await authClient.twoFactor.verifyTotp({ code })

Sent automatically on sign-up when auth.requireEmailVerification is true. Organization invitations follow Better Auth’s standard verified-email gate: invited users must verify their email before accepting or rejecting an invitation.

Multi-tenant organization support is built in. Each user can belong to multiple organizations with role-based access.

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 organization
await authClient.organization.create({ name, slug })
// Switch active organization
await authClient.organization.setActive({ organizationId })
// Invite a member
await authClient.organization.inviteMember({ email, role: "member" })

Organization deletion is protected — all non-owner members must be removed first.

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, email verification, sign-in, and then invitation acceptance.

A two-tier role system separates global and organization-scoped permissions.

Role Purpose
admin System administrator — bypasses all org-level permission checks
member Regular user (default on sign-up)

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:

  1. Global admin → always allowed
  2. API key → checks explicit permission scopes
  3. Org role → checks role-based access control matrix
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 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 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 sessions
await authClient.multiSession.listDeviceSessions()
// Revoke a specific session
await authClient.multiSession.revoke({ sessionToken })

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

Auth behavior is extended through a chain of BetterAuth plugins, applied in order:

  1. convex — Core Convex database binding
  2. passkeyEnforce — Enforce passkey when registered
  3. mfaOrgGuard — Block org activation without MFA
  4. twoFactor — TOTP 2FA management
  5. passkey — WebAuthn support (conditional)
  6. multiSession — Device session management
  7. admin — User management and impersonation
  8. organization — Multi-tenant organizations
  9. apiKey — API key generation and validation
  10. waitlistGate — Sign-up token validation (conditional)
  11. billingProvision — Provision billing on org creation
  12. auditTrail — Log all auth events
  13. crossDomain — Cross-origin auth (must be last)

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