- Notifications
You must be signed in to change notification settings - Fork1
Better Auth + Convex (local)
License
udecode/better-auth-convex
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Local installation of Better Auth directly in your Convex app schema, with direct database access instead of component-based queries.
The official@convex-dev/better-auth component stores auth tables in a component schema. This package provides an alternative approach with direct schema integration.
This package provides direct local installation:
- Auth tables live in your app schema - Not in a component boundary
- Direct database access - No
ctx.runQuery/ctx.runMutationoverhead (>50ms latency that increases with app size) - Unified context - Auth triggers can directly access and modify your app tables transactionally
- Full TypeScript inference - Single schema, single source of truth
Warning
BREAKING CHANGE: Auth tables are stored in your app schema instead of the component schema. If you're already in production with@convex-dev/better-auth, you'll need to write a migration script to move your auth data.
- Follow theofficial Better Auth + Convex setup guide first
- Choose yourframework guide
- IGNORE these steps from the framework guide:
- Step 2: "Register the component" - We don't use the component approach
- Step 5:
convex/auth.ts- We'll use a different setup - Step 7:
convex/http.ts- We use different route registration
- IGNORE these steps from the framework guide:
- Then come back here to install locally
pnpm add better-auth@1.3.27 better-auth-convex
You'll needconvex/auth.config.ts and update your files to install Better Auth directly in your app:
// convex/auth.tsimport{betterAuth}from'better-auth';import{convex}from'@convex-dev/better-auth/plugins';import{admin,organization}from'better-auth/plugins';// Optional pluginsimport{typeAuthFunctions,createClient,createApi,}from'better-auth-convex';import{internal}from'./_generated/api';importtype{MutationCtx,QueryCtx,GenericCtx}from'./_generated/server';importtype{DataModel}from'./_generated/dataModel';importschemafrom'./schema';// YOUR app schema with auth tables// 1. Internal API functions for auth operationsconstauthFunctions:AuthFunctions=internal.auth;// 2. Auth client with triggers that run in your app contextexportconstauthClient=createClient<DataModel,typeofschema>({ authFunctions, schema,triggers:{user:{beforeCreate:async(_ctx,data)=>{// Ensure every user has a username, filling in a simple fallbackconstusername=data.username?.trim()||data.email?.split('@')[0]||`user-${Date.now()}`;return{ ...data, username,};},onCreate:async(ctx,user)=>{// Direct access to your database// Example: Create personal organizationconstorgId=awaitctx.db.insert('organization',{name:`${user.name}'s Workspace`,slug:`personal-${user._id}`,// ... other fields});// Update user with personalOrganizationIdawaitctx.db.patch(user._id,{personalOrganizationId:orgId,});},beforeDelete:async(ctx,user)=>{// Example: clean up custom tables before removing the userif(user.personalOrganizationId){awaitctx.db.delete(user.personalOrganizationId);}returnuser;},},session:{onCreate:async(ctx,session)=>{// Set default active organization on session creationif(!session.activeOrganizationId){constuser=awaitctx.db.get(session.userId);if(user?.personalOrganizationId){awaitctx.db.patch(session._id,{activeOrganizationId:user.personalOrganizationId,});}}},},},});// 3. Create auth configuration (with options for HTTP-only mode)exportconstcreateAuth=(ctx:GenericCtx,{ optionsOnly}={optionsOnly:false})=>{constbaseURL=process.env.NEXT_PUBLIC_SITE_URL!;returnbetterAuth({ baseURL,logger:{disabled:optionsOnly},plugins:[convex(),// Requiredadmin(),organization({// Organization plugin config}),],session:{expiresIn:60*60*24*30,// 30 daysupdateAge:60*60*24*15,// 15 days},database:authClient.httpAdapter(ctx),// ... other config (social providers, user fields, etc.)});};// 4. Static auth instance for configurationexportconstauth=createAuth({}asany,{optionsOnly:true});// 5. IMPORTANT: Use getAuth for queries/mutations (direct DB access)exportconstgetAuth=<CtxextendsQueryCtx|MutationCtx>(ctx:Ctx)=>{returnbetterAuth({ ...auth.options,database:authClient.adapter(ctx,auth.options),});};// 6. Export trigger handlers for Convexexportconst{ beforeCreate, beforeDelete, beforeUpdate, onCreate, onDelete, onUpdate,}=authClient.triggersApi();// 7. Export API functions for internal useexportconst{ create, deleteMany, deleteOne, findMany, findOne, updateMany, updateOne,}=createApi(schema,auth.options);// Optional: If you need custom mutation builders (e.g., for custom context)// Pass internalMutation to both createClient and createApi// export const authClient = createClient<DataModel, typeof schema>({// authFunctions,// schema,// internalMutation: myCustomInternalMutation,// triggers: { ... }// });//// export const { create, ... } = createApi(schema, {// ...auth.options,// internalMutation: myCustomInternalMutation,// });
The trigger API exposes bothbefore* andon* hooks. Thebefore variants run inside the same Convex transaction just ahead of the database write, letting you normalize input, enforce invariants, or perform cleanup and return any transformed payload that should be persisted.
// convex/http.tsimport{httpRouter}from'convex/server';import{registerRoutes}from'better-auth-convex';import{createAuth}from'./auth';consthttp=httpRouter();registerRoutes(http,createAuth);exportdefaulthttp;
// ✅ In queries/mutations: Use getAuth (direct DB access)exportconstsomeQuery=query({handler:async(ctx)=>{constauth=getAuth(ctx);// Direct DB accessconstuser=awaitauth.api.getUser({ userId});},});// ⚠️ In actions: Use createAuth (needs HTTP adapter for external calls)exportconstsomeAction=action({handler:async(ctx)=>{constauth=createAuth(ctx);// Actions can't directly access DB// Use for webhooks, external API calls, etc.},});
// Component approach (@convex-dev/better-auth):// - Auth tables in components.betterAuth schema// - Requires ctx.runQuery/runMutation for auth operations// - Component boundaries between auth and app tables// Local approach (better-auth-convex):// ✅ Auth tables in your app schema// ✅ Direct queries across auth + app tables// ✅ Single transaction for complex operations// ✅ Direct function calls
All helpers are exported from the main package:
import{getAuthUserId,getSession,getHeaders}from'better-auth-convex';// Get current user IDconstuserId=awaitgetAuthUserId(ctx);// Get full sessionconstsession=awaitgetSession(ctx);// Get headers for auth.api callsconstheaders=awaitgetHeaders(ctx);
BothcreateClient andcreateApi accept an optionalinternalMutation parameter, allowing you to wrap internal mutations with custom context or behavior.
This is useful when you need to:
- Wrap database operations with custom context (e.g., triggers, logging)
- Apply middleware to all auth mutations
- Inject dependencies or configuration
import{customMutation,customCtx}from'convex-helpers/server/customFunctions';import{internalMutationGeneric}from'convex/server';import{registerTriggers}from'@convex/triggers';consttriggers=registerTriggers();// Wrap mutations to include trigger-wrapped databaseconstinternalMutation=customMutation(internalMutationGeneric,customCtx(async(ctx)=>({db:triggers.wrapDB(ctx).db,})));// Pass to createClientexportconstauthClient=createClient<DataModel,typeofschema>({ authFunctions, schema, internalMutation,// Use custom mutation buildertriggers:{ ...}});// Pass to createApiexportconst{ create, updateOne, ...}=createApi(schema,{ ...auth.options, internalMutation,// Use same custom mutation builder});
This ensures all auth operations (CRUD + triggers) use your wrapped database context.
Better Auth configuration changes may require schema updates. The Better Auth docs will often note when this is the case. To regenerate the schema (it's generally safe to do), run:
cd convex&& npx @better-auth/cli generate -y --output authSchema.ts
Import the generated schema in yourconvex/schema.ts:
import{authSchema}from'./authSchema';import{defineSchema}from'convex/server';exportdefaultdefineSchema({ ...authSchema,// Your other tables here});
Alternatively, use the generated schema as a reference to manually update your existing schema:
// Example: Adding a missing field discovered from generated schemaimport{defineSchema,defineTable}from'convex/server';import{v}from'convex/values';exportdefaultdefineSchema({user:defineTable({// ... existing fieldstwoFactorEnabled:v.optional(v.union(v.null(),v.boolean())),// New field from Better Auth update// ... rest of your schema}).index('email_name',['email','name']),// ... other indexes});
Better Auth may log warnings about missing indexes for certain queries. You can add custom indexes by extending the generated schema:
// convex/schema.tsimport{authSchema}from'./authSchema';import{defineSchema}from'convex/server';exportdefaultdefineSchema({ ...authSchema,// Override with custom indexesuser:authSchema.user.index('username',['username']),// Your other tables});
Note:authSchema table names and field names should not be customized directly. Use Better Auth configuration options to customize the schema, then regenerate to see the expected structure.
Built on top ofBetter Auth and@convex-dev/better-auth, optimized forConvex.
About
Better Auth + Convex (local)
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Contributors3
Uh oh!
There was an error while loading.Please reload this page.