> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theauth.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from the better-auth agent plugin

> Switch from @better-auth/agent-auth to theAuth AgentIdentity. Maps scopes to Permission objects and delegation chains, with cascading revocation support.

`@better-auth/agent-auth` is better-auth's attempt to add AI-agent support alongside a human-first auth library. It is marked "heavy development, not yet stable" in their repo as of 2026-03. If you started there to get agent identity with the broader better-auth feature set, moving to theAuth gives you a single codebase where agents are a first-class entity instead of a separate package bolted onto the user model.

Both libraries share the same ancestor on the human-auth side. Most of the familiar hooks keep their names. The agent story is the part that changes materially.

If you use the human-auth half of better-auth and not the agent plugin yet, read the [general better-auth migration guide](/migrate/from-better-auth) first, this page only covers the agent-plugin deltas.

## Where the model diverges

better-auth treats an agent as an OAuth client attached to a user. A single-hop delegation is possible. There is no delegation chain tracking, no trust score, no cost attribution, no ephemeral session type, and no MCP OAuth 2.1 authorization server built in.

theAuth treats an agent as a primary database entity with its own lifecycle:

* `AgentIdentity` row in the schema, owned by a user, with status transitions (`active`, `revoked`, `expired`).
* Multi-hop delegation chains with a per-call depth limit and cascading revocation of chains.
* Trust scoring, 5 levels, computed from the audit log.
* Cost attribution per agent, tool, and chain.
* An MCP OAuth 2.1 authorization server (`createMcpModule`) that sits alongside the same instance and adapter.
* Ephemeral agent sessions for one-off tasks (`createEphemeralSessionModule`).

## Concepts map

| `@better-auth/agent-auth` | theAuth |
| - | - |
| `agentPlugin()` added to `betterAuth` | Built into `createTheAuth`. No plugin needed for the core agent surface. |
| `auth.api.createAgent({ userId, scopes })` | `theauth.agent.create({ ownerId, name, type, permissions })` |
| `agent.accessToken` | `agent.token` (`kv_` followed by base64url of 32 random bytes, returned once, SHA-256 hashed at rest) |
| `agent.scopes: string[]` | `agent.permissions: Permission[]` (resource patterns + actions + constraints) |
| `agent.kind: 'service' \| 'user-agent'` | `agent.type: 'autonomous' \| 'delegated' \| 'service'` |
| `auth.api.verifyAgentToken(token)` | `theauth.agent.validateToken(token)` (returns the agent or `null`) or `theauth.authorizeByToken(token, req)` |
| Single-hop delegation via second `createAgent` call | `theauth.delegate({ fromAgent, toAgent, permissions, expiresAt, maxDepth })` |
| No cascading revocation | `theauth.delegation.revoke(chainId)` revokes that link and every downstream link. Immediate. |
| No audit module (relies on generic logs) | `theauth.audit.query()` and `theauth.audit.export()` |
| No trust score | `theauth.trust.computeScore(agentId)` |
| No MCP server | OAuth 2.1 authorization server from `createMcpModule` (`@glinr/theauth/mcp`), PKCE S256, RFC 9728 / 8707 / 8414 / 7591 |
| No per-agent rate cap | Permission constraint: `maxCallsPerHour` |
| No approval gate | Permission constraint `requireApproval: true` denies the call with a "requires human approval" reason. Create and resolve requests with `theauth.approval`. |
| No ephemeral sessions | `createEphemeralSessionModule({ db: theauth.db }).createSession({ ownerId, permissions, ttlSeconds })`, see [Ephemeral sessions](/ephemeral-sessions) |

## Server setup

```ts theme={"dark"}
// BEFORE: lib/auth.ts (better-auth + agent plugin)
import { betterAuth } from 'better-auth';
import { agent } from '@better-auth/agent-auth';

export const auth = betterAuth({
  database: { provider: 'postgresql', url: process.env.DATABASE_URL },
  emailAndPassword: { enabled: true },
  plugins: [agent()],
});
```

```ts theme={"dark"}
// AFTER: lib/theauth.ts
import { createTheAuth } from '@glinr/theauth';

export const theauth = await createTheAuth({
  database: { provider: 'postgres', url: process.env.DATABASE_URL! },
  secret: process.env.THEAUTH_SECRET!,
  baseUrl: process.env.AUTH_BASE_URL!,
  auth: { session: { secret: process.env.SESSION_SECRET! } },
  username: { password: { minLength: 8 } }, // TheAuth's password auth is username-based, see /auth/username
  // No agent plugin, it is core.
});
```

theAuth has no `emailAndPassword` config key, its built-in password module authenticates by username (see [Username and password](/auth/username)). The rest of the human-auth config (`organization`, `twoFactor`, etc.) keeps a similar plugin shape, imported from `@glinr/theauth/auth` rather than `@glinr/theauth/plugins`. Check each feature's page, the option names are not identical.

## Creating an agent

```ts theme={"dark"}
// BEFORE (better-auth + agent plugin)
const agent = await auth.api.createAgent({
  userId: user.id,
  name: 'github-reader',
  kind: 'service',
  scopes: ['github:read'],
});

// agent.accessToken is the bearer. Store it.
```

