# Troubleshooting [#troubleshooting] Access fails closed and early. Most problems show up as an `AccessError` when the instance is built, with a message that says what to change. Look the message up here, or find its code on [Error codes](/access/docs/error-codes). ## Startup [#startup] ### The wrong Better Auth version [#the-wrong-better-auth-version] ```text Aventara Access: createAccess runs on better-auth ~1.7.12, which is not installed: npm install better-auth@~1.7.12 Aventara Access: createAccess runs on better-auth ~1.7.12 (1.7.12 or a later 1.7 patch), and this app has better-auth 1.8.0: npm install better-auth@~1.7.12 ``` Code `ACCESS_BETTER_AUTH_VERSION`. Install Better Auth in the range: `npm install --save-exact better-auth@~1.7.12`. Under `mcp` or `audience` the OAuth provider and `cimd()` must be in the same range: `npm install @better-auth/oauth-provider@~1.7.12 @better-auth/cimd@~1.7.12`. A prerelease is refused too. ### Node.js cannot load Better Auth [#nodejs-cannot-load-better-auth] ```text Aventara Access: createAccess loads better-auth through Node.js's module loader, which this runtime does not provide; run it on Node.js ^22.23.4 || >=24.22.1. ``` Run on a supported Node.js ([Installation](/access/docs/installation#requirements)). ### No base URL [#no-base-url] ```text Aventara Access: Access issues its tokens with a fixed issuer and audience, the app's base URL, and Better Auth has none: set ... (or `BETTER_AUTH_URL`), or give a dynamic `baseURL` its `fallback`. ``` Code `ACCESS_OPTIONS_INVALID`. Set `baseURL` in the `betterAuth` block or `BETTER_AUTH_URL` in the environment ([The base URL](/access/docs/create-access#the-base-url)). Commands that load `src/access.ts` (`npx auth@~1.7.12 generate --yes`, and on Next.js the build) need the variable too, so set it in the environment they run in. ### Argon2 on Node.js 22 [#argon2-on-nodejs-22] ```text Aventara Access: argon2id needs Node.js's `crypto.argon2` (Node.js 24.7.0 and later), which this runtime does not have: on Node.js 22, install @node-rs/argon2 (npm install --save-exact @node-rs/argon2@2.2.2); or set `...password` to hash passwords yourself. ``` Install `@node-rs/argon2@2.2.2` exactly. The kiosk's PIN hashing says the same ([Passwords](/access/docs/passwords#nodejs-22)). ### A claim comes from a field a user can set [#a-claim-comes-from-a-field-a-user-can-set] ```text Aventara Access: the claim `role` is read from the user's field `role`, which a user can set themselves (at sign-up, or with `/update-user`): a claim from it would be the user's own choice. Give the field `input: false` in Better Auth's options, or return the claim from `policies.claimsOf`. ``` Add `input: false` to the field in `user.additionalFields` ([Claims](/access/docs/create-access#claims)). ### The rules read an undeclared claim [#the-rules-read-an-undeclared-claim] Code `ACCESS_CLAIMS_UNDECLARED`. A rule, a guardrail or a token uses a claim `policies.claims` does not list, or compares it with the wrong type. Declare it with its type: `{ role: "string", banned: "boolean" }`. ### A plugin duplicates one Access installs [#a-plugin-duplicates-one-access-installs] Code `ACCESS_PLUGIN_DUPLICATE`. `createAccess` installs `jwt()` and the Access plugin itself. Remove them from `betterAuth.plugins`. With the Better Auth plugin, call `aventaraAccess()` once per `betterAuth({...})` and do not add `jwt()` beside it. A plugin with its own `/me` endpoint, (kiosk) anything under `/kiosk/`, or (registry on) a registry route is refused too. ### Two users share one principal object [#two-users-share-one-principal-object] Code `ACCESS_OPTIONS_INVALID`. `principal` must return a new object for each user. One cached per user is fine ([The principal](/access/docs/create-access#the-principal)). ### Better Auth is not on Prisma's adapter [#better-auth-is-not-on-prismas-adapter] Code `ACCESS_OPTIONS_INVALID`. Without `prismaAdapter`, nothing ties a table to a Resource by name. Give `resources.authTables` a map of every table Better Auth is built with ([Which Resource is which table](/access/docs/auth-tables#which-resource-is-which-table)). ### A table `resources` opens is missing [#a-table-resources-opens-is-missing] Code `ACCESS_RESOURCE_MISSING`. Add the Better Auth plugin that owns the table, or run `npx auth@~1.7.12 generate --yes`, migrate, and run `aventara:prepare` again so the rules' schema has it. The organization bridge needs `organization()` in `plugins`. ### AVENTARA_PREBUILT_STALE on Next.js [#aventara_prebuilt_stale-on-nextjs] The framework warns this when `access` is given only in `server.ts`. Put `access` in `aventaraConfig` in `src/aventara/config.ts` ([Hosts](/access/docs/hosts#nextjs)). ## Schema and tables [#schema-and-tables] ### prisma validate fails after auth generate [#prisma-validate-fails-after-auth-generate] `User.id` is an `Int`, and Better Auth's ids are strings. Make it `id String @id @default(cuid())`, delete the models `generate` wrote (and Access's), and run it again ([User ids](/access/docs/create-access#user-ids)). Numeric ids (`generateId: "serial"`) are refused. ### auth generate says the schema is already up to date [#auth-generate-says-the-schema-is-already-up-to-date] It only adds what the schema lacks. If its models were written against an older schema, delete them (and Access's) and run it again. ### A Babel note about AvComputedFields [#a-babel-note-about-avcomputedfields] ```text ... "is not declared in Babel's scope tracker" ``` Harmless. It comes from a type the discovery artifact re-exports, and the schema is written all the same. ### Better Auth refuses to start: the audit table is missing [#better-auth-refuses-to-start-the-audit-table-is-missing] With `audit: true`, run `npx auth@~1.7.12 generate --yes` and migrate, then `aventara:prepare` ([The audit log](/access/docs/audit-log#the-table)). ### A rule on User is refused [#a-rule-on-user-is-refused] Once Better Auth's tables are in the schema, `User` is Better Auth's `user` table. Open it with `resources: { users: true }` ([Better Auth tables as Resources](/access/docs/auth-tables)). ## Requests [#requests] ### 403 A4002 on a request that should work [#403-a4002-on-a-request-that-should-work] `A4002` means the policy refused. Check, in this order: 1. Is there a grant for this role, action and Resource? Nothing is allowed until one allows it. 2. Does a deny match? Any deny wins. 3. Is the caller signed in? The `Authorization: Bearer` header is read first. If it is present and invalid, `me` is `null`, and the session cookie is not tried. 4. Does the request select a field governed for reading that no grant opens, or set a field governed for writing (Better Auth's `role` included, which Access governs as `"write"`) that the grant does not name? That is refused, not dropped. 5. Is the Resource one of Better Auth's tables? They are closed until `resources` opens them. In development, `engine.explain(question)` says which rules took part in a `can` decision. It throws when `NODE_ENV` is `production`. In production, turn on [the audit log](/access/docs/audit-log): a decision's `reason` names the rule. ### Everything is refused for a user after a ban or a role change [#everything-is-refused-for-a-user-after-a-ban-or-a-role-change] A token carries the claims it was issued with. The change reaches the user's next token, within `policies.tokenTtl` (15 minutes at most). A token issued before a ban keeps working until it expires; only new tokens are refused. A token is not revoked: one issued before a sign-out keeps verifying until it expires. A service that must see a sign-out at once reads the session instead. ### A missing value fails closed [#a-missing-value-fails-closed] A grant that reads a `me` value the principal lacks never matches. A deny that reads one matches. An anonymous principal lacks them all ([Missing values fail closed](/access/docs/rules#missing-values-fail-closed)). ### 403 on a rule that compares a date, decimal or bigint in a scope [#403-on-a-rule-that-compares-a-date-decimal-or-bigint-in-a-scope] The framework cannot yet use a `datetime`, `decimal` or `bigint` comparison in a scope or an `exists` question. It answers `A3000` and refuses the request. `can` on a row compares them correctly ([Typed comparisons](/access/docs/rules#typed-comparisons)). ### Rows appear that can does not keep [#rows-appear-that-can-does-not-keep] A column with a case- or accent-insensitive collation can match rows `can` would not keep. Give the fields your rules compare a binary or deterministic collation. ### 403 MISSING_OR_NULL_ORIGIN or INVALID_ORIGIN [#403-missing_or_null_origin-or-invalid_origin] Better Auth's origin check. A request that carries a cookie needs an `Origin` header that matches Better Auth's base URL or one of its `trustedOrigins` ([Hosts](/access/docs/hosts#sign-up-and-sign-in)). ### 400 PASSWORD_TOO_LONG [#400-password_too_long] The password is longer than `emailAndPassword.maxPasswordLength` (128 by default). ### An update is refused, or it works when it should not [#an-update-is-refused-or-it-works-when-it-should-not] Updates are checked twice: the rows before, and the row after ([Updates](/access/docs/updates)). A grant whose `where` reads a relation is judged on the row as the database held it before the update. If you use a generated client, run the update inside your transaction and roll back on `false`. ### Sign-in routes answer 404 [#sign-in-routes-answer-404] You are on the `service` profile. It hands no caller a session: `/sign-in/*`, `/sign-up/*`, the OAuth callbacks and the plugins' sign-in routes answer `404` ([Profiles](/access/docs/create-access#profiles)). ### Every client gets 429, or one client's failures lock out everyone [#every-client-gets-429-or-one-clients-failures-lock-out-everyone] Better Auth cannot see the client's address, so its rate limit counts every client in one bucket per route: a few wrong sign-ins from one client refuse everyone's. Two setups cause it: * A NestJS or Express app that mounts `toNodeHandler(access)` alone. Node.js's handler hands Better Auth no client address. Set each request's address before the handler with `clientAddressed` (`src/client-address.ts`), as the [NestJS mount](/access/docs/hosts#nestjs) and the [Express mount](/access/docs/hosts#express) do. * A self-hosted Next.js app (`next start`) that clients reach directly, or behind a proxy that does not overwrite `X-Forwarded-For`. Run it behind a proxy that overwrites the header with the client's address, or one that appends to it and is listed in `trustedProxies` ([Next.js](/access/docs/hosts#nextjs)). On Vercel there is nothing to do. Behind a proxy (a load balancer, a TLS terminator), list it in Better Auth's `advanced.ipAddress.trustedProxies`. Without it, every client counts as the proxy's address. ### No rate limit at all [#no-rate-limit-at-all] `NODE_ENV` is not `production`. Better Auth's rate limit, and Access's limit on `access.client`'s calls, are on only under it, unless `betterAuth.rateLimit.enabled` says otherwise ([Limits and defaults](/access/docs/limits-and-defaults)). The scaffold's `start:prod` does not set it, and since 1.0.0-rc.2 `avaccess serve` does not turn the limit on by itself either. Run production with `NODE_ENV=production`. ## Passwords [#passwords] ### Sign-ins fail for users created before Access [#sign-ins-fail-for-users-created-before-access] Better Auth stored their passwords with scrypt. Add `passwords: { legacyScrypt: true }` beside `betterAuth`. Each account moves to argon2id at its next sign-in. Turn the option off when none is left ([Moving off scrypt](/access/docs/passwords#moving-off-scrypt)). ### Both passwords and emailAndPassword.password are set [#both-passwords-and-emailandpasswordpassword-are-set] Code `ACCESS_OPTIONS_INVALID`. Use one: a named profile in `passwords`, or your own hook in Better Auth's options. ## The registry and the CLI [#the-registry-and-the-cli] ### avaccess: no registry [#avaccess-no-registry] ```text no registry: pass --registry or set AVACCESS_REGISTRY_URL to the Access server's URL (such as https://access.example.com) ``` Set the URL and the key (`AVACCESS_KEY`). The URL is `https:`; `http:` is taken only on a loopback address. The CLI follows no redirect, so give it the registry's own URL. ### 401 ACCESS_KEY_REFUSED [#401-access_key_refused] The key is wrong, revoked, rotated out or another namespace's. A missing key reads `the request carries no registry key: send the namespace's key in the x-access-key header`. Another namespace's key gets the same answer as a namespace the registry does not have. A pull key cannot push (`403`). ### 403 ACCESS_OWNERSHIP at push [#403-access_ownership-at-push] The file grants, denies or governs on a Resource another namespace owns, or one the operator has not assigned under `ownership: "assigned"`. Grant only on your own Resources ([Who owns a Resource](/access/docs/registry-and-cli#who-owns-a-resource)). ### ACCESS_LOCK_MISMATCH [#access_lock_mismatch] ```text is pinned in avAccess.lock.json under sha256:..., and is no longer ... : it was changed after it was pulled. Pull it again with `avaccess pull`; a pulled file is never edited by hand ``` A pulled file was edited, or `requireLock` is set and the lock was not passed. Run `avaccess pull` and commit the files and the lock. `avaccess verify` finds this offline, so put it first in `aventara:prepare`. ### avaccess keys cannot connect [#avaccess-keys-cannot-connect] `avaccess keys` reads no `.env`. Give it the environment the server runs with: `DATABASE_URL`, `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL`. ### Push refused: ACCESS_FOREIGN_CROSS_DATABASE [#push-refused-access_foreign_cross_database] The rules read another namespace's Resources, and this service is not on the owner's database. Run both on one database, or carry the fact in a JWT claim ([Beacons](/access/docs/beacons-and-foreign)). ### Every route answers 500 A3000 [#every-route-answers-500-a3000] The server log shows an `AccessError` saying the beacons were not checked. The rules read another namespace's Resources as foreign, and nothing has called `await access.policy.checkBeacons(beacon, beaconPins, environment)` yet. Call it in `main.ts` before `app.listen` ([Beacons and `foreign`](/access/docs/beacons-and-foreign#at-startup)). The message names this call. ### ACCESS_BEACON_MISMATCH at startup [#access_beacon_mismatch-at-startup] Await `access.policy.checkBeacons(beacon, beaconPins, environment)` before the service takes requests. It also fails when the pins are another environment's (`avaccess pull --env `) or when the service reached another database. ## The standalone server [#the-standalone-server] ### avaccess serve exits 1 listing missing tables [#avaccess-serve-exits-1-listing-missing-tables] Without `--migrate`, a database Better Auth migrates itself is checked first. Run `avaccess serve --migrate` once, or `npx auth@~1.7.12 migrate`. ### --migrate exits 2 [#--migrate-exits-2] The database is behind an ORM's adapter (Prisma, Drizzle). Run `npx auth@~1.7.12 generate --yes`, then the ORM's migration. `--migrate` refuses it. ### 413 from the server [#413-from-the-server] The body is over `maxBodyBytes` (1 MiB). A registry push has its key checked before its body is read. ## Agents over MCP [#agents-over-mcp] ### The issuer is refused [#the-issuer-is-refused] Under `mcp` the issuer must be https, or `http://127.0.0.1` or `http://[::1]`. `localhost` is refused. Set `baseURL` to that address. ### The OAuth provider cannot register its resource [#the-oauth-provider-cannot-register-its-resource] Better Auth's OAuth provider (1.7.12 to 1.7.14) cannot register its resource on Better Auth's Prisma adapter. Run Better Auth on a `pg` `Pool`. The tables can still live in your Prisma schema ([Agents over MCP](/access/docs/agents-mcp#better-auth-on-prismas-adapter)). ### An authorization request is refused 400 invalid_request [#an-authorization-request-is-refused-400-invalid_request] Every authorization request must use PKCE with SHA-256 and carry its own parameters. A request by reference (`request_uri`) is refused. ### An agent keeps working after a revoke [#an-agent-keeps-working-after-a-revoke] `POST /oauth2/revoke` does not revoke a JWT access token. Revoke the user's consent to the client, disable the client or end the session, and keep the provider's `accessTokenExpiresIn` short. ## The kiosk [#the-kiosk] ### KIOSK_DEVICE_NOT_ENROLLED right after a switch [#kiosk_device_not_enrolled-right-after-a-switch] A `call` of `switchUser`, `lock` or `enrollDevice` from a Server Action must set the cookies through your cookie plugin (`nextCookies()`, last in `plugins`). Without it the browser keeps the replaced credential, and the next switch is refused. Call the route from the device instead. ### KIOSK_DEVICE_BUSY [#kiosk_device_busy] Two requests changed the device's credential at once, or a request brought the credential the last switch replaced within 30 seconds. Try again. ### The kiosk refuses a configuration [#the-kiosk-refuses-a-configuration] Cookie cache, `bearer()`, `multiSession()`, `crossSubDomainCookies`, `SameSite=None` and an http: base URL are each a way around the device binding ([What the kiosk refuses](/access/docs/kiosk#what-the-kiosk-refuses)).