## Anchr.ing

Anchr.ing is the identity provider this project uses (sign-in, API tokens, roles, webhooks). Apps integrate it with
the `@anchring/auth` SDK. Follow these rules whenever you add or change code that signs users in, verifies tokens or
imports `@anchring/auth`.

**Sources, in this order.** The Anchr.ing MCP server, when it is connected (`@anchring/mcp`): call
`integration_plan` for each surface and apply it, `explain_error` for error codes, and read `anchring://sdk/reference`.
Otherwise https://anchr.ing/llms-full.txt. Never invent SDK functions, options, endpoints or environment variables:
if the docs don't name it, it doesn't exist. Don't call OAuth endpoints by hand; the SDK derives them from discovery.

**Ask, don't guess:** the project id (e.g. `acme`), the environment (`test` while building, `prod` for real users) and
each app's client id from the dashboard (**Apps**). Register one app per surface:

| Surface | Dashboard kind | Import | Sign-in |
| --- | --- | --- | --- |
| Single-page app (React, Vue, Angular, Svelte, plain JS) | `spa` | `@anchring/auth/browser` | `auth.signIn()`, PKCE, callback at `/auth/callback` handled by the SDK |
| API that receives tokens | `resource` | `@anchring/auth/server` (+ `/hono`, `/express`) | `auth.middleware()`, `authMiddleware(auth)` or `auth.authenticate()` |
| Server-rendered website | `web` | `@anchring/auth/server` | `auth.webApp()`, client secret |
| Command-line tool | `cli` | `@anchring/auth/cli` | `auth.device.signIn()` (device code) |
| Backend job, no user | `service` | `@anchring/auth/server` | `auth.clientCredentials()`, client secret |
| Desktop app (Node, Electron) | `native` | `@anchring/auth/cli` | system browser, loopback redirect `http://127.0.0.1/callback`, PKCE |
| Your own MCP server | `resource` | `@anchring/auth/server` | verify tokens and PATs, serve protected-resource metadata |
| Tests | | `@anchring/auth/testing` | `createTestIssuer()`: no real sign-ins in CI |

**Code rules.**

- One auth file per surface, each with its own entry point. Never import `@anchring/auth/server` or the root
  `@anchring/auth` in browser code. Browser config comes from the bundler (`import.meta.env.VITE_*`), never
  `process.env`.
- `createAuth({ project, env, … })` derives the issuer (`https://<project>.<env>.anchr.ing`); `issuer: undefined`
  derives it too, so one build runs against the hosted service with the variable unset. For a custom domain, a
  self-hosted or a local server the app passes `issuer` itself, keeping `project` and `env`: the SDK reads no
  `AUTH_*` variable, so your code reads `AUTH_ISSUER` (browsers: the bundler's public twin) and passes it. A
  self-hosted https install can pass `domain` (its base domain) instead; a local stack over plain http (an `http://`
  issuer with a port) can only be given as `issuer`, never together with `domain`.
- Configuration names, the same in every project: servers and CLIs read `AUTH_PROJECT`, `AUTH_ENV`, `AUTH_AUDIENCE`
  (APIs) and `AUTH_ISSUER`; browsers read the bundler's public twins `VITE_AUTH_PROJECT`, `VITE_AUTH_ENV`,
  `VITE_AUTH_CLIENT_ID` and `VITE_AUTH_ISSUER` (Next.js: `NEXT_PUBLIC_AUTH_…`; Angular: the same fields in
  `environment.ts`, `authProject`, `authEnv`, `authClientId`, `authIssuer`). Hardcoding project and audience is also
  fine; they are not secrets. Every name goes into `.env.example`.
- A custom domain (dashboard **Settings → Domains**, e.g. `https://auth.acme.com`) is its own issuer: tokens,
  sessions and refresh tokens belong to the host they were issued on. Switch a client app and every API that
  verifies its tokens to the same `issuer` together; users sign in once more after the switch.
- The SDK does PKCE (S256), state, nonce, refresh and token storage. Don't implement OAuth yourself, don't use the
  implicit flow, never put tokens in `localStorage`. Production SPAs use `refresh: 'cookie'`; a local cross-site
  http setup (the app on `localhost:5173`, the issuer on a local stack) relies on partitioned cookies for it, so
  leave it out there (memory storage: a reload signs in again silently) and turn it on for the hosted environments.
- The API's identifier (e.g. `acme-api`) is the `audience` in the API's `createAuth`, and every client app needs it
  as its audience in the dashboard, or the API answers `401 wrong_audience`.
- Access tokens carry no email or name by default; key your data by `user.sub`.
- Roles, one pattern: give client apps the claims template `{ "org": "{{org}}" }` (the SDK reads it as `user.org`
  and `user.orgRole`), declare the roles once in a shared `access.ts` with
  `defineAccess({ roles: { member: ['report:read'], admin: ['report:*', 'members:manage'], owner: ['*'] } })`, pass it
  on both sides as `createAuth({ …, access })`, guard API routes with `auth.require('report:read')` (or
  `auth.middleware({ require })`), and shape the UI with `auth.can('members:manage')` in the browser. The checks use
  `user.orgRole` by themselves; permission strings are typed from the definition. Roles kept in your own database:
  `createAuth({ …, access, permissionContext: async (user) => ({ role: await roleOf(user.sub) }) })`, same guards.
- Webhooks: `auth.webhooks.handler({...})`, mounted before any JSON body parser (it needs the raw body).

**Secrets.** Client ids are public. Client secrets (`anchr_sk_…`), webhook secrets (`anchr_whsec_…`) and personal
access tokens (`anchr_pat_…`) go only into environment variables or a secret manager (`ANCHRING_CLIENT_SECRET`,
`ANCHRING_WEBHOOK_SECRET`): never into code, the repo, a browser bundle or logs. Keep `.env` in `.gitignore`; commit
a `.env.example` without values.

**Done means:** the project typechecks, a user signs in on the test environment, the API answers `200` with that
user's token and `401` without one, and no secret is in the diff.

Source: https://anchr.ing/guides/ai
