#Does convex @convex-dev/better-auth support custom AdditionalFields?

34 messages · Page 1 of 1 (latest)

heavy radish
#

Is it true that I can't use custom AdditionalFields with betterAuth convex package?

The Component's User Table Schema (Fixed)

The @convex-dev/better-auth component internally defines its own schema:

// Component's internal schema - we can't modify this
user: {
email: v.string(),
name: v.string(),
emailVerified: v.boolean(),
image: v.optional(v.string()),
// ... other standard Better Auth fields
// ❌ No custom fields allowed
}

silk lightBOT
#

Hi @heavy radish. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Short answer: yes, but only in a specific setup.

1. Default Convex + Better Auth component

With the default @convex-dev/better-auth component install (no Local Install):

  • The component ships with a fixed, preconfigured schema for its tables (including user).
  • That schema is not alterable: you cannot add custom fields or change validators. [Basic usage]
  • So in this mode, your statement is effectively true:

    “Component's internal schema – we can't modify this … no custom fields allowed”

If you need extra user‑related data in this mode, you have to store it in your own Convex tables that reference the Better Auth user via _id or userId (depending on your migration path), not by modifying the component’s user table.

2. Local Install: where additionalFields is supported

If you use Local Install for Convex + Better Auth:

  • You define a local Better Auth component under convex/betterAuth/.
  • You generate the schema with the Better Auth CLI and import it as your component schema. [Local install]
  • In this mode, you can use Better Auth’s additionalFields to extend the user schema, then regenerate the schema:
export const createAuth = (
  ctx: GenericCtx<DataModel>,
  { optionsOnly } = { optionsOnly: false }
) => {
  return betterAuth({
    user: {
      additionalFields: {
        bio: { type: "string", required: false },
        // ...etc
      },
    },
    // ...
  });
};

Then:

cd convex/betterAuth
npx @better-auth/cli generate -y

This updates the component schema with those fields. [Local install – usage]

#

A migration guide explicitly shows this pattern for adding a userId field via additionalFields and then regenerating the schema. [Migrate userId]

3. So what’s the correct statement?

  • Default component install:
    You cannot use custom additionalFields that affect the Convex schema of the component tables. The schema is fixed and managed by the package.

  • Local Install:
    You can use Better Auth’s additionalFields and have them reflected in the Convex schema by regenerating via the CLI.

If you need custom fields on the Better Auth user table itself, you must switch to Local Install; otherwise, use separate app tables and keep the component schema as‑is.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

heavy radish
#

Does convex @convex-dev/better-auth support custom AdditionalFields?

silk lightBOT
#

@heavy radish Kapa.ai is still learning and improving, please let me know how I did by reacting below.

heavy radish
#

When to use local install and when the default?

#

@silk light what is the difference between local and default install?

silk lightBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

“Default install” and “Local Install” are two ways of using the @convex-dev/better-auth component, with different levels of control.

Default install

  • Uses the prebuilt Better Auth component from NPM.
  • The component’s schema is fixed: you cannot change the Better Auth tables (like user) or run the Better Auth CLI against them. [Basic usage]
  • Only the documented “supported plugins” work “out of the box” (Anonymous, Email OTP, Magic Link, etc.). Plugins that require schema changes or extra tables are not supported in this mode. [Supported plugins; Discord org plugin thread]
  • You typically just import the component’s config from the package and use it; there is no convex/betterAuth directory of your own.

Local Install

Local Install “brings the component into your repo” so you own its Convex code and schema. [Local install]

Key differences:

  1. Component lives in your project

    • You create convex/betterAuth/convex.config.ts with defineComponent("betterAuth"), and register it from your app’s convex/convex.config.ts. [Local install – installation]
    • Convex treats convex/betterAuth as a locally installed component.
  2. Schema is generated and customizable (within Better Auth rules)

