[GH-ISSUE #7261] $Infer.Session type doesn't include additionalFields when using dynamic trustedOrigins function #28096

Closed
opened 2026-04-17 19:29:52 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @AndKenneth on GitHub (Jan 11, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7261

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

// auth.ts
import { betterAuth } from 'better-auth'

export const auth = betterAuth({
  baseURL: 'http://localhost:3000',
  trustedOrigins: async (request: Request) => {
    // Dynamic function to support Vercel preview URLs
    return ['http://localhost:3000']
  },
  user: {
    additionalFields: {
      selectedOrganisationId: {
        type: 'string',
      },
    },
  },
})

// Usage - this causes TypeScript error
const session = await auth.api.getSession({ headers })
session?.user.selectedOrganisationId 
// TS Error: Property 'selectedOrganisationId' does not exist on type

Current vs. Expected behavior

Current: When trustedOrigins is a function, auth.$Infer.Session.user does not include fields from additionalFields. TypeScript shows:

Property 'selectedOrganisationId' does not exist on type '{ id: string; createdAt: Date; ... }'

Expected: additionalFields should be inferred on the User type regardless of whether trustedOrigins is a static array or a function.

Workaround: Using a static array with wildcards works correctly (works for me now because this was fixed in a recent update):

trustedOrigins: ['http://localhost:3000', 'https://example-*.vercel.app']

Broken in: 1.4.9, 1.4.10

What version of Better Auth are you using?

1.4.10

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "release": "25.1.0"
  },
  "node": {
    "version": "v22.21.1"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.4"
  },
  "frameworks": [
    { "name": "next", "version": "^16.1.1" },
    { "name": "react", "version": "19.2.0" }
  ],
  "databases": [
    { "name": "@prisma/client", "version": "^7.0.0" }
  ],
  "betterAuth": {
    "version": "^1.4.10"
  }
}

Which area(s) are affected? (Select all that apply)

Types

Auth config (if applicable)


Additional context

No response

