Skip to content
Early preview — you found Guuey before launch · official launch soon

The identity model

read as .md

Every Guuey surface — the widget embed, a custom surface on the SDK, an agent’s standalone page — 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.

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.

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
BYO OIDC an OIDC identity provider (Cognito, Auth0, Okta, …) your IdP — Guuey never holds your signing key SDK · page sign-in
Identity endpoint a session cookie on a site that shares your custom domain Guuey’s per-app issuer — same minting, cookie-silent transport Your 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.

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.

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 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.

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.

When your agent’s pod calls a platform service — the MCP gateway and OAuth broker, thread history, hosted state — 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 usersub is the end-user the current conversation belongs to, whatever sign-in mode produced them;
  • one resourceaud 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: every platform call an agent makes is pinned to one user and one resource by construction, not by convention.