
Security News
GitHub Actions Pricing Whiplash: Self-Hosted Actions Billing Change Postponed
GitHub postponed a new billing model for self-hosted Actions after developer pushback, but moved forward with hosted runner price cuts on January 1.
better-auth-convex
Advanced tools
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:
ctx.runQuery/ctx.runMutation overhead (>50ms latency that increases with app size)[!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.
convex/auth.ts - We'll use a different setupconvex/http.ts - We use different route registrationpnpm add better-auth@1.4.7 better-auth-convex
You'll need convex/auth.config.ts and update your files to install Better Auth directly in your app:
// convex/auth.config.ts
import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
import type { AuthConfig } from "convex/server";
export default {
providers: [getAuthConfigProvider({ jwks: process.env.JWKS })],
} satisfies AuthConfig;
// convex/auth.ts
import { betterAuth, type BetterAuthOptions } from "better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { admin, organization } from "better-auth/plugins"; // Optional plugins
import {
type AuthFunctions,
createClient,
createApi,
} from "better-auth-convex";
import { internal } from "./_generated/api";
import type { MutationCtx, QueryCtx, GenericCtx } from "./_generated/server";
import type { DataModel } from "./_generated/dataModel";
import schema from "./schema"; // YOUR app schema with auth tables
import authConfig from "./auth.config";
// 1. Internal API functions for auth operations
const authFunctions: AuthFunctions = internal.auth;
// 2. Auth client with triggers that run in your app context
export const authClient = createClient<DataModel, typeof schema>({
authFunctions,
schema,
triggers: {
user: {
beforeCreate: async (_ctx, data) => {
// Ensure every user has a username, filling in a simple fallback
const username =
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 organization
const orgId = await ctx.db.insert("organization", {
name: `${user.name}'s Workspace`,
slug: `personal-${user._id}`,
// ... other fields
});
// Update user with personalOrganizationId
await ctx.db.patch(user._id, {
personalOrganizationId: orgId,
});
},
beforeDelete: async (ctx, user) => {
// Example: clean up custom tables before removing the user
if (user.personalOrganizationId) {
await ctx.db.delete(user.personalOrganizationId);
}
return user;
},
},
session: {
onCreate: async (ctx, session) => {
// Set default active organization on session creation
if (!session.activeOrganizationId) {
const user = await ctx.db.get(session.userId);
if (user?.personalOrganizationId) {
await ctx.db.patch(session._id, {
activeOrganizationId: user.personalOrganizationId,
});
}
}
},
},
},
});
// 3. Auth options factory
export const createAuthOptions = (ctx: GenericCtx) =>
({
baseURL: process.env.SITE_URL!,
plugins: [
convex({
authConfig,
jwks: process.env.JWKS,
}),
admin(),
organization({
// Organization plugin config
}),
],
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
updateAge: 60 * 60 * 24 * 15, // 15 days
},
database: authClient.httpAdapter(ctx),
// ... other config (social providers, user fields, etc.)
}) satisfies BetterAuthOptions;
// 4. Create auth instance
export const createAuth = (ctx: GenericCtx) =>
betterAuth(createAuthOptions(ctx));
// 5. IMPORTANT: Use getAuth for queries/mutations (direct DB access)
export const getAuth = <Ctx extends QueryCtx | MutationCtx>(ctx: Ctx) => {
return betterAuth({
...createAuthOptions({} as any),
database: authClient.adapter(ctx, createAuthOptions),
});
};
// 6. Export trigger handlers for Convex
export const {
beforeCreate,
beforeDelete,
beforeUpdate,
onCreate,
onDelete,
onUpdate,
} = authClient.triggersApi();
// 7. Export API functions for internal use
export const {
create,
deleteMany,
deleteOne,
findMany,
findOne,
updateMany,
updateOne,
getLatestJwks,
rotateKeys,
} = createApi(schema, createAuth, {
// Optional: Skip input validation for smaller generated types
// Since these are internal functions, validation is optional
skipValidation: true,
});
// 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, createAuth, {
// internalMutation: myCustomInternalMutation,
// });
The trigger API exposes both before* and on* hooks. The before 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.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "better-auth-convex";
import { createAuth } from "./auth";
const http = httpRouter();
registerRoutes(http, createAuth);
export default http;
After deploying, generate and set the JWKS env var:
# Development
npx convex run auth:getLatestJwks | npx convex env set JWKS
# Production
npx convex run auth:getLatestJwks --prod | npx convex env set JWKS --prod
To rotate keys (invalidates all existing tokens, forces re-authentication):
npx convex run auth:rotateKeys | npx convex env set JWKS
// ✅ In queries/mutations: Use getAuth (direct DB access)
export const someQuery = query({
handler: async (ctx) => {
const auth = getAuth(ctx); // Direct DB access
const user = await auth.api.getUser({ userId });
},
});
// ⚠️ In actions: Use createAuth (needs HTTP adapter for external calls)
export const someAction = action({
handler: async (ctx) => {
const auth = 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 ID
const userId = await getAuthUserId(ctx);
// Get full session
const session = await getSession(ctx);
// Get headers for auth.api calls
const headers = await getHeaders(ctx);
skipValidationThe createApi function accepts a skipValidation option that uses generic validators instead of typed validators:
export const { create, ... } = createApi(schema, createAuth, {
skipValidation: true, // Smaller generated types
});
When to use: Enable this option to significantly reduce generated type sizes. Since these are internal functions only called by the auth adapter, input validation is optional. The trade-off is less precise TypeScript inference for the internal API arguments.
Both createClient and createApi accept an optional internalMutation parameter, allowing you to wrap internal mutations with custom context or behavior.
This is useful when you need to:
import { customMutation, customCtx } from 'convex-helpers/server/customFunctions';
import { internalMutationGeneric } from 'convex/server';
import { registerTriggers } from '@convex/triggers';
const triggers = registerTriggers();
// Wrap mutations to include trigger-wrapped database
const internalMutation = customMutation(
internalMutationGeneric,
customCtx(async (ctx) => ({
db: triggers.wrapDB(ctx).db,
}))
);
// Pass to createClient
export const authClient = createClient<DataModel, typeof schema>({
authFunctions,
schema,
internalMutation, // Use custom mutation builder
triggers: { ... }
});
// Pass to createApi
export const { create, updateOne, ... } = createApi(schema, createAuth, {
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 your convex/schema.ts:
import { authSchema } from "./authSchema";
import { defineSchema } from "convex/server";
export default defineSchema({
...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 schema
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
user: defineTable({
// ... existing fields
twoFactorEnabled: 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.ts
import { authSchema } from "./authSchema";
import { defineSchema } from "convex/server";
export default defineSchema({
...authSchema,
// Override with custom indexes
user: 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 of Better Auth and @convex-dev/better-auth, optimized for Convex.
FAQs
Better Auth Convex local integration
The npm package better-auth-convex receives a total of 31 weekly downloads. As such, better-auth-convex popularity was classified as not popular.
We found that better-auth-convex demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
GitHub postponed a new billing model for self-hosted Actions after developer pushback, but moved forward with hosted runner price cuts on January 1.

Research
Destructive malware is rising across open source registries, using delays and kill switches to wipe code, break builds, and disrupt CI/CD.

Security News
Socket CTO Ahmad Nassri shares practical AI coding techniques, tools, and team workflows, plus what still feels noisy and why shipping remains human-led.