Originally created by @AndKenneth on GitHub (Jan 11, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7261 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ```typescript // auth.ts import { betterAuth } from 'better-auth' export const auth = betterAuth({ baseURL: 'http://localhost:3000', trustedOrigins: async (request: Request) => { // Dynamic function to support Vercel preview URLs return ['http://localhost:3000'] }, user: { additionalFields: { selectedOrganisationId: { type: 'string', }, }, }, }) // Usage - this causes TypeScript error const session = await auth.api.getSession({ headers }) session?.user.selectedOrganisationId // TS Error: Property 'selectedOrganisationId' does not exist on type ``` ### Current vs. Expected behavior **Current:** When `trustedOrigins` is a function, `auth.$Infer.Session.user` does not include fields from `additionalFields`. TypeScript shows: ``` Property 'selectedOrganisationId' does not exist on type '{ id: string; createdAt: Date; ... }' ``` **Expected:** `additionalFields` should be inferred on the User type regardless of whether `trustedOrigins` is a static array or a function. **Workaround:** Using a static array with wildcards works correctly (works for me now because this was fixed in a recent update): ```typescript trustedOrigins: ['http://localhost:3000', 'https://example-*.vercel.app'] ``` **Broken in:** 1.4.9, 1.4.10 ### What version of Better Auth are you using? 1.4.10 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "release": "25.1.0" }, "node": { "version": "v22.21.1" }, "packageManager": { "name": "npm", "version": "10.9.4" }, "frameworks": [ { "name": "next", "version": "^16.1.1" }, { "name": "react", "version": "19.2.0" } ], "databases": [ { "name": "@prisma/client", "version": "^7.0.0" } ], "betterAuth": { "version": "^1.4.10" } } ``` ### Which area(s) are affected? (Select all that apply) Types ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the lockedenhancement labels 2026-04-17 19:29:52 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Jan 11, 2026):

It looks like you're experiencing an issue where auth.$Infer.Session.user does not include fields defined in additionalFields when using a dynamic function for trustedOrigins. This is a known issue where the TypeScript inference fails to capture these custom fields when the trustedOrigins configuration is a function.

The expected behavior, as you've noted, is for additionalFields to be included in the inferred types, whether the trustedOrigins is a static array or a function. As a workaround, you mentioned that using a static array works, which has been confirmed in updates addressing similar issues.

To resolve this, you may want to check if you're using the latest version of Better Auth and keep an eye on related issues for fixes in future releases. There’s an ongoing effort to improve type inference for custom fields further, so stay tuned for any updates that may address this specific concern.

For now, if you continue to encounter this problem, consider tagging @bekacru or @ping__ in your GitHub discussions for more tailored assistance!

Is there anything specific you'd like help with while working through this?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3733971763 --> @better-auth-agent[bot] commented on GitHub (Jan 11, 2026): It looks like you're experiencing an issue where `auth.$Infer.Session.user` does not include fields defined in `additionalFields` when using a dynamic function for `trustedOrigins`. This is a known issue where the TypeScript inference fails to capture these custom fields when the `trustedOrigins` configuration is a function. The expected behavior, as you've noted, is for `additionalFields` to be included in the inferred types, whether the `trustedOrigins` is a static array or a function. As a workaround, you mentioned that using a static array works, which has been confirmed in updates addressing similar issues. To resolve this, you may want to check if you're using the latest version of Better Auth and keep an eye on related issues for fixes in future releases. There’s an ongoing effort to improve type inference for custom fields further, so stay tuned for any updates that may address this specific concern. For now, if you continue to encounter this problem, consider tagging @bekacru or @ping__ in your GitHub discussions for more tailored assistance! Is there anything specific you'd like help with while working through this? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@dosubot[bot] commented on GitHub (Jan 11, 2026):

This is a known TypeScript inference limitation in Better Auth: when you use a dynamic function for trustedOrigins, TypeScript can't narrow the config to a concrete literal type, so $Infer.Session.user loses your additionalFields and falls back to the base user shape. This happens because the compiler can't statically analyze the config object when a function is present, so it widens the type and omits custom fields like selectedOrganisationId [source].

Workarounds:

  • Define your config as a const and explicitly type your auth export using ReturnType<typeof betterAuth<typeof config>>. For example:

    const config = {
      baseURL: 'http://localhost:3000',
      trustedOrigins: async (request: Request) => ['http://localhost:3000'],
      user: {
        additionalFields: {
          selectedOrganisationId: { type: 'string' },
        },
      },
    } as const;
    
    type Auth = ReturnType<typeof betterAuth<typeof config>>;
    export const auth: Auth = betterAuth(config);
    

    Then use Auth["$Infer"]["Session"] for correct types [source].

  • On the client, use the inferAdditionalFields plugin with your client auth instance to propagate custom fields into hooks like useSession [source].

  • As a last resort, manually cast the session or user object to the expected type in your code.

This issue is not specific to trustedOrigins itself, but to how TypeScript infers types when config contains dynamic functions. Using a static array for trustedOrigins works because it allows TypeScript to infer the config as a literal.

There is ongoing work to improve this, but for now, explicit typing or the plugin workaround is the best approach. If you split your config across files, ensure the full config (including additionalFields) is available in the same file for best inference [source]. Disabling exactOptionalPropertyTypes in your tsconfig can also help in some cases [source].

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3733974040 --> @dosubot[bot] commented on GitHub (Jan 11, 2026): <!-- Answer --> This is a known TypeScript inference limitation in Better Auth: when you use a dynamic function for `trustedOrigins`, TypeScript can't narrow the config to a concrete literal type, so `$Infer.Session.user` loses your `additionalFields` and falls back to the base user shape. This happens because the compiler can't statically analyze the config object when a function is present, so it widens the type and omits custom fields like `selectedOrganisationId` [[source](https://github.com/better-auth/better-auth/issues/6239)]. Workarounds: - Define your config as a `const` and explicitly type your `auth` export using `ReturnType<typeof betterAuth<typeof config>>`. For example: ```ts const config = { baseURL: 'http://localhost:3000', trustedOrigins: async (request: Request) => ['http://localhost:3000'], user: { additionalFields: { selectedOrganisationId: { type: 'string' }, }, }, } as const; type Auth = ReturnType<typeof betterAuth<typeof config>>; export const auth: Auth = betterAuth(config); ``` Then use `Auth["$Infer"]["Session"]` for correct types [[source](https://github.com/better-auth/better-auth/issues/2089)]. - On the client, use the `inferAdditionalFields` plugin with your client auth instance to propagate custom fields into hooks like `useSession` [[source](https://github.com/better-auth/better-auth/issues/3328)]. - As a last resort, manually cast the session or user object to the expected type in your code. This issue is not specific to `trustedOrigins` itself, but to how TypeScript infers types when config contains dynamic functions. Using a static array for `trustedOrigins` works because it allows TypeScript to infer the config as a literal. There is ongoing work to improve this, but for now, explicit typing or the plugin workaround is the best approach. If you split your config across files, ensure the full config (including `additionalFields`) is available in the same file for best inference [[source](https://github.com/better-auth/better-auth/issues/1870)]. Disabling `exactOptionalPropertyTypes` in your `tsconfig` can also help in some cases [[source](https://github.com/better-auth/better-auth/issues/5159)]. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/7d2130bc-40eb-4bc8-8fa5-fc9f9cb9ad30?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20TypeScript%20inference%20limitation%20in%20Better%20Auth%3A%20when%20you%20use%20a%20dynamic%20function%20for%20%60trustedOrigins%60%2C%20TypeScript%20can%27t%20narrow%20the%20config%20to%20a%20concrete%20literal%20type%2C%20so%20%60%24Infer.Session.user%60%20loses%20your%20%60additionalFields%60%20and%20falls%20back%20to%20the%20base%20user%20shape.%20This%20happens%20because%20the%20compiler%20can%27t%20statically%20analyze%20the%20config%20object%20when%20a%20function%20is%20present%2C%20so%20it%20widens%20the%20type%20and%20omits%20custom%20fields%20like%20%60selectedOrganisationId%60%20%5B%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/6239%29%5D.%0A%0AWorkarounds%3A%0A-%20Define%20your%20config%20as%20a%20%60const%60%20and%20explicitly%20type%20your%20%60auth%60%20export%20using%20%60ReturnType%3Ctypeof%20betterAuth%3Ctypeof%20config%3E%3E%60.%20For%20example%3A%0A%20%20%60%60%60ts%0A%20%20const%20config%20%3D%20%7B%0A%20%20%20%20baseURL%3A%20%27http%3A//localhost%3A3000%27%2C%0A%20%20%20%20trustedOrigins%3A%20async%20%28request%3A%20Request%29%20%3D%3E%20%5B%27http%3A//localhost%3A3000%27%5D%2C%0A%20%20%20%20user%3A%20%7B%0A%20%20%20%20%20%20additionalFields%3A%20%7B%0A%20%20%20%20%20%20%20%20selectedOrganisationId%3A%20%7B%20type%3A%20%27string%27%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%20as%20const%3B%0A%0A%20%20type%20Auth%20%3D%20ReturnType%3Ctypeof%20betterAuth%3Ctypeof%20config%3E%3E%3B%0A%20%20export%20const%20auth%3A%20Auth%20%3D%20betterAuth%28config%29%3B%0A%20%20%60%60%60%0A%20%20Then%20use%20%60Auth%5B%22%24Infer%22%5D%5B%22Session%22%5D%60%20for%20correct%20types%20%5B%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/2089%29%5D.%0A%0A-%20On%20the%20client%2C%20use%20the%20%60inferAdditionalFields%60%20plugin%20with%20your%20client%20auth%20instance%20to%20propagate%20custom%20fields%20into%20hooks%20like%20%60useSession%60%20%5B%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/3328%29%5D.%0A%0A-%20As%20a%20last%20resort%2C%20manually%20cast%20the%20session%20or%20user%20object%20to%20the%20expected%20type%20in%20your%20code.%0A%0AThis%20issue%20is%20not%20specific%20to%20%60trustedOrigins%60%20itself%2C%20but%20to%20how%20TypeScript%20infers%20types%20when%20config%20contains%20dynamic%20functions.%20Using%20a%20static%20array%20for%20%60trustedOrigins%60%20works%20because%20it%20allows%20TypeScript%20to%20infer%20the%20config%20as%20a%20literal.%0A%0AThere%20is%20ongoing%20work%20to%20improve%20this%2C%20but%20for%20now%2C%20explicit%20typing%20or%20the%20plugin%20workaround%20is%20the%20best%20approach.%20If%20you%20split%20your%20config%20across%20files%2C%20ensure%20the%20full%20config%20%28including%20%60additionalFields%60%29%20is%20available%20in%20the%20same%20file%20for%20best%20inference%20%5B%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/1870%29%5D.%20Disabling%20%60exactOptionalPropertyTypes%60%20in%20your%20%60tsconfig%60%20can%20also%20help%20in%20some%20cases%20%5B%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/5159%29%5D.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/7261)
Author
Owner

@bytaesu commented on GitHub (Jan 11, 2026):

Hi @AndKenneth,

It seems like temporary issue or TypeScript configuration issue.
Could you check our docs and feel free to let me know if anything is unclear 🙏

https://www.better-auth.com/docs/concepts/typescript

Image
<!-- gh-comment-id:3734225788 --> @bytaesu commented on GitHub (Jan 11, 2026): Hi @AndKenneth, It seems like temporary issue or TypeScript configuration issue. Could you check our docs and feel free to let me know if anything is unclear 🙏 https://www.better-auth.com/docs/concepts/typescript <img width="708" height="428" alt="Image" src="https://github.com/user-attachments/assets/34ca482f-5ea8-4fc5-bd69-e86c71e99eb0" />
Author
Owner

@flythenimbus commented on GitHub (Feb 5, 2026):

I have the same problem @bytaesu - better auth can't infer the type for user when the admin plugin is enabled. This is causing me problems overall, because the admin plugin errors on its own and I've been ignoring it for months now

		// @ts-expect-error admin plugin type mismatch - library author will fix someday
		admin({
			defaultRole: "user",
			ac,
			roles: {
				user: userRole,
				admin: adminRole,
				partner: partnerRole,
				super_admin: superAdminRole,
			} as unknown as Record<string, never>,
		}),
Property 'email' is optional in type '{ id: string; createdAt: Date; updatedAt: Date; emailVerified: boolean; name: string; email?: string; image?: string; } & Record<string, unknown>' but required in type '{ id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image?: string; }'.

This is being tracked in #5047 - where now I see you mention that it is fixed, but it isn't working for me and I get the same error.

This is my entire config

import { env, waitUntil } from "cloudflare:workers";
import { passkey } from "@better-auth/passkey";
import { SignupEmailBodySchema } from "@myapp/contracts";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, anonymous, captcha, twoFactor } from "better-auth/plugins";
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "@/db/schema";
import {
	send2faOtpEmail,
	sendChangeEmailConfirmation,
	sendPasswordResetEmail,
	sendVerificationEmail,
} from "@/services/email";
import { setupStripeCustomer } from "./services/subscription";

/**
 * Access control for the admin plugin.
 * Extends the default admin statements so both admin and super_admin
 * retain all built-in admin permissions.
 */
const ac = createAccessControl(defaultStatements);
const userRole = ac.newRole({});
const adminRole = ac.newRole({ ...adminAc.statements });
const superAdminRole = ac.newRole({ ...adminAc.statements });
const partnerRole = ac.newRole({});

const db = drizzle(env.DB, { schema });

const secondaryStorage = {
	get: (key: string) => env.USER_STORAGE.get(key),
	set: (key: string, value: string, expirationTtl?: number) =>
		env.USER_STORAGE.put(key, value, {
			expirationTtl: expirationTtl ? Math.max(expirationTtl, 60) : undefined,
		}),
	delete: (key: string) => env.USER_STORAGE.delete(key),
};

export const auth = betterAuth({
	appName: "MyApp",
	database: drizzleAdapter(db, { provider: "sqlite", schema }),
	secondaryStorage,
	baseURL: env.BETTER_AUTH_URL,
	secret: env.BETTER_AUTH_SECRET,
	trustedOrigins: [env.APP_ORIGIN, env.ADMIN_ORIGIN].filter(Boolean),
	session: {
		cookieCache: {
			enabled: true,
			maxAge: 5 * 60, // 5 minutes
		},
	},
	user: {
		additionalFields: {
			stripeCustomerId: {
				type: "string",
				required: false,
				input: false,
			},
			acceptedTermsAt: {
				type: "date",
				required: false,
				input: true,
			},
			isAnonymous: {
				type: "boolean",
				required: false,
				defaultValue: false,
				input: false,
			},
		},
		changeEmail: {
			enabled: true,
			sendChangeEmailConfirmation: async ({ user, newEmail, url }) => {
				if (!newEmail || !user.name || !user.email) {
					return;
				}

				const { email, name } = user;
				sendChangeEmailConfirmation({ email, name }, url);
			},
		},
	},
	socialProviders: {
		github: {
			clientId: env.GITHUB_CLIENT_ID,
			clientSecret: env.GITHUB_CLIENT_SECRET,
		},
		google: {
			clientId: env.GOOGLE_CLIENT_ID,
			clientSecret: env.GOOGLE_CLIENT_SECRET,
		},
	},
	emailAndPassword: {
		enabled: true,
		autoSignIn: true,
		requireEmailVerification: false,
		sendResetPassword: async ({ user, url }) => {
			if (!user.email || !user.name) return;
			const { email, name } = user;
			sendPasswordResetEmail({ email, name }, url);
		},
	},
	emailVerification: {
		sendOnSignUp: true,
		autoSignInAfterVerification: false,
		sendVerificationEmail: async ({ user, url }) => {
			if (!user.email || !user.name) return;
			const { email, name } = user;
			sendVerificationEmail({ email, name }, url);
		},
	},
	rateLimit: {
		storage: "secondary-storage",
	},
	advanced: {
		crossSubDomainCookies: {
			enabled: env.ENVIRONMENT !== "development",
		},
		ipAddress: {
			ipAddressHeaders: ["cf-connecting-ip"],
		},
	},
	plugins: [
		passkey(),
		twoFactor({
			otpOptions: {
				async sendOTP({ user, otp }) {
					if (!user.email) return;
					send2faOtpEmail({ email: user.email, name: user.name }, otp);
				},
			},
			skipVerificationOnEnable: true,
		}),
		admin({
			defaultRole: "user",
			ac,
			roles: {
				user: userRole,
				admin: adminRole,
				partner: partnerRole,
				super_admin: superAdminRole,
			} as unknown as Record<string, never>,
		}),
		captcha({
			provider: "cloudflare-turnstile",
			secretKey: env.TURNSTILE_SECRET_KEY,
		}),
		anonymous({
			emailDomainName: "anon.example.com",
		}),
	],
});

export type Auth = typeof auth;
export type User = typeof auth.$Infer.Session.user;

<!-- gh-comment-id:3856039690 --> @flythenimbus commented on GitHub (Feb 5, 2026): I have the same problem @bytaesu - better auth can't infer the type for user when the `admin` plugin is enabled. This is causing me problems overall, because the admin plugin errors on its own and I've been ignoring it for months now ``` // @ts-expect-error admin plugin type mismatch - library author will fix someday admin({ defaultRole: "user", ac, roles: { user: userRole, admin: adminRole, partner: partnerRole, super_admin: superAdminRole, } as unknown as Record<string, never>, }), ``` ``` Property 'email' is optional in type '{ id: string; createdAt: Date; updatedAt: Date; emailVerified: boolean; name: string; email?: string; image?: string; } & Record<string, unknown>' but required in type '{ id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image?: string; }'. ``` This is being tracked in #5047 - where now I see you mention that it is fixed, but it isn't working for me and I get the same error. This is my entire config ``` import { env, waitUntil } from "cloudflare:workers"; import { passkey } from "@better-auth/passkey"; import { SignupEmailBodySchema } from "@myapp/contracts"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { admin, anonymous, captcha, twoFactor } from "better-auth/plugins"; import { createAccessControl } from "better-auth/plugins/access"; import { adminAc, defaultStatements } from "better-auth/plugins/admin/access"; import { drizzle } from "drizzle-orm/d1"; import * as schema from "@/db/schema"; import { send2faOtpEmail, sendChangeEmailConfirmation, sendPasswordResetEmail, sendVerificationEmail, } from "@/services/email"; import { setupStripeCustomer } from "./services/subscription"; /** * Access control for the admin plugin. * Extends the default admin statements so both admin and super_admin * retain all built-in admin permissions. */ const ac = createAccessControl(defaultStatements); const userRole = ac.newRole({}); const adminRole = ac.newRole({ ...adminAc.statements }); const superAdminRole = ac.newRole({ ...adminAc.statements }); const partnerRole = ac.newRole({}); const db = drizzle(env.DB, { schema }); const secondaryStorage = { get: (key: string) => env.USER_STORAGE.get(key), set: (key: string, value: string, expirationTtl?: number) => env.USER_STORAGE.put(key, value, { expirationTtl: expirationTtl ? Math.max(expirationTtl, 60) : undefined, }), delete: (key: string) => env.USER_STORAGE.delete(key), }; export const auth = betterAuth({ appName: "MyApp", database: drizzleAdapter(db, { provider: "sqlite", schema }), secondaryStorage, baseURL: env.BETTER_AUTH_URL, secret: env.BETTER_AUTH_SECRET, trustedOrigins: [env.APP_ORIGIN, env.ADMIN_ORIGIN].filter(Boolean), session: { cookieCache: { enabled: true, maxAge: 5 * 60, // 5 minutes }, }, user: { additionalFields: { stripeCustomerId: { type: "string", required: false, input: false, }, acceptedTermsAt: { type: "date", required: false, input: true, }, isAnonymous: { type: "boolean", required: false, defaultValue: false, input: false, }, }, changeEmail: { enabled: true, sendChangeEmailConfirmation: async ({ user, newEmail, url }) => { if (!newEmail || !user.name || !user.email) { return; } const { email, name } = user; sendChangeEmailConfirmation({ email, name }, url); }, }, }, socialProviders: { github: { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET, }, google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }, }, emailAndPassword: { enabled: true, autoSignIn: true, requireEmailVerification: false, sendResetPassword: async ({ user, url }) => { if (!user.email || !user.name) return; const { email, name } = user; sendPasswordResetEmail({ email, name }, url); }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: false, sendVerificationEmail: async ({ user, url }) => { if (!user.email || !user.name) return; const { email, name } = user; sendVerificationEmail({ email, name }, url); }, }, rateLimit: { storage: "secondary-storage", }, advanced: { crossSubDomainCookies: { enabled: env.ENVIRONMENT !== "development", }, ipAddress: { ipAddressHeaders: ["cf-connecting-ip"], }, }, plugins: [ passkey(), twoFactor({ otpOptions: { async sendOTP({ user, otp }) { if (!user.email) return; send2faOtpEmail({ email: user.email, name: user.name }, otp); }, }, skipVerificationOnEnable: true, }), admin({ defaultRole: "user", ac, roles: { user: userRole, admin: adminRole, partner: partnerRole, super_admin: superAdminRole, } as unknown as Record<string, never>, }), captcha({ provider: "cloudflare-turnstile", secretKey: env.TURNSTILE_SECRET_KEY, }), anonymous({ emailDomainName: "anon.example.com", }), ], }); export type Auth = typeof auth; export type User = typeof auth.$Infer.Session.user; ```
Author
Owner

@ping-maxwell commented on GitHub (Feb 11, 2026):

Hello I'm closing this issue as everything is working as expected and most likely not a bug from BA, this is most probably a misconfiguration on tsconfig or something else.

@dugajean I just copied your setup and can infer it:

Image
<!-- gh-comment-id:3883225596 --> @ping-maxwell commented on GitHub (Feb 11, 2026): Hello I'm closing this issue as everything is working as expected and most likely not a bug from BA, this is most probably a misconfiguration on tsconfig or something else. @dugajean I just copied your setup and can infer it: <img width="1358" height="374" alt="Image" src="https://github.com/user-attachments/assets/0160f538-dc8e-4287-a092-847e84faa57b" />
Author
Owner

@Chardrazle commented on GitHub (Mar 10, 2026):

I had a similar build issue after I'd added a dynamic trustedOrigins entry (in the auth config).
I got the long-winded typescript errors, when accessing the user field of the const { data } = useSession(); (for which I had additionalFields).

For the helper function, I'd used the signature:
function getTrustedOrigins(request: Request)
but this needed to be:
function getTrustedOrigins(request: Request | undefined)
🤦
Moral: check your types.

<!-- gh-comment-id:4030813750 --> @Chardrazle commented on GitHub (Mar 10, 2026): I had a similar build issue after I'd added a dynamic `trustedOrigins` entry (in the _auth_ config). I got the long-winded typescript errors, when accessing the `user` field of the `const { data } = useSession();` (for which I had `additionalFields`). For the helper function, I'd used the signature: `function getTrustedOrigins(request: Request)` but this needed to be: `function getTrustedOrigins(request: Request | undefined)` 🤦 Moral: check your types.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28096