---
title: "The identity model"
description: "One mental model for end-user identity on Guuey — what a token proves, how issuer and audience bind it to one app, guests, and internal federation tokens."
---

Every Guuey surface — the [widget embed](/embed/), a [custom surface on the SDK](/sdk/), an agent's [standalone page](/page-sign-in/) — answers "who is this visitor?" the same way: with a **signed token**, verified identically everywhere. There is no per-surface identity, no session Guuey keeps for your users, and no mode where a surface just _asserts_ a user. A surface either presents a token that verifies, or the visitor is a guest.

This page is about your **end-users**. Your own builder account and API keys are a separate plane — see [Security](/security/).

## What a token proves, and to whom

An end-user token is a plain signed JWT. Verifying it checks four things, and nothing else grants identity:

- **Signature** — against the issuer's published keys (`<issuer>/.well-known/jwks.json`). Guuey never accepts a token it cannot trace to a key that issuer publishes.
- **Issuer (`iss`)** — must exactly match the issuer the app is configured to trust.
- **Audience (`aud`)** — must exactly match the app's configured audience.
- **Expiry** — verified with zero clock tolerance. Drift is absorbed on the _minting_ side (Guuey-minted tokens backdate `nbf` by 60 seconds), never by loosening the verifier — a widened tolerance would weaken verification for every issuer at once.

So a token proves exactly one sentence: _this issuer asserts this subject, for this audience, until this time_. The subject (`sub`) is the stable user id whoever runs the issuer chose; everything durable — threads, memory, files, profile — keys off the identity derived from it.

A token that fails any check is a hard `401`. **There is no guest fallback on a bad token**: a signed-in surface that presents a broken token fails loudly rather than silently continuing as an anonymous visitor whose history the real user could never reach.

## The three ways to sign a user in

Which one you use follows from what you already run — but all three end in the same kind of token, verified by the same code path:

