Warning
Unofficial, alpha, development-only tooling for Better Auth. Do not enable it in production.
better-auth-devtools is a community Better Auth plugin for local auth scenario testing. It gives you managed test users, instant session switching, session inspection, and a React panel for approved session-field edits.
- The plugin stores managed test-user records in its own Better Auth model.
- Each managed test-user record points to a real Better Auth user in your app.
- Switching users creates a Better Auth session for that real user.
- The panel only shows and edits fields your app exposes through
getSessionViewandpatchSession.
AI agent prompt:
Install and integrate better-auth-devtools as an unofficial development-only Better Auth utility. Use better-auth-devtools/plugin for the Better Auth server/client plugin setup and better-auth-devtools/react for the floating panel. Keep it disabled in production, require DEV_AUTH_ENABLED=true, use managed test users only, create real host-app users in createManagedUser, let the plugin provide its Better Auth schema, rerun the Better Auth CLI after adding it, and for Next.js App Router keep DB-backed devtools config on the server while passing panelProps into a client wrapper.
pnpm add better-auth-devtoolsPeer requirements:
pnpm add better-auth react react-domDefine your templates and host-app callbacks once. In Next.js App Router, keep database-backed devtools code on the server and pass panel props into a client wrapper from a server layout.
Use these subpath exports:
import {
createDevtoolsIntegration,
defineDevtoolsConfig,
} from "better-auth-devtools/plugin";
import { BetterAuthDevtools } from "better-auth-devtools/react";Required environment guard:
DEV_AUTH_ENABLED=trueThe devtools run only when DEV_AUTH_ENABLED=true and the app is not running in production.
import {
createDevtoolsIntegration,
defineDevtoolsConfig,
} from "better-auth-devtools/plugin";
export const devtoolsConfig = defineDevtoolsConfig({
templates: {
admin: { label: "Admin", meta: { role: "admin" } },
viewer: { label: "Viewer", meta: { role: "viewer" } },
},
editableFields: [
{
key: "role",
label: "Role",
type: "select",
options: ["admin", "viewer"],
},
],
async createManagedUser(args) {
const user = await db.user.create({
data: {
email: args.email,
name: args.template.label,
role: String(args.template.meta?.role ?? "viewer"),
},
});
return {
userId: user.id,
email: user.email,
label: args.template.label,
};
},
async getSessionView(args) {
const user = await db.user.findUnique({ where: { id: args.userId } });
return {
userId: args.userId,
email: user?.email,
label: user?.name,
fields: {
sessionId: args.sessionId,
role: user?.role ?? "viewer",
},
editableFields: ["role"],
};
},
async patchSession(args) {
await db.user.update({
where: { id: args.userId },
data: { role: String(args.patch.role ?? "viewer") },
});
const user = await db.user.findUnique({ where: { id: args.userId } });
return {
userId: args.userId,
email: user?.email,
label: user?.name,
fields: {
sessionId: args.sessionId,
role: user?.role ?? "viewer",
},
editableFields: ["role"],
};
},
});
export const devtools = createDevtoolsIntegration(devtoolsConfig, {
position: "bottom-right",
triggerLabel: "Auth DevTools",
});createManagedUser must create a real Better Auth user in your app database and return that real user ID.
import { betterAuth } from "better-auth";
import { devtools } from "./devtools";
export const auth = betterAuth({
database,
plugins: [devtools.serverPlugin],
});The server plugin already declares its storage model through Better Auth's plugin schema, including the devtoolsUser table used for managed test-user records. You do not need to hand-write a DevtoolsUser model just to use this package.
After adding the plugin to your Better Auth config, rerun the Better Auth CLI so the plugin schema is picked up:
npx auth@latest migrateIf you use Prisma, Drizzle, or another ORM adapter, generate the schema from your Better Auth config and then apply it with your normal ORM workflow:
npx auth@latest generateRerun the CLI whenever you add the plugin or change plugin-managed schema.
import { createAuthClient } from "better-auth/react";
import { devtoolsClientPluginFor } from "better-auth-devtools/plugin";
import type { DevtoolsConfig } from "@/lib/devtools-types";
export const authClient = createAuthClient({
plugins: [devtoolsClientPluginFor<DevtoolsConfig>()],
});If you want strongly typed client actions, pass your devtools config type to devtoolsClientPluginFor<...>(). In Next.js App Router, use a type-only import or a shared type alias instead of importing a DB-backed runtime module into client code.
defineDevtoolsConfig(...) takes the server-side callbacks and template metadata that drive the plugin:
templates: map of stable test personas. Each template needs alabeland can optionally includeemailPatternandmeta.editableFields: optional list of fields the panel may edit. Supported field types arestring,number,boolean, andselect.createManagedUser: must create a real user in your app database and return its realuserId. You can also overrideemailandlabel.getSessionView: returns the session data the panel should display for the active user.patchSession: applies an allowed patch and returns the refreshed session view.
createDevtoolsIntegration(config, panel) bundles the server plugin, client plugin, and panel props. The optional panel config supports:
enabledbasePathdefaultOpenpositiontriggerLabel
Keep the devtools config on the server:
import {
createDevtoolsIntegration,
defineDevtoolsConfig,
} from "better-auth-devtools/plugin";
export const devtoolsConfig = defineDevtoolsConfig({
templates: {
admin: { label: "Admin", meta: { role: "admin" } },
viewer: { label: "Viewer", meta: { role: "viewer" } },
},
editableFields: [
{
key: "role",
label: "Role",
type: "select",
options: ["admin", "viewer"],
},
],
async createManagedUser(args) {
const user = await db.user.create({
data: {
email: args.email,
name: args.template.label,
role: String(args.template.meta?.role ?? "viewer"),
},
});
return {
userId: user.id,
email: user.email,
label: args.template.label,
};
},
async getSessionView(args) {
const user = await db.user.findUnique({ where: { id: args.userId } });
return {
userId: args.userId,
email: user?.email,
label: user?.name,
fields: {
sessionId: args.sessionId,
role: user?.role ?? "viewer",
},
editableFields: ["role"],
};
},
async patchSession(args) {
await db.user.update({
where: { id: args.userId },
data: { role: String(args.patch.role ?? "viewer") },
});
const user = await db.user.findUnique({ where: { id: args.userId } });
return {
userId: args.userId,
email: user?.email,
label: user?.name,
fields: {
sessionId: args.sessionId,
role: user?.role ?? "viewer",
},
editableFields: ["role"],
};
},
});
export const devtools = createDevtoolsIntegration(devtoolsConfig);Create a client-safe type bridge:
export type DevtoolsConfig = typeof import("./devtools").devtoolsConfig;Create your auth client with devtoolsClientPluginFor<...>():
import { createAuthClient } from "better-auth/react";
import { devtoolsClientPluginFor } from "better-auth-devtools/plugin";
import type { DevtoolsConfig } from "@/lib/devtools-types";
export const authClient = createAuthClient({
plugins: [devtoolsClientPluginFor<DevtoolsConfig>()],
});If you want strongly typed client actions, pass your devtools config type to devtoolsClientPluginFor<...>(). In Next.js App Router, use a type-only import or a shared type alias instead of importing a DB-backed runtime module into client code.
Pass panelProps from a server layout:
import { devtools } from "@/lib/devtools";
import { DevtoolsWrapper } from "./devtools-wrapper";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<DevtoolsWrapper panelProps={devtools.panelProps} />
</body>
</html>
);
}Render the panel in a client wrapper:
"use client";
import { BetterAuthDevtools } from "better-auth-devtools/react";
import type { BetterAuthDevtoolsProps } from "better-auth-devtools/react";
export function DevtoolsWrapper({
panelProps,
}: {
panelProps: BetterAuthDevtoolsProps;
}) {
return <BetterAuthDevtools {...panelProps} />;
}Do not import a DB-backed devtools config module directly into a client component.
import { betterAuth } from "better-auth";
import { createAuthClient } from "better-auth/react";
import { devtoolsClientPlugin, devtoolsPlugin } from "better-auth-devtools/plugin";
export const auth = betterAuth({
database,
plugins: [
devtoolsPlugin({
templates: {
admin: { label: "Admin", meta: { role: "admin" } },
viewer: { label: "Viewer", meta: { role: "viewer" } },
},
editableFields: [
{
key: "role",
label: "Role",
type: "select",
options: ["admin", "viewer"],
},
],
async createManagedUser(args) {
const user = await db.user.create({
data: {
email: args.email,
name: args.template.label,
role: String(args.template.meta?.role ?? "viewer"),
},
});
return {
userId: user.id,
email: user.email,
label: args.template.label,
};
},
async getSessionView(args) {
const user = await db.user.findUnique({ where: { id: args.userId } });
return {
userId: args.userId,
email: user?.email,
label: user?.name,
fields: {
sessionId: args.sessionId,
role: user?.role ?? "viewer",
},
editableFields: ["role"],
};
},
async patchSession(args) {
await db.user.update({
where: { id: args.userId },
data: { role: String(args.patch.role ?? "viewer") },
});
const user = await db.user.findUnique({ where: { id: args.userId } });
return {
userId: args.userId,
email: user?.email,
label: user?.name,
fields: {
sessionId: args.sessionId,
role: user?.role ?? "viewer",
},
editableFields: ["role"],
};
},
}),
],
});
export const authClient = createAuthClient({
plugins: [devtoolsClientPlugin()],
});"use client";
import { BetterAuthDevtools } from "better-auth-devtools/react";
export function Devtools() {
return (
<BetterAuthDevtools
enabled={true}
basePath="/api/auth"
templates={["admin", "viewer"]}
editableFields={[
{ key: "role", label: "Role", type: "select", options: ["admin", "viewer"] },
]}
/>
);
}Use the lower-level API if you need to wire the Better Auth server plugin separately from your panel props. In Next.js App Router, keep the plugin config on the server and pass panel props into a client wrapper.
Table name: devtoolsUser
| Field | Type | Key | Description |
|---|---|---|---|
id |
string |
PK | Better Auth generated row identifier for the managed test-user record |
userId |
string |
- | Real Better Auth user ID that the managed test user points to |
templateKey |
string |
- | Template key used when the managed test user was created |
label |
string |
- | Display label shown in the panel |
email |
string |
- | Email shown for the managed test user record |
createdAt |
date |
- | Creation timestamp for the record |
updatedAt |
date |
- | Last update timestamp for the record |
Cause:
DEV_AUTH_ENABLED=trueis missing- app is running in production
Fix:
- set
DEV_AUTH_ENABLED=true - verify you are not in production
Cause:
createManagedUserreturned an ID for a user that was never actually created
Fix:
- create a real host-app user and return that real ID
Cause:
- plugin schema has not been generated or applied for your adapter yet
Fix:
- rerun the Better Auth CLI so the plugin schema is generated or migrated for your setup
Cause:
- a DB-backed or server-only module is being imported into a client component
Fix:
- pass
panelPropsfrom a server layout into a client wrapper
pnpm install
pnpm --dir apps/demo-app db:init
pnpm devThe repo includes a local demo app in apps/demo-app, but the README examples above are the public integration pattern to follow.
- Managed test users only. This is not arbitrary user impersonation.
- Intended for local and trusted development environments.
- Current public API:
better-auth-devtools/pluginbetter-auth-devtools/react