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 exampleorganization; - a client plugin in
defineConvexAuthClient, for exampleorganizationClient().
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:
| File | Purpose |
|---|---|
convex/betterAuth/convex.config.ts | Defines the local betterAuth component |
convex/betterAuth/schemaPlugins.ts | Lists the server plugins that change the schema |
convex/betterAuth/schemaOptions.ts | Build-only Better Auth options for schema generation |
convex/betterAuth/schema.ts | Generated component schema; do not edit |
convex/betterAuth/schemaMetadata.ts | Generated schema fingerprint; do not edit |
convex/betterAuth/adapter.ts | Exports 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:
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:
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:
pnpm exec better-convex auth schema \
--config convex/betterAuth/schemaOptions.ts \
--output convex/betterAuthIt 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:
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
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.