Skip to main content

Better Auth plugins

Turn on a supported Better Auth plugin on the server and the client, and regenerate the auth schema.

A Better Auth plugin has two halves:

  • a server option of createBetterConvexAuth, for example organization;
  • a client plugin in defineConvexAuthClient, for example organizationClient().

Only some plugins are supported. See Better Auth plugin support for the list. This page uses the organization plugin as the example.

Before you start

The organization and two-factor plugins add tables to the Better Auth component. They need a local copy of the component inside your convex/ folder. The email OTP plugin adds no tables and works with the packaged component. pnpm exec better-convex init creates the local component:

FilePurpose
convex/betterAuth/convex.config.tsDefines the local betterAuth component
convex/betterAuth/schemaPlugins.tsLists the server plugins that change the schema
convex/betterAuth/schemaOptions.tsBuild-only Better Auth options for schema generation
convex/betterAuth/schema.tsGenerated component schema; do not edit
convex/betterAuth/schemaMetadata.tsGenerated schema fingerprint; do not edit
convex/betterAuth/adapter.tsExports the component functions from defineAuthAdapterFunctions

convex/convex.config.ts mounts this local component as betterAuth. Do not mount the packaged component from @lupinum/better-convex-nuxt/better-auth/convex.config at the same time.

1. Share the plugin options

Write the plugin options once, in convex/betterAuth/schemaPlugins.ts. The schema generator and convex/auth.ts both import them, so the schema always matches the running server:

convex/betterAuth/schemaPlugins.ts
import { jwt, organization, type OrganizationOptions } from 'better-auth/plugins'

export function createOrganizationOptions() {
  return {
    requireEmailVerificationOnInvitation: true,
  } as const satisfies OrganizationOptions
}

export function createAuthSchemaPlugins(authIssuer: string) {
  return [
    organization(createOrganizationOptions()),
    jwt({
      disableSettingJwtHeader: true,
      jwks: {
        disablePrivateKeyEncryption: false,
        gracePeriod: 21 * 60,
        keyPairConfig: { alg: 'RS256' },
      },
      jwt: { audience: authIssuer, expirationTime: '10m', issuer: authIssuer },
    }),
  ]
}

Keep the jwt entry exactly as better-convex init wrote it. The library uses the same JWT settings at runtime.

2. Turn the plugin on in the server

Pass the same options to createBetterConvexAuth:

convex/auth.ts
import { createBetterConvexAuth } from '@lupinum/better-convex-nuxt/better-auth/server'

import { components } from './_generated/api'
import type { DataModel } from './_generated/dataModel'
import { createOrganizationOptions } from './betterAuth/schemaPlugins'

export const auth = createBetterConvexAuth<DataModel>(components.betterAuth, {
  organization: createOrganizationOptions(),
})

With organization set, auth.getAuth(ctx) returns a Better Auth instance whose auth.api has the organization endpoints, for example auth.api.createOrganization.

3. Regenerate the schema

Run the generator after every change to schemaPlugins.ts:

bash
pnpm exec better-convex auth schema \
  --config convex/betterAuth/schemaOptions.ts \
  --output convex/betterAuth

It writes schema.ts and schemaMetadata.ts together. Commit both files. Add --check in CI: the command then fails when the files are out of date. The adapter also refuses to run when the schema and its fingerprint do not match.

The generator does not read production secrets. It uses only the build-only options in schemaOptions.ts.

4. Add the client plugin

Create convex-auth.ts in the Nuxt source folder. In a Nuxt 4 application, that is app/convex-auth.ts:

app/convex-auth.ts
import { defineConvexAuthClient } from '@lupinum/better-convex-nuxt/better-auth/client'
import { organizationClient } from 'better-auth/client/plugins'

export default defineConvexAuthClient({
  plugins: [organizationClient()],
})

The module finds this file, adds its own Convex client plugin, creates one client, and generates the types. The file only describes the client. It never creates one. To use another path, set convex.auth.client in nuxt.config.ts.

5. Call a plugin method

ts
const { client } = useConvexAuth()

const organizations = await client.organization.list()

Call plugin methods from event handlers or onMounted(). A method that changes the session finishes only after Convex accepts the new session. A read-only method, such as organization.list(), finishes without a session refresh.

Complete example

Use the repository's starters/team/convex/betterAuth folder as a complete reference. It turns on organizations with teams and custom roles, and shares those options between schemaPlugins.ts and convex/auth.ts in the same way.