```ts theme={"dark"}
// AFTER (TheAuth)
const agent = await theauth.agent.create({
  ownerId: user.id,
  name: 'github-reader',
  type: 'service',
  permissions: [
    {
      resource: 'mcp:github:*',
      actions: ['read'],
      constraints: { maxCallsPerHour: 500 },
    },
  ],
});

// agent.token (kv_ plus base64url of 32 random bytes) is the bearer. Returned once,
// only its SHA-256 hash is stored. Rotate with theauth.agent.rotate(agent.id) to cut
// a new one; the old token stops validating as soon as the new hash is written.
```

The shape of the call is close. The substantive difference is the permission model. A `scopes: ['github:read']` list maps to `{ resource: 'mcp:github:*', actions: ['read'] }`. The extra structure pays for itself when you need to add rate caps, argument allowlists, or time windows. In a resource pattern, `*` matches the rest of the path from that position on, so `mcp:github:*` also covers `mcp:github:repos:list`. `ownerId` must be the id of an existing user (`theauth_users`).

## Authorizing a tool call

```ts theme={"dark"}
// BEFORE (better-auth + agent plugin)
const result = await auth.api.verifyAgentToken(token);
if (!result.valid) return new Response('Unauthorized', { status: 401 });

if (!result.scopes.includes('github:read')) {
  return new Response('Forbidden', { status: 403 });
}
```

```ts theme={"dark"}
// AFTER (TheAuth)
const result = await theauth.authorizeByToken(token, {
  action: 'read',
  resource: 'mcp:github:repos',
});

if (!result.allowed) {
  return new Response(result.reason ?? 'Forbidden', { status: 403 });
}

// Every call is audited. result.auditId points at the row.
```

One call. One audit row. One place to reason about rate caps and constraints.

## Delegation

```ts theme={"dark"}
// BEFORE: single-hop only. You create a new agent whose scopes are a subset of the parent.
const child = await auth.api.createAgent({
  userId: user.id,
  name: 'child-agent',
  kind: 'service',
  scopes: ['github:read'], // must be a subset of parent
});
```

```ts theme={"dark"}
// AFTER: explicit chain with depth and expiry
const chain = await theauth.delegate({
  fromAgent: parent.id,
  toAgent: child.id,
  permissions: [{ resource: 'mcp:github:*', actions: ['read'] }],
  maxDepth: 2,
  expiresAt: new Date(Date.now() + 60 * 60 * 1000),
});

// Revoke the link, and every link downstream of `child`, in one call:
await theauth.delegation.revoke(chain.id);
```

The delegated permissions must be a subset of the parent agent's own permissions, otherwise `delegate` throws. `expiresAt` is required.

Revocation works on chains, not on the parent agent. `theauth.agent.revoke(parent.id)` marks that agent revoked so its own calls are denied, but it does not touch the chains it already issued. Call `theauth.delegation.revoke(chain.id)` to cut the child off, which also revokes the chains the child delegated onward. Use `theauth.delegation.listChains(agentId)` to find them.

`maxDepth` is checked on each call to `theauth.delegate`: the new link's depth must be less than or equal to the `maxDepth` you pass on that call (default 3). Depth is one more than the deepest active chain that ends at `fromAgent`. So `maxDepth: 2` stops a third hop only if you pass it when creating that third link. Nothing stores a root-level cap that later calls inherit, so pass the same value from your delegation helper every time.

## Trust scoring

`@better-auth/agent-auth` does not ship scoring. theAuth computes a 0-100 score from the audit history, mapped to five named levels. Useful as a gate for sensitive actions that should only run for agents with a clean track record.

```ts theme={"dark"}
const score = await theauth.trust.computeScore(agent.id);

if (score.level === 'untrusted' || score.level === 'limited') {
  // Route through the approval flow instead of auto-execute.
  await theauth.approval.request({
    agentId: agent.id,
    userId: agent.ownerId,
    action: 'delete',
    resource: 'file:prod-data/*',
  });
  return { queued: true };
}
```

## MCP server

If you were running a separate MCP OAuth server alongside better-auth (because the `@better-auth/mcp` plugin is a thin OIDC wrapper without agent semantics), you can retire it. theAuth's OAuth 2.1 server is a module you create with `createMcpModule` from `@glinr/theauth/mcp` and pass to your framework adapter. It does not take the `theauth` instance, and it has no built-in database: you supply the storage callbacks. [MCP](/mcp) has a complete in-memory example of those callbacks.

```ts theme={"dark"}
import { createTheAuth } from '@glinr/theauth';
import { createMcpModule } from '@glinr/theauth/mcp';
import { theAuthNextjs } from '@glinr/theauth-nextjs';
import { mcpStore } from './mcp-store'; // storeClient, findClient, storeAuthorizationCode, ...

const theauth = await createTheAuth({
  database: { provider: 'postgres', url: process.env.DATABASE_URL! },
  mcp: { enabled: true }, // creates the MCP server registry table only
});

const mcp = createMcpModule({
  config: {
    enabled: true,
    issuer: 'https://auth.yourapp.com',
    // Public origin plus the adapter mount path
    baseUrl: 'https://auth.yourapp.com/api/theauth',
    signingSecret: process.env.MCP_SIGNING_SECRET!, // at least 32 characters
  },
  ...mcpStore,
});

export const { GET, POST, PATCH, DELETE, OPTIONS } = theAuthNextjs(theauth, { mcp });
```