#
  • You export a static auth instance and run the Better Auth CLI to generate a Convex schema in convex/betterAuth/schema.ts. [Local install – installation]
    • You can regenerate the schema whenever your Better Auth options change (for example, when you add additionalFields):
      cd convex/betterAuth
      npx @better-auth/cli generate -y
      
      [Local install – usage]
    • You can also generate to a secondary generatedSchema.ts and extend it with custom indexes in schema.ts. [Local install – usage]
    • Migration docs show adding new fields like userId to the Better Auth user model via additionalFields, then regenerating the schema so that Convex’s tables include those fields. [Migrate userId – component table]
  1. Adapter and APIs are local Convex functions

    • You define convex/betterAuth/adapter.ts that exports create, findOne, findMany, etc. using createApi(schema, createAuth). [Local install – installation]
  2. Supports more plugins and deeper integration

#
  • Local Install is explicitly recommended when:
    • You want to use plugins not on the supported list, or plugins that require schema changes. [Supported plugins]
    • You need to customize Better Auth’s schema (extra fields, extra indexes, etc.). [Local install]
    • You can also write Convex functions in convex/betterAuth that directly access Better Auth tables, and call them from your app via ctx.runQuery / ctx.runMutation. [Local install – accessing data]
  1. Client config uses the local schema

    • Instead of createClient<DataModel>(components.betterAuth), you pass the local schema type so calls are fully typed:
      import authSchema from "./betterAuth/schema";
      
      export const authComponent = createClient<DataModel, typeof authSchema>(
        components.betterAuth,
        {
          local: { schema: authSchema },
        }
      );
      
      [Local install – installation]

Summary

  • Default install: fastest setup, fixed schema, only supported plugins, no schema generation.
  • Local Install: more setup, but you own the component, can generate and extend the schema (including additionalFields via Better Auth config), can use more plugins, and can write Convex code that directly works with Better Auth tables.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@heavy radish Kapa.ai is still learning and improving, please let me know how I did by reacting below.

heavy radish
silk lightBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

For custom user fields, the “right” approach depends on how far you want to go.

If you just need a few extra fields and are OK changing your setup

Use Local Install and configure additionalFields:

  1. Convert to Local Install (create convex/betterAuth/convex.config.ts, export auth for schema generation, update convex/convex.config.ts, etc.). [Local install]

  2. Add your fields in createAuth:

    export const createAuth = (ctx: GenericCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
      return betterAuth({
        user: {
          additionalFields: {
            bio: { type: "string", required: false },
            // ...
          },
        },
        // ...
      });
    };
    
  3. Regenerate the schema:

    cd convex/betterAuth
    npx @better-auth/cli generate -y
    

    This is exactly how Better Auth docs show adding a userId field to the component user table. [UserId in component table; Local install usage]

This is the approach if you want custom fields on the Better Auth user table itself.

If you want to keep the default component install

With the default @convex-dev/better-auth install:

#
  • The component’s Convex schema is fixed and can’t be changed (no schema generation step). [Basic usage]
  • In that case, the recommended pattern is:
    • Keep Better Auth’s user table as‑is.
    • Create your own Convex users (or profiles) table with whatever fields you want.
    • Link it to the Better Auth user via authId or userId, following the migration guides if needed. [Migrate userId]

This is the approach if you don’t want Local Install and are okay with a separate app‑level user/profile table.


So:

  • Need custom fields directly in the Better Auth tables?Local Install + additionalFields.
  • Prefer minimal setup, okay with a separate app table?Default install + your own user/profile table referencing Better Auth users.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@heavy radish Kapa.ai is still learning and improving, please let me know how I did by reacting below.

heavy radish
#

@silk light I am not using local install but asking about how to handle this:
What's Broken Right Now

  1. Hooks - I removed the heavy query but left convexUser partially working (JWT decode)
  2. (app)/+layout.server.ts - I added the Convex query but it's not fully wired up
  3. app.d.ts - Still has all the heavy properties in App.Locals
  4. 38 files - Still reference locals.profile which won't be populated for API endpoints

