[GH-ISSUE #9101] docs: clarify if testUtils plugin is fine to unconditionally include in prod #11275

Open
opened 2026-04-13 07:37:19 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @jippi on GitHub (Apr 10, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9101

Is this suited for github?

  • Yes, this is suited for github

Reproduction

N/A

Current vs. Expected behavior

its not clear if it must only be used in testing context, or if its safe to just always add to the plugin list unconditionally

obviously you shouldn't use the test helpers IN prod (e.g. for signup) - but the docs are not clear on wether its safe to even load it unconditionally and in prod

What version of Better Auth are you using?

N/A

System info

N/A

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

Documentation

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @jippi on GitHub (Apr 10, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9101 ### Is this suited for github? - [x] Yes, this is suited for github ### Reproduction N/A ### Current vs. Expected behavior its not clear if it must only be used in testing context, or if its safe to just always add to the plugin list unconditionally obviously you shouldn't use the test helpers IN prod (e.g. for signup) - but the docs are not clear on wether its safe to even load it unconditionally and in prod ### What version of Better Auth are you using? N/A ### System info ```bash N/A ``` ### Which area(s) are affected? (Select all that apply) Documentation ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the docs label 2026-04-13 07:37:19 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Apr 10, 2026):

The plugin does not register any HTTP routes or API endpoints — it only exposes helpers on the internal ctx.test object [1]. So simply having it in your plugins array won't create externally exploitable endpoints.

That said, the docs explicitly warn against production usage, and for good reason:

  • It provides powerful capabilities like arbitrary session creation without credentials and direct DB manipulation via ctx.test [2] [3]. If application code is compromised, these become an escalation vector.
  • When captureOTP is enabled, a database hook runs on every verification record creation, storing OTPs in memory [4] — unnecessary overhead in prod.
  • There's a known dependency issue where vitest (an optional peer dep) gets pulled into production installs, adding ~30MB of bloat.

Recommended approach — conditionally include it:

import { betterAuth } from "better-auth";
import { testUtils } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    ...(process.env.NODE_ENV === "test"
      ? [testUtils({ captureOTP: true })]
      : []),
  ],
});