`mcpStore` supplies `storeClient`, `findClient`, `storeAuthorizationCode`, `consumeAuthorizationCode`, `storeToken`, `findTokenByRefreshToken`, `revokeToken`, and `resolveUserId`. The adapter serves `/mcp/register`, `/mcp/authorize`, and `/mcp/token`, plus the two `.well-known` documents, all relative to its mount path (`/api/theauth` by default). MCP clients look for the `.well-known` documents at the root of your origin, so add root-level routes or rewrites when you mount under a prefix, see [MCP](/mcp).

## Data migration

Agent rows in better-auth live in whatever table the plugin writes to (check the plugin's schema migration). theAuth stores agents in `theauth_agents` and their permissions in `theauth_permissions`, and it keeps only the SHA-256 hash of the full `kv_...` token. A better-auth token hash cannot be reused: the bearer would have to be a `kv_` token whose SHA-256 matches, which no old token is. So do not copy token hashes with SQL. Recreate each agent through the API and hand the owner a new token:

```ts theme={"dark"}
// scripts/import-agents.ts
import { theauth } from '../lib/theauth.js';

interface LegacyAgent {
  userId: string;
  name: string;
  kind: 'service' | 'user-agent';
  scopes: string[]; // e.g. ['github:read']
}

export async function importAgent(legacy: LegacyAgent) {
  const agent = await theauth.agent.create({
    ownerId: legacy.userId, // must already exist in theauth_users
    name: legacy.name,
    type: legacy.kind === 'service' ? 'service' : 'autonomous',
    permissions: legacy.scopes.map((scope) => {
      // 'github:read' becomes resource 'mcp:github:*' with action 'read'
      const idx = scope.lastIndexOf(':');
      const resource = scope.slice(0, idx);
      const action = scope.slice(idx + 1);
      return { resource: `mcp:${resource}:*`, actions: [action] };
    }),
    metadata: { importedFrom: 'better-auth-agent-plugin' },
  });

  // agent.token is shown once. Deliver it to the service that used the old accessToken.
  return agent;
}
```

The scope mapping above is only a starting point. Decide per scope what resource pattern and actions it should map to, and test the result in staging, the mapping is the load-bearing piece.

## Cutover plan

1. Stand up theAuth next to better-auth against the same database (or a shadow copy). Do not delete the better-auth schema yet.
2. Import agents with `theauth.agent.create`. Validate one new service token with `theauth.authorizeByToken(token, {...})`.
3. Give a single non-critical service its new token and switch it to `theauth.authorizeByToken` instead of `auth.api.verifyAgentToken`.
4. Watch the audit log for `allowed: false` entries. Fix permission mappings.
5. Expand the theAuth-guarded agent surface one service at a time.
6. Remove the `@better-auth/agent-auth` plugin from `lib/auth.ts` once no service still calls it.

The human-auth side can move in the same PR or stay on better-auth during the transition, see the [general better-auth migration guide](/migrate/from-better-auth).

## Rollback

Keep the better-auth agent plugin wired for at least one rotation cycle after the cut-over. If you need to revert:

1. Re-enable the `agent()` plugin in `lib/auth.ts`.
2. Point services back at `auth.api.verifyAgentToken`.
3. theAuth agent rows remain, they just stop being read. Old better-auth bearers keep working on better-auth because you never revoked them there, new `kv_` tokens only work against theAuth.

## Runnable example

[`examples/migrate-from-better-auth-agent-plugin`](https://github.com/glincker/theauth/tree/main/examples/migrate-from-better-auth-agent-plugin) runs the AFTER patterns end-to-end: AgentIdentity creation, multi-hop delegation with `maxDepth`, authorize-falls-back-to-chain semantics, and cascading chain revocation. The script uses the monorepo `workspace:*` version so it tracks the released package, and a vitest smoke suite runs it in CI.

```bash theme={"dark"}
pnpm --filter @glinr/theauth-example-migrate-from-better-auth-agent-plugin start
pnpm --filter @glinr/theauth-example-migrate-from-better-auth-agent-plugin test
```

## Next steps

<CardGroup cols={2}>
  <Card title="Agent identity" icon="robot" href="/agents">
    The primary entity model and lifecycle.
  </Card>

  <Card title="Delegation chains" icon="link" href="/delegation">
    Multi-hop delegation with depth and cascading revocation.
  </Card>

  <Card title="Trust scoring" icon="user-shield" href="/trust">
    Graduated autonomy by audit history.
  </Card>

  <Card title="MCP OAuth 2.1" icon="globe" href="/mcp">
    The authorization server the better-auth plugin does not ship.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.