The Key Decision

For API endpoints, you have two choices:

Option A: Keep using locals.convexUser for basic auth checks

  • Hooks decode JWT → locals.convexUser = { id, email, name }
  • API endpoints check locals.convexUser exists, then call Convex
  • Convex handles all authorization internally (membership checks, etc.)

Option B: API endpoints decode token themselves

  • Hooks only set locals.token
  • Each API endpoint uses a utility to get user info from token
  • More explicit but repetitive
silk lightBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re in “default install” Better Auth + Convex (no local install), so you can’t rely on custom fields coming from Better Auth’s DB schema, and Convex should be the source of truth for authorization and user data. [Basic usage; Auth in functions]

Given your two options, the Convex docs and Better Auth + Convex docs together imply a clear direction:


1. What Convex wants you to do

On the backend, Convex expects you to:

From Discord:

“User data from the jwt claims isn’t assumed to be accurate, it could change after the token is created. So … the recommendation is to query your user data with a Convex function.” [Discord user data]

So Convex’s strong guidance is:

  • JWT is only for identity (who is this user).
  • Convex DB is for authorization & user profile.

2. Implications for your two options

You’re deciding how your SvelteKit API endpoints integrate with Convex.

Option A: Use locals.convexUser only as a thin auth gate

#

Hooks decode JWT → locals.convexUser = { id, email, name }
API endpoints:

  • just check “is the user present?”
  • then call Convex
    Convex functions:
  • use ctx.auth.getUserIdentity()
  • do all membership / org / role checks internally

This aligns with Convex guidance:

  • JWT decode on the framework side is only for basic auth gating, not for permissions.
  • All real authorization decisions happen in Convex functions, based on DB data. [Auth in functions; Authorization helpers]

It also means:

  • You can keep locals.convexUser small: { id, email, name }.
  • Your API endpoints stay thin: “if not logged in → 401, else call Convex”.

Option B: Decode token & use claims in each endpoint

Hooks set locals.token, every endpoint decodes and uses claims.

Problems relative to Convex guidance:

  • Repeats logic in every endpoint.
  • Tempts you to use JWT claims (e.g. roles, orgId) as source of truth, which Convex explicitly discourages. [Discord user data]
  • Makes it harder to centralize authorization in Convex functions.

3. Recommended approach for your setup

Given the docs:

  • You’re not on Local Install, so you don’t customize Better Auth tables. [Basic usage]
  • Convex wants all real auth logic in Convex functions, powered by ctx.auth.getUserIdentity() and DB lookups. [Auth in functions; Database users]

So:

Use Option A.

Concretely:

#
  1. Hook (hooks.server.ts)

    • Decode the Better Auth session/JWT once.
    • Set:
      • locals.token (raw token for passing to Convex HTTP client if needed). [SvelteKit token hook]
      • locals.convexUser = { id, email, name } | null for “is logged in”.
  2. API endpoints

    • Check locals.convexUser exists.
    • Call Convex (via Convex client / HTTP) and let Convex:
      • Read ctx.auth.getUserIdentity().
      • Load the user / org / memberships from DB.
      • Enforce all authorization rules there.
  3. Convex

This keeps your framework layer thin and stateless, and centralizes the complicated, security‑critical logic where Convex’s tooling and patterns are designed to help.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@heavy radish Kapa.ai is still learning and improving, please let me know how I did by reacting below.

heavy radish
#

@silk light can you give me the setup and descriptions of all variables that I need to set in prod and cloud dev? ALso when I am working on localhost?

silk lightBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Here’s what the docs explicitly cover about environment variables for Convex, Convex Auth, Better Auth, and adapters. I’ll group them by context: Convex dev deployment, Convex prod deployment, and frontend hosting (e.g. Vercel).


1. Convex deployments: dev vs prod

Convex environment variables are per deployment (dev / prod / previews) and are managed in the Convex dashboard or via CLI. [Env vars overview]

