Better Auth Documentation Audit
Better Auth ships substantial, well-structured developer docs with real depth (concepts, adapters, plugins, framework integrations, AI-resource surfaces, and an MCP server), but the docs carry several concrete contradictions, broken links, and migration gaps that will actively bite developers and AI agents copying snippets out of them.
1. Rate-limit window default contradicts itself across three pages (critical)
Location: /docs/concepts/rate-limit and /docs/reference/options
Problem: The rate-limit concept page prose says the production default is window 60 seconds, max 100 requests, but the example config on the same page uses window: 10. The canonical /docs/reference/options page lists the default as window 10 seconds, max 100. So the same option has two contradictory documented defaults (60s vs 10s) — a 6× difference.
Consequence: A developer (or an AI agent extracting "production defaults" to size their threat model or alerting) will pick whichever value they hit first and silently misconfigure either their abuse protection (if they trust 60s) or their alerting thresholds (if they trust 10s). Agents can't reconcile this — they'll emit confidently wrong config comments.
The fix: Pick one default (the reference options page is the canonical source, so 10 seconds), correct the rate-limit concept page prose, and add a single sentence cross-linking to /docs/reference/options as the source of truth.
2. CLI command name inconsistent across plugin docs — npx auth vs npx auth@latest (significant)
Location: /docs/concepts/cli vs /docs/plugins/2fa, /docs/plugins/oidc-provider, /docs/plugins/stripe, /docs/basic-usage
Problem: The CLI doc uses npx auth@latest generate / npx auth@latest migrate consistently. Multiple plugin pages (2FA, OIDC Provider, Stripe) instruct users to run npx auth migrate / npx auth generate without the @latest tag. The basic-usage page also uses npx auth migrate / npx auth generate.
Consequence: npx auth resolves to whichever cached version the user has locally; npx auth@latest always fetches the latest. For a project that recently introduced a "standalone CLI" in v1.5.0 (per the changelog) and a "Dual Module Hazard" FAQ specifically tied to mixed versions, this is exactly the kind of inconsistency that produces silent schema-drift bugs against the wrong CLI version. Agents copying these snippets will mix conventions across the same project.
The fix: Sweep all plugin and getting-started pages to use npx auth@latest (or commit to npx auth everywhere and document the pinning expectations). Add the canonical form to a "CLI commands" snippet in llms.txt so agents resolve it once.
3. Prisma adapter: install command and import path point at two different packages (significant)
Location: /docs/adapters/prisma
Problem: The install commands instruct npm install @better-auth/prisma-adapter, but the very next code block imports from better-auth/adapters/prisma — i.e., from the main better-auth package, not the separately installed adapter. The page's own reviewer note flags this. The changelog explains why: v1.5.0 extracted "Multiple adapter extraction (Drizzle, Prisma, Kysely, MongoDB)" into independent packages, but the import example was not updated. (Drizzle's page is internally consistent — install and import both use @better-auth/drizzle-adapter.)
Consequence: A developer follows the install command, gets @better-auth/prisma-adapter in their node_modules, then the import resolves against the bundled-in copy inside better-auth/adapters/prisma instead. This is precisely the "multiple versions of better-auth/@better-auth/core in the dependency tree" condition the FAQ calls the "Dual Module Hazard" — and Prisma is one of the adapters most likely to trip it.
The fix: Decide which path is canonical post-extraction. If the new package is canonical: change the import to import { prismaAdapter } from "@better-auth/prisma-adapter". If the nested path is canonical: drop the @better-auth/prisma-adapter install line. Either way, mirror Drizzle's internally-consistent shape and add a migration note to the changelog entry.
4. Two-Factor plugin canonical URL mismatch — /docs/plugins/two-factor 404s (significant)
Location: /docs/plugins/2fa (and 404 at /docs/plugins/two-factor)
Problem: The canonical URL for the two-factor plugin is /docs/plugins/2fa. The natural URL /docs/plugins/two-factor returns 404. The Sentinel page has the same shape: /docs/infrastructure/sentinel 404s, and the canonical lives at /docs/infrastructure/plugins/sentinel. The changelog page also exhibits this — /docs/changelogs 404s; the changelog lives at /changelog, no /docs prefix.
Consequence: Inbound links from blog posts, Stack Overflow answers, AI-generated docs, and the LLMs.txt mirror routinely use the spelled-out forms (two-factor, sentinel, changelogs). Every one of those dead-ends a developer or an agent fetching documentation by guessed slug — a particularly bad failure mode for the documentation MCP server they advertise.
The fix: Add HTTP 301 redirects: /docs/plugins/two-factor → /docs/plugins/2fa, /docs/infrastructure/sentinel → /docs/infrastructure/plugins/sentinel, /docs/changelogs → /changelog. These are cheap and break a real class of inbound-link failure.
5. Default secret is a fixed, well-known string (critical)
Location: /docs/reference/options
Problem: The secret option default is documented as the literal string "better-auth-secret-12345678901234567890". The page notes it throws in production if unset — but development mode silently uses this fixed value for session signing/encryption.
Consequence: Two real risks. (1) Anything signed with this key in development is forge-able by anyone reading the docs; if a developer's local instance is ever exposed (ngrok preview, CI artifact, a "staging" environment that didn't set NODE_ENV=production), sessions are trivially trivially forgeable. (2) Documentation that publishes a literal default secret will be hash-matched by leaked-secret scanners and used as a fingerprint to identify Better Auth instances. Compare to e.g. Django, which generates per-project secrets at install time.
The fix: Either generate a per-install dev secret on first run (the CLI already has npx auth@latest secret), or refuse to start without BETTER_AUTH_SECRET even in development and document a one-liner. At minimum, surface the dev-only fixed-secret risk explicitly on the installation and security pages — not buried in the reference options table.
6. Sign-up payload contradiction: image parameter exists, but image cannot be removed from the user table (significant)
Location: /docs/basic-usage and /docs/reference/faq
Problem: Basic-usage and email-password docs show authClient.signUp.email({ name, email, password, image, callbackURL }) — image is a first-class signup parameter. The FAQ states flatly: "At this time, you can't remove the name, image, or email fields from the user table. We do plan to have more customizability in the future in this regard, but for now, you can't remove these fields."
Consequence: Developers building products with no notion of a profile image (B2B internal tools, machine-to-machine accounts, SSO-only deployments where the IdP doesn't supply an avatar URL) cannot drop the column. This is an architectural constraint disclosed only in the FAQ, not on the database concept page or the schema/installation pages where developers actually decide what tables to create. Agents generating a schema have no way to know they can't omit these columns.
The fix: Move the "required fields on the user table" constraint into /docs/concepts/database as a top-level note (it's a schema contract, not a FAQ item). Better yet: actually allow opting out via a user.fields.image = false shape consistent with how custom fields are documented.
7. No structured error-code reference for any plugin (significant)
Location: Across all plugin docs (captcha, stripe, 2fa, magic-link, organization, sso, jwt) and /docs/concepts/api
Problem: The API concept page documents that errors are APIError instances with message/status and a $ERROR_CODES map on the client. The changelog v1.6.0 advertises "typed error codes across all plugins." But none of the plugin docs list the actual error codes they emit. The captcha plugin doc explicitly does not surface error codes for failed captcha validations beyond "blocks the request." The Stripe plugin has no list of webhook signature failure modes. The 2FA plugin lists no error codes for invalid TOTP, exhausted backup codes, or expired challenges.
Consequence: Developers writing client-side error UI must reverse-engineer codes by triggering each failure and inspecting responses, or read source. AI agents cannot generate exhaustive switch statements on $ERROR_CODES.* because the enum's members aren't documented. Given that v1.6.11 announced adding the change-email-disabled code, the catalog plainly exists internally — it's just not in the docs.
The fix: Generate a per-plugin "Error Codes" table from the typed error code enum introduced in v1.6.0 (likely a one-time codegen pass), and a global /docs/reference/error-codes index. This is exactly the kind of structured reference machine-readable docs and agents most need.
8. No OpenAPI / machine-readable API spec published (significant)
Location: Site-wide
Problem: Despite Better Auth being an HTTP API mounted at /api/auth/* with dozens of endpoints across core and plugins (sign-in, sign-up, sessions, 2FA, magic-link, SSO callbacks, OIDC provider, JWKS, Stripe webhooks, etc.), there is no published OpenAPI/Swagger spec. The ai-resources page advertises LLMs.txt and an MCP server, but nothing exposing endpoint shapes, parameter schemas, status codes, or response types in a machine-parseable format. The api concept page even notes the underlying framework is better-call "which enables calling REST endpoints as standard functions" — i.e., the routing layer plainly has enough information to emit OpenAPI.
Consequence: Agents that try to programmatically discover endpoints (Postman imports, API gateway integrations, generated clients, schema-driven testing) can't. Customers writing reverse-proxy rules in front of Better Auth (e.g., per-endpoint rate limit overrides, WAF tuning) have no canonical endpoint list. The OIDC Provider doc says you "MUST disable the /token endpoint" via disabledPaths: ["/token"] — but there's no master list anywhere of what paths exist to disable.
The fix: Emit an OpenAPI 3.1 spec at /api/auth/openapi.json (or publish a static one at /openapi.json in the docs site) generated from the better-call route registry. Link it from llms.txt. This will pay for itself in agent quality alone.
9. Clerk migration guide has no rollback or dual-run plan (significant)
Location: /docs/guides/clerk-migration-guide
Problem: The guide notes "the migration invalidates existing sessions" and instructs configuring Better Auth to use bcrypt to match Clerk's password hashes, but provides no staged dual-running guidance, no rollback procedure if the import fails partway, no instructions for verifying user counts/email collisions before cutover, no batching guidance for large user tables, and explicitly does not cover organization data transfer.
Consequence: A team migrating tens of thousands of users gets a CSV export, a TypeScript import script, and "your sessions are invalidated." If row 8,000 of 20,000 fails validation, there is no documented procedure to resume, roll back, or run both auth systems in parallel during cutover. This is exactly the migration shape where "skip rollback" causes weekend incidents.
The fix: Add a "Production migration" section: pre-flight checks (collision detection, schema validation), batched import with idempotency keys, verification step before cutover, a dual-run pattern (Better Auth shadow-validates Clerk-issued sessions for N days), and explicit rollback steps. The Stripe-migration video in /docs/reference/resources implies the team understands cutover; this guide should match.
10. Bearer-token / JWT auth flow underspecified end-to-end (significant)
Location: /docs/plugins/jwt vs the rest of the docs
Problem: The JWT plugin doc explains how to retrieve a token (authClient.token(), /token, or set-auth-jwt response header) and exposes a JWKS endpoint, but does not walk through the full lifecycle a backend developer needs: (1) how to validate a Better-Auth-issued JWT in an unrelated downstream service (sample code with jose + JWKS URL), (2) what the token's sub/aud/iss claims look like by default, (3) how long the token lives by default, (4) how rotation interacts with cached JWKS at consumers, and (5) when to use the JWT plugin vs the session cookie vs the JWT mode of cookie caching documented under session-management.
Consequence: A developer building a microservice that needs to validate Better Auth tokens has to guess claims, manually inspect a decoded JWT, and read the JWKS endpoint to figure out signing algorithms. Worse, there are now three ways to get a JWT-ish thing out of Better Auth (the JWT plugin, the jwt cookie-cache encoding, OIDC /oauth2/token) and the docs don't say when to choose which.
The fix: Add a "Verifying tokens in a downstream service" section to the JWT plugin doc with copy-paste-runnable verification code, a documented default-claims schema, and a comparison table: JWT plugin vs cookie-cache JWT mode vs OIDC Provider tokens — with use-case-driven guidance for which to pick.
11. OIDC Provider doc marked deprecated and not production-ready, but still shipped (significant)
Location: /docs/plugins/oidc-provider
Problem: The page carries two simultaneous warnings: "This plugin will soon be deprecated in favor of the OAuth Provider Plugin" and "This plugin is in active development and may not be suitable for production use." It also lists "JWKS Endpoint: Publish a JWKS endpoint to allow clients to verify tokens. (Not fully implemented)" as a "feature." Code samples have a formatting bug — unquoted string literals like client_name: My App, client_uri: https://client.example.com that aren't valid TS/JS. CLI commands use npx auth migrate (no @latest), inconsistent with the canonical CLI doc.
Consequence: A developer evaluating "can Better Auth be my OIDC provider?" gets a doc that says "yes but no, also not yet, also this is being replaced, also our code samples don't parse." Agents extracting these snippets emit code that fails to compile. The "OAuth Provider Plugin" link is the suggested replacement, but the deprecation date and migration path aren't given.
The fix: Either (a) keep this page but fence the broken examples as Markdown blocks with valid syntax, add a clear "deprecated in vX.Y, removed in vX.Z" timeline, and link to a migration guide; or (b) hide the page entirely and redirect to the OAuth Provider Plugin until that one is ready. Don't ship both warnings on the same page.
12. Captcha plugin doesn't document its error contract or misconfiguration behavior (minor)
Location: /docs/plugins/captcha
Problem: The plugin intercepts POST requests to configured endpoints and reads x-captcha-response headers, but the docs do not state: (a) what happens if the header is missing vs invalid vs the provider service is down, (b) what error code/status the client receives, (c) what happens if a captcha is configured for an endpoint that doesn't exist or has a typo, (d) whether failures count toward the rate-limiter.
Consequence: Developers can't build correct retry/error UI around captcha failures without trial-and-error. Misconfigured endpoint names fail silently (no captcha enforced) — the worst failure mode for a security plugin.
The fix: Add an "Error responses" table, document a startup-time warning for endpoints in endpoints that don't match any registered route, and clarify the interaction with rate-limit.
13. AI-Resources page lists Skills and an MCP server but the docs MCP page hardcodes a single transport (minor)
Location: /docs/ai-resources/mcp
Problem: The MCP setup doc lists Cursor, Claude Code, and Open Code with copy-paste config and a --cursor / --claude-code / --open-code CLI. Other major MCP clients (Cline, Continue, Zed, generic stdio clients) get a one-line "Manual: create/merge an mcp.json." There's no documentation of the protocol version supported, auth (the URL is unauthenticated https://mcp.better-auth.com/mcp), rate limits, or what data the MCP server actually exposes vs the LLMs.txt mirror.
Consequence: Teams standardizing on a non-listed client have to guess. Security-conscious teams have no way to know if querying the MCP server transmits any project context to Better Auth's servers (the telemetry doc covers the auth library's runtime telemetry, not the MCP service).
The fix: Add a "What this server exposes" section, document the protocol version + any auth, and list which clients are explicitly tested.
What they do well
- Genuine first-class AI-resources surface — LLMs.txt, MCP server, and Skills are documented in one place at
/docs/ai-resources, which is rare and valuable. - Specific, evidence-cited security posture — the security reference page explains scrypt choice, CSRF defenses (origin header, SameSite=Lax, Fetch Metadata), signup-enumeration mitigation, and OAuth state/PKCE storage in concrete terms rather than buzzwords.
- Strong framework-integration coverage with real gotchas — Express explicitly warns about
express.json()hanging the handler; Next.js cookie plugin must be "last"; SvelteKit needs thesveltekitCookiesplugin for server actions. These are the failure modes developers actually hit.
Top 3 recommendations
- Pick one canonical CLI form and one canonical adapter package shape, then sweep every page. The
npx authvsnpx auth@latestsplit and the Prisma install/import mismatch are the two highest-confusion contradictions and both stem from incomplete propagation of the v1.5.0 CLI/adapter extraction. - Publish an OpenAPI spec and a global error-code reference. The framework is HTTP-rooted, the changelog says typed error codes already exist internally, and the AI-resources page advertises agent-readiness — but neither machine-readable surface ships. These are the two highest-leverage additions for agent and integrator quality.
- Add HTTP 301 redirects for the natural-spelling URLs and stop using a fixed default
secretvalue. Both are tiny code changes with outsized impact: the redirects fix a real class of inbound-link failure, and removing the hardcoded dev secret closes a security footgun documented in plain sight on the reference options page.