TL;DR: Loading it in prod won't expose dangerous routes, but it adds unnecessary risk surface, memory overhead, and dependency bloat with no upside. Conditionally include it.

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:4223994337 --> @dosubot[bot] commented on GitHub (Apr 10, 2026): <!-- Answer --> The plugin **does not register any HTTP routes or API endpoints** — it only exposes helpers on the internal `ctx.test` object [[1]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/index.ts#L143-L146). So simply having it in your plugins array won't create externally exploitable endpoints. That said, the [docs explicitly warn](https://www.better-auth.com/docs/plugins/test-utils) against production usage, and for good reason: - It provides powerful capabilities like **arbitrary session creation** without credentials and **direct DB manipulation** via `ctx.test` [[2]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/auth-helpers.ts#L5-L30) [[3]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/db-helpers.ts). If application code is compromised, these become an escalation vector. - When `captureOTP` is enabled, a **database hook** runs on every verification record creation, storing OTPs in memory [[4]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/index.ts#L105-L141) — unnecessary overhead in prod. - There's a [known dependency issue](https://github.com/better-auth/better-auth/issues/9083#issuecomment-4222304135) where `vitest` (an optional peer dep) gets pulled into production installs, adding ~30MB of bloat. **Recommended approach** — conditionally include it: ```typescript import { betterAuth } from "better-auth"; import { testUtils } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ ...(process.env.NODE_ENV === "test" ? [testUtils({ captureOTP: true })] : []), ], }); ``` **TL;DR:** Loading it in prod won't expose dangerous routes, but it adds unnecessary risk surface, memory overhead, and dependency bloat with no upside. Conditionally include it. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=95ebdf4d-4d85-45da-b12d-6b0ca628f9bf&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&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=The%20plugin%20%2A%2Adoes%20not%20register%20any%20HTTP%20routes%20or%20API%20endpoints%2A%2A%20%E2%80%94%20it%20only%20exposes%20helpers%20on%20the%20internal%20%60ctx.test%60%20object%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/index.ts%23L143-L146%29.%20So%20simply%20having%20it%20in%20your%20plugins%20array%20won%27t%20create%20externally%20exploitable%20endpoints.%0A%0AThat%20said%2C%20the%20%5Bdocs%20explicitly%20warn%5D%28https%3A//www.better-auth.com/docs/plugins/test-utils%29%20against%20production%20usage%2C%20and%20for%20good%20reason%3A%0A%0A-%20It%20provides%20powerful%20capabilities%20like%20%2A%2Aarbitrary%20session%20creation%2A%2A%20without%20credentials%20and%20%2A%2Adirect%20DB%20manipulation%2A%2A%20via%20%60ctx.test%60%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/auth-helpers.ts%23L5-L30%29%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/db-helpers.ts%29.%20If%20application%20code%20is%20compromised%2C%20these%20become%20an%20escalation%20vector.%0A-%20When%20%60captureOTP%60%20is%20enabled%2C%20a%20%2A%2Adatabase%20hook%2A%2A%20runs%20on%20every%20verification%20record%20creation%2C%20storing%20OTPs%20in%20memory%20%5B%5B4%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/index.ts%23L105-L141%29%20%E2%80%94%20unnecessary%20overhead%20in%20prod.%0A-%20There%27s%20a%20%5Bknown%20dependency%20issue%5D%28https%3A//github.com/better-auth/better-auth/issues/9083%23issuecomment-4222304135%29%20where%20%60vitest%60%20%28an%20optional%20peer%20dep%29%20gets%20pulled%20into%20production%20installs%2C%20adding%20~30MB%20of%20bloat.%0A%0A%2A%2ARecommended%20approach%2A%2A%20%E2%80%94%20conditionally%20include%20it%3A%0A%0A%60%60%60typescript%0Aimport%20%7B%20betterAuth%20%7D%20from%20%22better-auth%22%3B%0Aimport%20%7B%20testUtils%20%7D%20from%20%22better-auth/plugins%22%3B%0A%0Aexport%20const%20auth%20%3D%20betterAuth%28%7B%0A%20%20plugins%3A%20%5B%0A%20%20%20%20...%28process.env.NODE_ENV%20%3D%3D%3D%20%22test%22%0A%20%20%20%20%20%20%3F%20%5BtestUtils%28%7B%20captureOTP%3A%20true%20%7D%29%5D%0A%20%20%20%20%20%20%3A%20%5B%5D%29%2C%0A%20%20%5D%2C%0A%7D%29%3B%0A%60%60%60%0A%0A%2A%2ATL%3BDR%3A%2A%2A%20Loading%20it%20in%20prod%20won%27t%20expose%20dangerous%20routes%2C%20but%20it%20adds%20unnecessary%20risk%20surface%2C%20memory%20overhead%2C%20and%20dependency%20bloat%20with%20no%20upside.%20Conditionally%20include%20it.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/9101)
Author
Owner

@jippi commented on GitHub (Apr 10, 2026):

@dosu when doing it conditionally, the typescript engine can't infer any of the helpers functions/methods when using them on the auth const

<!-- gh-comment-id:4224054377 --> @jippi commented on GitHub (Apr 10, 2026): @dosu when doing it conditionally, the typescript engine can't infer any of the helpers functions/methods when using them on the auth const
Author
Owner

@dosubot[bot] commented on GitHub (Apr 10, 2026):

Good point — this is a known limitation. Better Auth's plugin type inference relies on statically analyzable plugin arrays (via generic type parameters on betterAuth<Options>) [1]. Conditional spreads produce a union type that breaks both the tuple-recursion and union-to-intersection inference paths [2].

A few options:

1. Include unconditionally (simplest)
As covered in the previous comment, the plugin doesn't expose HTTP routes — it only adds helpers to ctx.test [3]. So including it unconditionally preserves full type inference and is the path of least resistance:

export const auth = betterAuth({
  plugins: [
    testUtils({ captureOTP: true }),
  ],
});

2. Separate config files per environment
Create auth.ts (prod) and auth.test.ts (test) with different plugin lists. Import the appropriate one based on your environment. This keeps full type safety in both contexts without any conditional logic in the plugin array.

3. Explicit type annotation (last resort)
If you must conditionally include it, you can force the type, but this sacrifices some type safety:

import type { Auth } from "better-auth";

export const auth = betterAuth({
  plugins: [
    ...(process.env.NODE_ENV === "test"
      ? [testUtils({ captureOTP: true })]
      : []),
  ],
}) as Auth<{ plugins: [ReturnType<typeof testUtils>] }>;

Option 1 or 2 are the recommended approaches [4]. as const won't help here since the conditional spread still produces a union type at the TypeScript level.

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:4224085050 --> @dosubot[bot] commented on GitHub (Apr 10, 2026): <!-- Answer --> Good point — this is a known limitation. Better Auth's plugin type inference relies on statically analyzable plugin arrays (via generic type parameters on `betterAuth<Options>`) [[1]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/auth/full.ts#L27-L31). Conditional spreads produce a union type that breaks both the tuple-recursion and union-to-intersection inference paths [[2]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/types/helper.ts#L77-L87). A few options: **1. Include unconditionally (simplest)** As covered in the previous comment, the plugin doesn't expose HTTP routes — it only adds helpers to `ctx.test` [[3]](https://github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/index.ts#L143-L148). So including it unconditionally preserves full type inference and is the path of least resistance: ```typescript export const auth = betterAuth({ plugins: [ testUtils({ captureOTP: true }), ], }); ``` **2. Separate config files per environment** Create `auth.ts` (prod) and `auth.test.ts` (test) with different plugin lists. Import the appropriate one based on your environment. This keeps full type safety in both contexts without any conditional logic in the plugin array. **3. Explicit type annotation (last resort)** If you must conditionally include it, you can force the type, but this sacrifices some type safety: ```typescript import type { Auth } from "better-auth"; export const auth = betterAuth({ plugins: [ ...(process.env.NODE_ENV === "test" ? [testUtils({ captureOTP: true })] : []), ], }) as Auth<{ plugins: [ReturnType<typeof testUtils>] }>; ``` Option 1 or 2 are the recommended approaches [[4]](https://github.com/better-auth/better-auth/issues/6965). `as const` won't help here since the conditional spread still produces a union type at the TypeScript level. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=c8729350-c10d-4095-b326-d1bed382ce19&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=c8729350-c10d-4095-b326-d1bed382ce19&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=c8729350-c10d-4095-b326-d1bed382ce19&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=c8729350-c10d-4095-b326-d1bed382ce19&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=c8729350-c10d-4095-b326-d1bed382ce19&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=c8729350-c10d-4095-b326-d1bed382ce19&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=c8729350-c10d-4095-b326-d1bed382ce19&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&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Good%20point%20%E2%80%94%20this%20is%20a%20known%20limitation.%20Better%20Auth%27s%20plugin%20type%20inference%20relies%20on%20statically%20analyzable%20plugin%20arrays%20%28via%20generic%20type%20parameters%20on%20%60betterAuth%3COptions%3E%60%29%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/auth/full.ts%23L27-L31%29.%20Conditional%20spreads%20produce%20a%20union%20type%20that%20breaks%20both%20the%20tuple-recursion%20and%20union-to-intersection%20inference%20paths%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/types/helper.ts%23L77-L87%29.%0A%0AA%20few%20options%3A%0A%0A%2A%2A1.%20Include%20unconditionally%20%28simplest%29%2A%2A%0AAs%20covered%20in%20the%20previous%20comment%2C%20the%20plugin%20doesn%27t%20expose%20HTTP%20routes%20%E2%80%94%20it%20only%20adds%20helpers%20to%20%60ctx.test%60%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/c5066fe5d68babf2376cfc63d813de5542eca463/packages/better-auth/src/plugins/test-utils/index.ts%23L143-L148%29.%20So%20including%20it%20unconditionally%20preserves%20full%20type%20inference%20and%20is%20the%20path%20of%20least%20resistance%3A%0A%0A%60%60%60typescript%0Aexport%20const%20auth%20%3D%20betterAuth%28%7B%0A%20%20plugins%3A%20%5B%0A%20%20%20%20testUtils%28%7B%20captureOTP%3A%20true%20%7D%29%2C%0A%20%20%5D%2C%0A%7D%29%3B%0A%60%60%60%0A%0A%2A%2A2.%20Separate%20config%20files%20per%20environment%2A%2A%0ACreate%20%60auth.ts%60%20%28prod%29%20and%20%60auth.test.ts%60%20%28test%29%20with%20different%20plugin%20lists.%20Import%20the%20appropriate%20one%20based%20on%20your%20environment.%20This%20keeps%20full%20type%20safety%20in%20both%20contexts%20without%20any%20conditional%20logic%20in%20the%20plugin%20array.%0A%0A%2A%2A3.%20Explicit%20type%20annotation%20%28last%20resort%29%2A%2A%0AIf%20you%20must%20conditionally%20include%20it%2C%20you%20can%20force%20the%20type%2C%20but%20this%20sacrifices%20some%20type%20safety%3A%0A%0A%60%60%60typescript%0Aimport%20type%20%7B%20Auth%20%7D%20from%20%22better-auth%22%3B%0A%0Aexport%20const%20auth%20%3D%20betterAuth%28%7B%0A%20%20plugins%3A%20%5B%0A%20%20%20%20...%28process.env.NODE_ENV%20%3D%3D%3D%20%22test%22%0A%20%20%20%20%20%20%3F%20%5BtestUtils%28%7B%20captureOTP%3A%20true%20%7D%29%5D%0A%20%20%20%20%20%20%3A%20%5B%5D%29%2C%0A%20%20%5D%2C%0A%7D%29%20as%20Auth%3C%7B%20plugins%3A%20%5BReturnType%3Ctypeof%20testUtils%3E%5D%20%7D%3E%3B%0A%60%60%60%0A%0AOption%201%20or%202%20are%20the%20recommended%20approaches%20%5B%5B4%5D%5D%28https%3A//github.com/better-auth/better-auth/issues/6965%29.%20%60as%20const%60%20won%27t%20help%20here%20since%20the%20conditional%20spread%20still%20produces%20a%20union%20type%20at%20the%20TypeScript%20level.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/9101)
Author
Owner

@jippi commented on GitHub (Apr 10, 2026):

great - I think the docs could use a lot of this context and clarification to make it more clear and safe to to work with

<!-- gh-comment-id:4224341383 --> @jippi commented on GitHub (Apr 10, 2026): great - I think the docs could use a lot of this context and clarification to make it more clear and safe to to work with
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11275