You can inspect/set them with:

# dev deployment
npx convex env list
npx convex env get NAME
npx convex env set NAME value

# prod deployment
npx convex env --prod list
npx convex env --prod get NAME
npx convex env --prod set NAME value

You typically have:

  • Dev deployment (used by npx convex dev)
  • Prod deployment (used by npx convex deploy and your live app) [Project config]

2. Convex Auth (@convex-dev/auth)

2.1 Core Convex Auth variables (Convex deployments)

From the manual setup: [Manual Convex Auth setup]

Required for Convex dev and prod deployments

On each Convex deployment (dev and prod) you need:

  1. SITE_URL

    • Used for OAuth redirects and magic links back to your site.
    • Example for local dev:
      npx convex env set SITE_URL <http://localhost:5173>
      
      (or <http://localhost:3000> for Next.js, etc.) [SITE_URL]
  2. JWT_PRIVATE_KEY

    • RS256 private key used by Convex Auth to sign tokens.

    • Generated by the generateKeys.mjs script:

      // generateKeys.mjs (run with `node generateKeys.mjs`)
      import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
#

const keys = await generateKeyPair("RS256", { extractable: true });
const privateKey = await exportPKCS8(keys.privateKey);
const publicKey = await exportJWK(keys.publicKey);
const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });

 process.stdout.write(
   `JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`,
 );
 process.stdout.write("\n");
 process.stdout.write(`JWKS=${jwks}`);
 process.stdout.write("\n");
 ```
  • Copy the printed JWT_PRIVATE_KEY=... into the Convex dashboard for that deployment. [JWT key setup]
  1. JWKS
    • JSON Web Key Set (public keys) matching JWT_PRIVATE_KEY.
    • Comes from the same script output; also set on the Convex deployment. [JWT key setup]

So on Convex dev and Convex prod you should have at least:

  • SITE_URL
  • JWT_PRIVATE_KEY
  • JWKS

You must set them separately per deployment.

If you’re seeing production-only JWT_PRIVATE_KEY errors, check that the dev and prod values are both set and correctly formatted:

npx convex env get JWT_PRIVATE_KEY
npx convex env get JWT_PRIVATE_KEY --prod

[Discord JWT_PRIVATE_KEY debugging]

2.2 Provider-specific variables (Convex deployments)

For external auth providers you typically use environment variables in Convex to swap values between dev and prod.

Auth0 example

  • In convex/auth.config.ts:

    export default {
      providers: [
        {
          domain: process.env.AUTH0_DOMAIN!,
          applicationID: process.env.AUTH0_CLIENT_ID!,
        },
      ],
    };
    

    [Auth0 dev/prod]

#
  • On Convex dev deployment:

    • AUTH0_DOMAIN (dev tenant)
    • AUTH0_CLIENT_ID (dev app)
  • On Convex prod deployment:

    • AUTH0_DOMAIN (prod tenant)
    • AUTH0_CLIENT_ID (prod app)

Clerk example

  • convex/auth.config.ts uses process.env.CLERK_JWT_ISSUER_DOMAIN. [Clerk dev/prod]
  • On Convex dev and prod deployments:
    • CLERK_JWT_ISSUER_DOMAIN (different per environment)

WorkOS AuthKit example

  • convex/auth.config.ts uses process.env.WORKOS_CLIENT_ID. [WorkOS dev/prod]
  • On Convex dev and prod:
    • WORKOS_CLIENT_ID (dev vs prod application)

3. Better Auth component (@convex-dev/better-auth)

You’re using the default (non-local) install, so:

3.1 Convex deployment variables (dev & prod)

All Better Auth framework guides share the same Convex-side pattern: [Next.js guide; React SPA; SvelteKit; Expo; TanStack Start]

On Convex dev deployment:

  1. BETTER_AUTH_SECRET
    • Encryption / hashing secret for Better Auth.
    • Set once per deployment:
      npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
      
#
  1. SITE_URL
    • Your app’s URL for that environment.
    • For local dev:
      • Next.js: <http://localhost:3000>
        npx convex env set SITE_URL <http://localhost:3000>
        ``` [[Next.js guide](<https://convex-better-auth.netlify.app/framework-guides/next#installation>)]
        
      • Vite / SPA: <http://localhost:5173>
        npx convex env set SITE_URL <http://localhost:5173>
        ``` [[React SPA guide](<https://convex-better-auth.netlify.app/framework-guides/react#installation>)]
        
      • SvelteKit: <http://localhost:5173> [SvelteKit guide]
      • TanStack Start / others: adjust accordingly.

On Convex prod deployment:

  • Same keys, with production URLs and secrets:
    • BETTER_AUTH_SECRET (new random value for prod)
    • SITE_URL (your production site URL, e.g. <https://your-domain.com>)

You set these separately on the prod deployment via dashboard or:

npx convex env --prod set BETTER_AUTH_SECRET ...
npx convex env --prod set SITE_URL <https://your-domain.com>

3.2 Frontend environment variables (localhost & hosting)

These live in your frontend framework (e.g. .env.local, Vercel variables), not in Convex.

Local dev (localhost)

From the framework guides:

  • Next.js: [Next.js guide]

    .env.local:

    # written by `npx convex dev`
    CONVEX_DEPLOYMENT=dev:adjective-animal-123
    
    NEXT_PUBLIC_CONVEX_URL=<https://adjective-animal-123.convex.cloud>
    NEXT_PUBLIC_CONVEX_SITE_URL=<https://adjective-animal-123.convex.site>
    
    # local site URL (also mirrored in Convex deployment’s SITE_URL)
    SITE_URL=<http://localhost:3000>
    
#
  • React (Vite SPA): [React SPA guide]

    .env.local:

    CONVEX_DEPLOYMENT=dev:adjective-animal-123
    
    VITE_CONVEX_URL=<https://adjective-animal-123.convex.cloud>
    VITE_CONVEX_SITE_URL=<https://adjective-animal-123.convex.site>
    
    SITE_URL=<http://localhost:5173>
    
  • SvelteKit: [SvelteKit guide]

    .env.local:

    CONVEX_DEPLOYMENT=dev:adjective-animal-123
    
    PUBLIC_CONVEX_URL=<https://adjective-animal-123.convex.cloud>
    PUBLIC_CONVEX_SITE_URL=<https://adjective-animal-123.convex.site>
    
    SITE_URL=<http://localhost:5173>
    
  • Expo: [Expo guide]

    .env.local:

    CONVEX_DEPLOYMENT=dev:adjective-animal-123
    
    EXPO_PUBLIC_CONVEX_URL=<https://adjective-animal-123.convex.cloud>
    EXPO_PUBLIC_CONVEX_SITE_URL=<https://adjective-animal-123.convex.site>
    

In all these cases:

  • CONVEX_DEPLOYMENT is where npx convex dev pushes functions (dev deployment). [Project config]
  • *_CONVEX_URL is the Convex cloud URL your client library connects to.
  • *_CONVEX_SITE_URL is the .site URL used for HTTP actions / Better Auth redirects.

Hosting / production (e.g. Vercel)

On your hosting provider (like Vercel):

  • Mirror the same client-side vars, but targeting your production Convex deployment URL:
#
  • For Next.js on Vercel:

    • NEXT_PUBLIC_CONVEX_URL=<https://your-prod-deployment.convex.cloud>

    • NEXT_PUBLIC_CONVEX_SITE_URL=<https://your-prod-deployment.convex.site>

    • Possibly SITE_URL=<https://your-domain.com> (to match Convex’s SITE_URL). [Next.js guide]

    • For Vite / TanStack / SvelteKit, use the corresponding VITE_ / PUBLIC_ names with the prod Convex URLs. [React SPA guide; TanStack Start guide; SvelteKit guide]

  • Also, if you’re using external providers (Auth0, Clerk, WorkOS, etc.), they require their own frontend env vars (publishable keys, redirect URIs) in your hosting platform .env / UI. The exact names depend on the provider and framework (e.g. VITE_AUTH0_DOMAIN, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, NEXT_PUBLIC_WORKOS_REDIRECT_URI). [Auth0 client env; Clerk client env; WorkOS client env]


4. Auth.js (NextAuth) Convex Adapter

If you’re also using the Convex adapter for Auth.js: [Auth.js adapter env]

  • On Next.js:

    • .env.local / .env:
      • CONVEX_AUTH_ADAPTER_SECRET=<random secret>
  • On Convex dev and prod deployments:

    • Also set CONVEX_AUTH_ADAPTER_SECRET to the same value per environment:
#
    # dev
    npx convex env set CONVEX_AUTH_ADAPTER_SECRET somevalue
    # prod
    npx convex env --prod set CONVEX_AUTH_ADAPTER_SECRET somevalue
    ```

This secret guards the public adapter endpoints so only your Next.js server can call them.

---

## 5. Summary per environment

### Localhost (dev):

- **Convex dev deployment**:
  - `SITE_URL=<http://localhost:<port>`>  
  - `BETTER_AUTH_SECRET` (Better Auth)  
  - For Convex Auth: `SITE_URL`, `JWT_PRIVATE_KEY`, `JWKS` (if using `@convex-dev/auth`)  
  - Provider-specific: `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, `CLERK_JWT_ISSUER_DOMAIN`, `WORKOS_CLIENT_ID`, etc. as needed.

- **Frontend `.env.local`**:
  - `CONVEX_DEPLOYMENT=dev:...` (written by `npx convex dev`)  
  - `<FRAMEWORK>_CONVEX_URL` (cloud URL)  
  - `<FRAMEWORK>_CONVEX_SITE_URL` (.site URL)  
  - `SITE_URL=<http://localhost:<port>`>  
  - Any provider-specific publishable keys.

### Convex cloud dev vs prod:

- **Dev deployment**:
  - `BETTER_AUTH_SECRET` (dev)  
  - `SITE_URL` (dev URL)  
  - For Convex Auth: `SITE_URL`, `JWT_PRIVATE_KEY`, `JWKS` (dev keys)  
  - Provider-specific dev vars (e.g. `AUTH0_DOMAIN` dev, `AUTH0_CLIENT_ID` dev, etc.)  
  - Optional: `CONVEX_AUTH_ADAPTER_SECRET` if using the NextAuth adapter.

- **Prod deployment**:
  - `BETTER_AUTH_SECRET` (prod, different from dev)  
  - `SITE_URL` (prod site URL)  
  - For Convex Auth: `SITE_URL`, `JWT_PRIVATE_KEY`, `JWKS` (prod keys)  
  - Provider-specific prod vars (Auth0 / Clerk / WorkOS)  
  - Optional: `CONVEX_AUTH_ADAPTER_SECRET` (prod) if using the adapter.

- Use `npx convex env ...` and `npx convex env --prod ...` to manage them. [[Env vars CLI](<https://docs.convex.dev/production/environment-variables>)]

### Hosting provider (e.g. Vercel prod):
#
  • Client-facing Convex URL vars:
    • NEXT_PUBLIC_CONVEX_URL / VITE_CONVEX_URL / PUBLIC_CONVEX_URL (prod Convex cloud URL)
    • NEXT_PUBLIC_CONVEX_SITE_URL / VITE_CONVEX_SITE_URL / PUBLIC_CONVEX_SITE_URL (prod .site URL)
  • Your app SITE_URL (matches Convex’s prod SITE_URL)
  • Provider-specific frontend vars (Auth0 / Clerk / WorkOS, etc.)

If you share your exact stack (Next.js, SvelteKit, etc.) I can map this to a concrete minimal list just for your setup.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: