{"slug": "theauth-for-ai-agents-identity-and-scoped-permissions", "title": "theAuth for AI Agents: Identity and Scoped Permissions", "summary": "A developer released theAuth, an open-source TypeScript library (@glinr/theauth) that gives each AI agent its own identity, token, and scoped permissions instead of sharing a human's API key. The guide walks through creating per-agent `kv_` tokens, calling `authorize()` before every action for allow/deny decisions with audit IDs, adding rate limits and argument constraints, delegating narrower scopes to sub-agents, and revoking one agent without affecting others.", "body_md": "theAuth is open-source auth for AI agents and humans. [Star the repo on GitHub](https://github.com/glincker/theauth) · [Read the docs](https://docs.theauth.dev?utm_source=devto&utm_medium=article&utm_campaign=guide-5-ai-agent-identity-scoped-permissions) · [Run the quickstart](https://docs.theauth.dev/quickstart?utm_source=devto&utm_medium=article&utm_campaign=guide-5-ai-agent-identity-scoped-permissions) · [theauth.dev](https://theauth.dev?utm_source=devto&utm_medium=article&utm_campaign=guide-5-ai-agent-identity-scoped-permissions)\n\nLast spring I found one API key in four places. It sat in a cron job, a Slack bot, a code-review agent, and a notebook a teammate had forgotten about. The key belonged to a human. It could read everything that human could read, and it could write most of it too.\n\nNothing was wrong yet. Then I asked a simple question: which of the four did that delete last Tuesday? I could not answer it. The logs said the human did it.\n\nThat is the problem this guide fixes. You will give each AI agent its own identity, its own token, and a short list of things it may do. By the end you will have a runnable script that creates agents, denies the wrong calls, logs every decision, and revokes one agent without touching the others.\n\nI use theAuth for this, an open-source TypeScript library (`@glinr/theauth`). I wrote parts of it, so weigh my opinions accordingly. I will say plainly where it does not fit.\n\nThis is guide 5 of 8 in the theAuth guides. It stands on its own, so you can start right here. Nothing comes before it. It is the base for guides 6 to 8.\n\n| Guide | Title | Read it when | \n|---|---|---|\n| 1 | [Add login to an existing Next.js app](https://dev.to/thegdsks/add-login-to-an-existing-nextjs-app-with-theauth-4kpk) | You have an app with no auth yet | \n| 2 | [Passwordless login: passkeys, links, OTP](https://dev.to/thegdsks/passwordless-login-in-typescript-passkeys-links-otp-59ja) | You want to drop passwords or add 2FA | \n| 3 | [Multi-tenant SaaS auth: orgs, RBAC, SSO, SCIM](https://dev.to/thegdsks/multi-tenant-saas-auth-orgs-rbac-sso-and-scim-1fod) | You sell to teams and companies | \n| 4 | [Migrate from Auth0 or Clerk](https://dev.to/thegdsks/migrate-from-auth0-or-clerk-to-theauth-step-by-step-4o47) | You already run another provider | \n| 5 | Give every AI agent its own identity (this guide) | You run AI agents and need to start somewhere | \n| 6 | [Cap agent spend and require human approval](https://dev.to/thegdsks/cap-agent-spend-and-require-human-approval-in-typescript-2k9h) | Your agents spend money or act on risky things | \n| 7 | [Secure an MCP server for production](https://dev.to/thegdsks/securing-an-mcp-server-for-production-step-by-step-53h7) | You expose tools over MCP | \n| 8 | [Build an audit trail for AI agent actions](https://dev.to/thegdsks/build-an-audit-trail-for-ai-agent-actions-23j7) | Someone will ask what your agents did | \n\nBuilding for people? Start at guide 1. Building for AI agents? Start at guide 5. Every guide links to the docs page for each concept it touches.\n\n| Step | What you do | Result | \n|---|---|---|\n| 1 | Install and create an instance | A local SQLite database with agents enabled | \n| 2 | Seed an owner | One human row that agents hang off | \n| 3 | Create one agent per job | A `kv_` token per agent, shown once | \n| 4 | Call `authorize()` before every action | An allow or deny with an audit ID | \n| 5 | Add constraints | Rate limits, argument patterns, time windows | \n| 6 | Check tokens in middleware | 401 and 403 at your HTTP edge | \n| 7 | Delegate to sub-agents | A narrower scope, with an expiry | \n| 8 | Read the audit trail, rotate, revoke | Answers to \"which agent did that\" | \n\nYou need Node 20 or newer, TypeScript, and a package manager. You do not need a running server or an external database. The examples use SQLite on disk so you can open the file afterward.\n\nYou also need one honest assumption: the agents in your system call tools you control. theAuth gives you the decision point. Your code must ask it before acting. If an agent can reach a database directly with its own credentials, no permission list in the world will stop it.\n\nA shared human key has three problems, and they stack.\n\nFirst, the blast radius equals the human's permissions. A code reviewer that only needs to read pull requests inherits write access to everything.\n\nSecond, you cannot revoke one agent. Rotate the key and all four processes break at once. You wait, and the exposed key stays live.\n\nThird, the audit trail lies. Every row names the human. Your compliance questions, and your own debugging, hit a wall.\n\nA per-agent identity fixes all three. Each agent holds a token that maps to one row, one owner, and one permission list. The [core concepts page](https://docs.theauth.dev/concepts) describes the loop in one sentence: a user creates agents, agents call `authorize()` before acting, and every decision lands in the audit trail.\n\nOne distinction matters early. An agent is not a user. It has no email, no password, no session, and no OAuth account. It has a bearer token and a permission set. If you catch yourself wiring password reset for an agent, you want a user. The [agent identity page](https://docs.theauth.dev/agents) draws that line.\n\n```\nmkdir agent-identity-demo && cd agent-identity-demo\nnpm init -y\nnpm install @glinr/theauth\nnpm install -D tsx typescript\n```\n\nNow create `demo.ts`. I will build it up one piece at a time, and the full file is the sum of the blocks below.\n\n``` js\nimport { createTheAuth } from '@glinr/theauth';\n\nconst theauth = await createTheAuth({\n  database: { provider: 'sqlite', url: 'theauth.db' },\n  agents: {\n    enabled: true,\n    maxPerUser: 10,\n    auditAll: true,\n    tokenExpiry: '24h',\n  },\n});\n```\n\nTwo settings deserve a note. `auditAll` records every `authorize()` call, and it defaults to on. `tokenExpiry` sets how long a new agent lives when you do not pass `expiresAt`. A 24 hour default means an agent you forget about stops working by tomorrow. I like that default. A forgotten credential should expire, not linger.\n\nThe quickstart walks through the same setup if you want the scaffolded Next.js version instead: [https://docs.theauth.dev/quickstart](https://docs.theauth.dev/quickstart).\n\nEvery agent has an owner, and the owner must exist as a row in `theauth_users`. That column is a foreign key. Skip this step and you will see `FOREIGN KEY constraint failed` on your first create call. Everyone hits it once.\n\nIf you already run theAuth's own auth modules, sign-up creates the row for you. In a scratch script, insert one:\n\n``` js\nimport { users } from '@glinr/theauth';\n\ntheauth.db.insert(users).values({\n  id: 'user-1',\n  email: 'owner@example.com',\n  name: 'Owner',\n  createdAt: new Date(),\n  updatedAt: new Date(),\n}).run();\n```\n\nThis insert pattern comes from the quickstart troubleshooting section and works for SQLite. For Postgres, the database docs list the matching call.\n\nHere is the rule I follow: one agent per job, not one agent per person. A nightly pull-request reviewer and a refund bot should never share an identity, even if the same human set up both.\n\n``` js\nconst reviewer = await theauth.agent.create({\n  ownerId: 'user-1',\n  name: 'pr-reviewer',\n  type: 'autonomous',\n  permissions: [\n    { resource: 'mcp:github:*', actions: ['read'] },\n  ],\n  expiresAt: new Date(Date.now() + 7 * 24 * 3_600_000),\n  metadata: { purpose: 'nightly PR review' },\n});\n\nconst refunder = await theauth.agent.create({\n  ownerId: 'user-1',\n  name: 'refund-bot',\n  type: 'autonomous',\n  permissions: [\n    { resource: 'billing:refunds', actions: ['read', 'write'] },\n  ],\n});\n\nconsole.log(reviewer.token); // kv_..., shown once\n```\n\nThe token starts with `kv_`, followed by 32 random bytes in base64url, 46 characters in total. theAuth stores only the SHA-256 hash. A full database dump cannot reveal a live token.\n\nThat has a cost. You see the plaintext exactly once, at creation. Put it in your secrets manager before the process exits, or rotate to get a new one. You cannot recover it later.\n\nThe `type` field takes three values.\n\nAn `autonomous` agent runs on its own, with no approval step unless a permission constraint demands one. Cron jobs and unattended assistants fit here. This is the default choice.\n\nA `delegated` agent receives its permissions from another agent through a delegation chain. Use it for short-lived workers spun up for one task.\n\nA `service` agent is a long-lived identity for infrastructure, such as an MCP server or an internal microservice. Treat it like a service account.\n\nBy default one user can own 10 active agents. The eleventh create call throws a plain `Error` with the message `User <id> has reached the maximum of <n> active agents.` The error has no dedicated code, and the REST endpoint reports it as a 500. Raise `maxPerUser` at initialization if you run a fleet, and catch the error by message, not by code.\n\nAn agent identity does nothing until your code asks the question. Put one call in front of every sensitive operation.\n\n``` js\nconst allowed = await theauth.authorize(reviewer.id, {\n  action: 'read',\n  resource: 'mcp:github:repos',\n});\nconsole.log(allowed.allowed); // true\n\nconst denied = await theauth.authorize(reviewer.id, {\n  action: 'write',\n  resource: 'mcp:github:repos',\n});\nconsole.log(denied.allowed, denied.reason); // false, plus a reason\nconsole.log(denied.auditId); // links to the audit row\n```\n\nThe result is `{ allowed, reason?, auditId }`. The reviewer holds only `read`. A `write` on the same resource fails, and the denial lands in the audit log with its reason.\n\nResources are colon-separated strings. You pick the convention. `mcp:github:repos`, `billing:refunds`, and `db:users:write` all work. Consistency matters more than syntax.\n\nThe wildcard has one behavior that surprises people. A `*` segment is not a single-segment wildcard. The matcher stops at the first `*` and accepts everything after it, including nothing. That means `mcp:github:*` matches `mcp:github:repos`, `mcp:github:repos:comments`, and `mcp:github` itself.\n\nPut `*` only in the last position. A pattern like `mcp:*:repos` looks like \"any server, repos only,\" but it accepts `mcp:slack:channels` too. Without a wildcard, the pattern and resource must have the same number of segments. The full table lives on the [permissions page](https://docs.theauth.dev/permissions).\n\nThe action list has no fixed set. `read`, `write`, `execute`, and `delete` are common, but you can define `comment` or `refund` if that reads better in your logs. theAuth checks that the requested action appears in the permission's `actions` array. It does not interpret the word.\n\nA permission can carry constraints, and every one must pass. This is where least privilege gets specific. \"May write files\" becomes \"may write files under `/tmp/agent/`, 20 times an hour, from the office network.\"\n\n``` js\nconst filer = await theauth.agent.create({\n  ownerId: 'user-1',\n  name: 'file-writer',\n  type: 'autonomous',\n  permissions: [\n    {\n      resource: 'tool:file_write',\n      actions: ['execute'],\n      constraints: {\n        maxCallsPerHour: 20,\n        allowedArgPatterns: ['^/(home/agent|tmp)/'],\n        timeWindow: { start: '09:00', end: '17:00' },\n        ipAllowlist: ['10.0.0.0/8'],\n      },\n    },\n  ],\n});\n\nconst outside = await theauth.authorize(filer.id, {\n  action: 'execute',\n  resource: 'tool:file_write',\n  arguments: { path: '/etc/passwd' },\n  ip: '10.1.2.3',\n});\nconsole.log(outside.allowed); // false, the path fails the pattern\n```\n\nEach constraint has edges you should know before you rely on it.\n\n`maxCallsPerHour` counts calls per agent and resource over a rolling hour, in 5-minute buckets. Calls beyond the limit get a reason that starts with `Rate limit exceeded`. The audit log records them as `denied`, not `rate_limited`, even though the type allows both values.\n\n`allowedArgPatterns` takes regular expression strings. Every string value in `arguments` must match every pattern. If you list two alternatives as two patterns, nothing will ever pass. Write one pattern with an alternation instead. The check skips non-string values, and it skips the whole request when `arguments` is missing.\n\nThat last detail matters. If your tool wrapper forgets to pass `arguments`, the constraint silently does nothing. Pass them every time.\n\nThe `timeWindow` compares `HH:MM` strings against the server's local clock, not UTC. Windows that wrap past midnight, such as 22:00 to 06:00, are not supported. If you need a night shift, you will need to model it differently or enforce it outside theAuth.\n\n`ipAllowlist` takes exact IPv4 addresses and IPv4 CIDR ranges. IPv6 is not supported. You must pass `ip` on the authorization request. A request with no `ip` fails, which is the safe default.\n\nSet `requireApproval: true` and `authorize()` always denies with the reason `This action requires human approval before execution`.\n\n``` js\nconst deployer = await theauth.agent.create({\n  ownerId: 'user-1',\n  name: 'deploy-bot',\n  type: 'autonomous',\n  permissions: [\n    { resource: 'mcp:deploy:staging', actions: ['execute'] },\n    {\n      resource: 'mcp:deploy:production',\n      actions: ['execute'],\n      constraints: { requireApproval: true },\n    },\n  ],\n});\n```\n\nRead the approval docs carefully, because this one trips people up. theAuth does not ship an approval UI, and `authorize()` never looks up approvals. After a human approves, `authorize()` still denies. Your application performs the action itself once `theauth.approval.get(id)` says `approved`. The enforcement point is theAuth. The workflow is yours. The [approval flows page](https://docs.theauth.dev/approval) has the request, approve, and cleanup calls.\n\nWriting permission arrays by hand gets old. theAuth ships named templates as plain `Permission[]` arrays.\n\n``` js\nimport { permissionTemplates, getPermissionTemplate } from '@glinr/theauth';\n\nconst reader = await theauth.agent.create({\n  ownerId: 'user-1',\n  name: 'docs-reader',\n  type: 'autonomous',\n  permissions: [\n    ...permissionTemplates.mcpBasic,\n    { resource: 'tool:custom_tool', actions: ['execute'] },\n  ],\n});\n\nconst copy = getPermissionTemplate('mcpBasic'); // deep copy, safe to edit\n```\n\nThe names are `readonly`, `readwrite`, `admin`, `mcpBasic`, `mcpFull`, `rateLimitedRead`, `approvalRequired`, and `businessHours`. Do not reach for `admin` out of habit. It grants every action on every resource, which is the shared human key all over again.\n\nOne gotcha. Spreading from `permissionTemplates` copies references to the shared objects. If you mutate an entry afterward, you change the template for everyone. Use `getPermissionTemplate` whenever you plan to edit.\n\nUntil now, the agent's ID went straight into `authorize()`. In a real system the agent calls your API with a bearer token. Use `authorizeByToken` in middleware.\n\n``` js\nexport async function handle(request: Request): Promise<Response> {\n  const token = request.headers.get('Authorization')?.replace('Bearer ', '');\n  if (!token) return new Response('Unauthorized', { status: 401 });\n\n  const result = await theauth.authorizeByToken(token, {\n    action: 'read',\n    resource: 'mcp:github:repos',\n  });\n\n  if (!result.allowed) {\n    return new Response(result.reason ?? 'Forbidden', { status: 403 });\n  }\n\n  return Response.json({ ok: true, auditId: result.auditId });\n}\n```\n\ntheAuth hashes the token, looks it up, evaluates the permissions, and logs the decision. No JWTs, and no network round trip to a separate auth service. The call hits your own database.\n\nTwo behaviors differ between the two entry points, and both bite in production.\n\nThe first is expiry. `authorizeByToken` rejects an expired agent and flips its status to `expired`. `authorize(agentId)` only checks `status`, so it does not notice an elapsed `expiresAt` until something marks the agent expired. If you call by ID in a background worker, add your own expiry check or route through the token path.\n\nThe second is delegation. `authorizeByToken` checks the agent's own permissions only. `authorize(agentId, ...)` applies permissions held through delegation. If your sub-agents rely on delegated scopes, call it by ID.\n\nThe quickstart also shows a Hono setup on Cloudflare Workers if you want a framework example.\n\nOrchestrators spawn workers. The worker should get less than the parent, never more.\n\n``` js\nconst worker = await theauth.agent.create({\n  ownerId: 'user-1',\n  name: 'issue-summarizer',\n  type: 'delegated',\n  permissions: [], // starts empty\n});\n\nawait theauth.delegate({\n  fromAgent: reviewer.id,\n  toAgent: worker.id,\n  permissions: [{ resource: 'mcp:github:issues', actions: ['read'] }],\n  expiresAt: new Date(Date.now() + 3_600_000), // one hour\n  maxDepth: 2,\n});\n\nconst held = await theauth.delegation.getEffectivePermissions(worker.id);\nconsole.log(held);\n```\n\nAn agent cannot delegate permissions it does not hold. The attempt fails at delegation time, not at the next `authorize()`. Each link carries an expiry and a depth counter. Revoking a link cascades to every delegation made onward from it.\n\nThe [delegation docs](https://docs.theauth.dev/delegation) cover depth limits and revocation order. For one-off jobs that should vanish on their own, look at [ephemeral sessions](https://docs.theauth.dev/ephemeral-sessions).\n\nNow the payoff. Remember the Tuesday delete I could not trace? Here is the query that answers it.\n\n``` js\nconst denials = await theauth.audit.query({\n  agentId: filer.id,\n  result: 'denied',\n  limit: 50,\n});\n\nfor (const row of denials) {\n  console.log(row.timestamp, row.action, row.resource, row.reason);\n}\n\nconst csv = await theauth.audit.export({ format: 'csv' });\n```\n\nEach row holds the agent ID, the owner's user ID, the action, the resource, the arguments, the result, the reason, and the evaluation time in milliseconds. Filter by `since`, `until`, `actions`, `userId`, or `result`.\n\nSome limits to plan around. Calls rejected before the permission engine runs, such as an unknown or revoked agent, are not logged. Exports cap at the 10,000 most recent entries, so page through `audit.query` for more. The `actions` filter applies after `limit` and `offset`, so a page can come back shorter than you asked for.\n\nThe SDK never edits entries, but tamper evidence is your database's job. The [audit trail page](https://docs.theauth.dev/audit) is honest about this, and so should you be if an auditor asks. theAuth does not make you compliant by itself.\n\nTwo operations finish the lifecycle.\n\n``` js\n// New token, old one invalid immediately, no overlap window\nconst rotated = await theauth.agent.rotate(reviewer.id);\nconsole.log(rotated.token);\n\n// Permanent. You cannot undo it.\nawait theauth.agent.revoke(refunder.id);\n```\n\nRotation is a single update, so no moment exists where both tokens work. Rotate on a schedule and whenever you suspect exposure. Only active agents can rotate.\n\nRevocation is permanent. To restore access, create a new agent. I think that is the right call. A revoked credential that can come back is a credential an attacker can wait out.\n\nPermission edits through `theauth.agent.update` take effect immediately for new requests. In-flight requests that already passed authorization are not affected.\n\nRun the whole file and watch the cap, the denials, and the audit rows:\n\n```\nnpx tsx demo.ts\n```\n\n`authorize()` is a simple path. It stops at the first permission that matches the resource and action, then falls back to delegated permissions. It does not expand roles and does not walk relationship graphs.\n\nWhen you need those, call `theauth.policy.evaluate()` directly.\n\n``` js\nconst decision = await theauth.policy.evaluate({\n  subject: { agentId: reviewer.id },\n  action: 'read',\n  resource: 'mcp:github:repos',\n  context: { ip: '203.0.113.42' },\n});\n\nconsole.log(decision.allowed, decision.effect, decision.reason);\nconsole.log(decision.cacheHit, decision.durationMs);\n```\n\nThe engine considers every matching permission and combines the results with deny-overrides by default. One failing constraint wins over any number of permits. If nothing matches, the effect is `indeterminate` with the reason `POLICY_NO_MATCHING_PERMISSION`, and `allowed` is false. Treat `indeterminate` as a deny.\n\nThis difference has teeth. Imagine an agent with two permissions that both match `mcp:deploy:prod`: a broad one, and a narrow one with a time window. `authorize()` evaluates the first match. `evaluate()` evaluates both and denies outside the window. The two paths can disagree, and the docs say so directly.\n\nFive limits apply to the engine today, and I would rather you hear them from me.\n\nThe decision cache is process-local, with 10,000 entries and a 60 second TTL by default. Nothing calls `invalidate()` for you when permissions change, so call `theauth.policy.invalidate({ agentId })` after a mutation you need to take effect now. In a multi-instance deployment, other processes keep stale entries until the TTL runs out.\n\nConstraints on delegated permissions are not applied inside `evaluate()`. Only `resource` and `actions` carry over. If a delegated scope relies on a time window, enforce it another way.\n\nUser-only subjects write no audit row, because `audit_logs.agent_id` is not nullable.\n\nThe engine also has no declarative policy language. Permissions are plain objects. If your security team demands Cedar or Rego, this is not the right fit today. The [policy engine page](https://docs.theauth.dev/policy-engine) lists every known gap, and the [ReBAC page](https://docs.theauth.dev/rebac) covers relationship checks.\n\nTwo features sit on top of identity. You can skip both.\n\nTrust scoring computes a 0 to 100 value from the agent's audit log. A new agent starts at 50. Successful calls add up to 25 points, each denial costs 5, and age adds up to 15. The level maps to `untrusted`, `limited`, `standard`, `trusted`, or `elevated`.\n\n``` js\nconst score = await theauth.trust.computeScore(reviewer.id);\nconsole.log(score.score, score.level);\n```\n\nScores compute on demand. No background scorer runs, and the core hardcodes the thresholds today. A brand-new agent sits at `limited`, which surprises people. Use the level to route sensitive actions to a human, and treat the number as a hint, not a verdict. Details are on the [trust scoring page](https://docs.theauth.dev/trust).\n\nBearer tokens work fine inside one deployment. When an agent must prove its identity across an organizational boundary, you can give it a W3C DID backed by an Ed25519 key.\n\n```\nconst { agentDid, privateKeyJwk } = await theauth.did.generateKey(reviewer.id);\nconsole.log(agentDid.did); // did:key:z6Mk...\n\nconst signed = await theauth.did.sign(\n  reviewer.id,\n  { action: 'review', repo: 'acme/api' },\n  privateKeyJwk,\n);\n```\n\nKnow the scope before you build on it. `theauth.did.verify()` looks up the public key in the theAuth database, so it only verifies DIDs created by the same instance. A third party holding only a DID string cannot use it. The private key is also returned once and never stored. Treat it like a bearer token. The [DID identity page](https://docs.theauth.dev/did) spells out the lower-level `verifyPayload` route for outside verifiers.\n\nYou skipped Step 2. The owner must exist in `theauth_users` first.\n\nCheck `expiresAt`. The default expiry is 24 hours from creation. `authorizeByToken` flips the status to `expired`, and an expired agent stays expired. Create a new agent or set a longer `expiresAt` up front.\n\n`authorize()` evaluates only the first permission whose resource and action match. If its constraints fail, it never tries later matches. Two overlapping permissions on one resource are a trap, so merge them into one.\n\nYour call omitted `arguments`. The check skips requests with no arguments. Also confirm the value is a string, because the check skips other types.\n\nThe window uses the server's local time. A container running in UTC will interpret `09:00` as 09:00 UTC. Pin the timezone or compute against it deliberately.\n\nExpected. `authorize()` does not consult approvals. Run the action from your own code after the approval status flips.\n\nLook at the cache if you use `evaluate()`. The cache can serve decisions for up to the TTL, 60 seconds by default, and nothing invalidates them for you. Call `invalidate` right after the change.\n\nThe pages below cover what this guide skipped.\n\nYou can, and it beats sharing. But a provider key gives you one scope level, set by the provider. It does not give you per-resource patterns, rate limits per tool, or one audit trail across vendors. Per-agent keys also do not know about your internal tools.\n\nNo, but it needs an owner. Agents have no password, session, or OAuth flow. Each one belongs to a user row, and that user must exist before you create the agent.\n\nRotate it. The old token dies immediately, with no overlap. Because theAuth keeps only the SHA-256 hash, a database leak does not expose live tokens. A leaked plaintext token, from a log line for example, is still usable until you rotate or revoke.\n\nNo. Humans signing in still use sessions or OAuth. Agent tokens cover non-interactive callers. For MCP servers that real users connect to, theAuth also ships an OAuth 2.1 server, which is a separate path from agent tokens.\n\nIf you run one agent, with one scope, in one process, a scoped provider key and a log line may be enough. This library earns its place once you run 5 or more agents, 2 or more owners, or an auditor asking questions.\n\nYes. The quickstart shows a D1 binding for Cloudflare Workers, and the database docs list the other providers. Use SQLite for local work and a server database in production.\n\nWhich of your agents holds the widest key right now, and what would break if you cut its permissions to one resource? I would like to hear the answer, especially if the answer is \"I do not know.\"\n\nThe fastest way in is the [quickstart](https://docs.theauth.dev/quickstart?utm_source=devto&utm_medium=article&utm_campaign=guide-5-ai-agent-identity-scoped-permissions). If this guide saved you time, a [star on GitHub](https://github.com/glincker/theauth) helps other developers find the project, and the [docs](https://docs.theauth.dev?utm_source=devto&utm_medium=article&utm_campaign=guide-5-ai-agent-identity-scoped-permissions) cover every option used above. More about the project lives at [theauth.dev](https://theauth.dev?utm_source=devto&utm_medium=article&utm_campaign=guide-5-ai-agent-identity-scoped-permissions).\n\nNext: [guide 6, spend caps and human approval](https://dev.to/thegdsks/cap-agent-spend-and-require-human-approval-in-typescript-2k9h). The full list sits in the table at the top of this page.\n\n**GDS K S** · [thegdsks.com](https://thegdsks.com) · building [Glincker](https://glincker.com) · follow on X [@thegdsks](https://x.com/thegdsks)\n\n*Every agent that can act should be able to answer one question: who exactly was that?*", "url": "https://wpnews.pro/news/theauth-for-ai-agents-identity-and-scoped-permissions", "canonical_source": "https://dev.to/thegdsks/theauth-for-ai-agents-identity-and-scoped-permissions-3mgn", "published_at": "2026-10-10 21:42:59+00:00", "updated_at": "2026-10-10 21:45:59.531575+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "agent-protocols", "ai-safety"], "entities": ["theAuth", "@glinr/theauth", "GitHub", "TypeScript", "SQLite", "Node"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/theauth-for-ai-agents-identity-and-scoped-permissions", "markdown": "https://wpnews.pro/news/theauth-for-ai-agents-identity-and-scoped-permissions.md", "text": "https://wpnews.pro/news/theauth-for-ai-agents-identity-and-scoped-permissions.txt", "jsonld": "https://wpnews.pro/news/theauth-for-ai-agents-identity-and-scoped-permissions.jsonld"}}