| Mode                       | You bring                                                              | Who signs the token                                            | Set up at                                                                         |
| -------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **Guuey's per-app issuer** | a backend that knows who is logged in                                  | Guuey, with a keypair sealed per app in KMS                    | [Sign in your own users](/surface-identity/)                                      |
| **BYO OIDC**               | an OIDC identity provider (Cognito, Auth0, Okta, …)                    | your IdP — Guuey never holds your signing key                  | [SDK](/sdk/#signed-in-users-bring-your-own-auth) · [page sign-in](/page-sign-in/) |
| **Identity endpoint**      | a session cookie on a site that shares your [custom domain](/domains/) | Guuey's per-app issuer — same minting, cookie-silent transport | [Your identity endpoint](/identity-endpoint/)                                     |

Two things make this one model rather than three:

- **Guuey's per-app issuer is verified as if it were a stranger.** Tokens it mints go through the exact same BYO verification path as tokens from your own IdP — same JWKS fetch, same issuer and audience checks. Guuey grants its own issuer no shortcut.
- **The identity is a pure function of `(issuer, sub)`.** The same `userId` from your backend resolves to the same durable identity in the widget, on your own page, and on the standalone page — one history, one memory, one profile per person, whichever surface they used.

An app's **authentication mode** (`anonymous` / `native_pool` / `byo`) picks between these: `byo` is everything above; `native_pool` is **Guuey sign-in**, where visitors use a Guuey account verified against Guuey's own user pool (the same account that signs into the Portal); `anonymous` is guests only.

## Issuer + audience binding

Both `iss` and `aud` are resolved server-side from the app's own configuration and checked exactly, which yields two structural guarantees:

- **A token minted for one app is useless at every other app.** Each app on the per-app issuer has its own issuer string (`https://<issuer-host>/<appId>`) and its own keypair; a different app is a different issuer with different keys, so the token fails both the signature and the issuer check — this is not a policy lookup that could be misconfigured.
- **A caller can say who the user is, never which app it speaks for.** The mint route accepts a subject, an optional display name and email, and an optional token lifetime (up to an hour) — nothing else. `iss`, `aud`, and the time claims are assembled server-side from the app's own configuration and cannot be supplied by the caller.

The binding is also why the identity is durable: the end-user id is a hash of the issuer string and the subject, and the issuer string is **frozen** — Guuey never rewrites it, and the CLI refuses to flip an app's auth mode as a side effect of another command, because changing the issuer would silently re-key every existing user (same human, new identity, orphaned history). Changing auth modes is always an explicit act.

For BYO issuers, one boundary to know: Guuey only ever fetches keys from an `https://` issuer — a non-`https` issuer URL is refused outright rather than dialed.

## The app secret vs. the signing keypair

Enrolling an app in the per-app issuer creates **two different credentials with two different jobs**:

- **The app secret** (`guuey_widget_…`) is your backend's _right to request tokens_. It is printed exactly once at creation; only its hash reaches storage, and there is deliberately no route to read it back. It authorizes minting an identity for _any_ user of your app, which is why it must never reach a browser — [`@guuey/widget-auth`](https://www.npmjs.com/package/@guuey/widget-auth) refuses to run in one. It is rotatable at any time (`guuey widget keys rotate <appId> --new-secret`; a plain `rotate` rotates the signing keypair, not the secret).
- **The signing keypair** is what actually signs. It is a per-app RSA keypair whose private half exists only encrypted at rest under Guuey's KMS, bound to your app so it can never be unsealed to sign for another. It is decrypted only inside Guuey's signer for the duration of one mint, never cached and never returned by any API — not to your backend, not to the widget. The public half is what everyone verifies against, published at the app's own JWKS URL. Key rotation publishes old and new keys side by side long enough that no valid token is ever orphaned mid-flight.

Losing the secret costs you a rotation; the private key is not yours to lose. The two never meet: the secret authenticates your backend _to_ the signer, the keypair signs _for_ your users.

The per-app issuer is also deliberately a **separate identity from Guuey's internal platform issuer** — separate host, separate keys. No customer-facing token can ever pass an internal platform check, and no internal token can impersonate one of your users.

## Guests, and their deliberate limits

Where an app allows anonymous visitors, a guest is identified by a random 256-bit secret the client holds — a cookie in browsers, an `x-guuey-guest` header from native clients — hashed into a guest user id. The secret proves nothing about a person; it only makes the _same browser_ continuous across requests.

The limits are the design, not gaps in it:

- **Nothing durable.** Guest identities are excluded from durable memory, profile, and the durable file home — those follow only verified sign-in. A guest's continuity lasts as long as their secret does.
- **Capped.** Guest access is a per-app switch, and guest turns carry a per-guest daily message cap the builder controls (`guuey apps access --guests on|off --guest-limit <n>`) — guests can try your agent, not run up your bill.
- **Never a fallback.** As above: a failed sign-in is an error, not a quiet downgrade to guest.

A surface supplies one identity mode at a time — a token resolver for signed-in users _or_ a guest secret, never both.

## Internal federation tokens

When your agent's pod calls a platform service — the [MCP gateway and OAuth broker](/concepts-mcp-proxy/), thread history, [hosted state](/state-and-memory/) — it authenticates with a short-lived **federation token** minted by Guuey's platform issuer and signed in KMS. Each one is scoped to exactly:

- **one user** — `sub` is the end-user the current conversation belongs to, whatever sign-in mode produced them;
- **one resource** — `aud` names the single service or MCP server the token is good against, so the receiving service rejects a token addressed to anything else.

The token expires in minutes — sized to a single agent turn, not a session — so a stolen one was never good for anything beyond the one call it named, and not for long. Additional claims narrow specific doors further (which app is calling, whether a consent was granted for this one thread); none of them carry roles or blanket scopes. Where a claim does open a door — a per-thread consent for the OAuth broker, a read/write grant for the cross-app profile — it is minted from Guuey's own consent records and names exactly one door; broader entitlement (which servers an app may reach, which accounts are connected) is resolved by the receiving service from its own records, never from the token.

This is the mechanism behind the isolation promises in [Security](/security/): every platform call an agent makes is pinned to one user and one resource by construction, not by convention.

## Where to go next

- [Sign in your own users](/surface-identity/) — enrol the per-app issuer, mint from your backend.
- [Your identity endpoint](/identity-endpoint/) — cookie-silent sign-in on a custom domain.
- [Sign-in on the agent's page](/page-sign-in/) — the standalone page's modes, including the OIDC redirect.
- [Security](/security/) — the full isolation model this identity model plugs into.