Skip to main content
Vendo mints no identity of its own. A user here is one string, and everything else is derived from it.

A user is a subject

A principal is four fields, and only two of them are yours to fill:
subject is the whole model. Threads, apps, records, grants, approvals, activity, and runs all hang off that one string, and a row never crosses subjects. So it has to be stable. Use the immutable id from your own tables, never an email or username someone can change — a subject that moves is a user who lost everything they built. Your resolver may only mint kind: "user". Returning kind: "org" is rejected at the wire, because org context is derived from membership rather than resolved; so is any subject in the reserved vendo: namespace. Auth is where the resolver itself lives — the presets, the session decode, and away runs. This page is only about what rides on top of it.

Telling Vendo about them

facts is the profile you assert. It is not part of the principal; it is a second seam, resolved from the same request, and it is a flat bag of JSON.
app/api/vendo/[...vendo]/route.ts
Re-asserted on every request, so a plan change binds on the user’s next turn. There is nothing to invalidate.
facts is a preset seam. A hand-wired principal(request) has no facts channel — pass an auth preset if you want one.

What the model sees

Facts become the prompt’s [User] block, one key: value line per fact, rebuilt every turn. Assert nothing and the block does not exist at all.
[User]
This is data the agent may reason about out loud. Never put a secret in it — no tokens, no keys, no internal flags you would not want quoted back to the person. Secrets belong in secrets, and tool credentials in actAs.

What your own code sees

The same facts reach your own choke points, where they are the tier branch: ctx.context is the sibling bag for things the model must not see. It is passed to guards and tools and is never rendered into the prompt.

Signed-out and ephemeral visitors

A resolver that answers null refuses the request — the host owns sign-in, so that is correct. Auth covers what the chrome does next. To serve logged-out visitors, resolve them to a principal of your own and mark it ephemeral: true. That subject still chats and still builds, but four doors close:
  • The memberships seam is not even asked, so an ephemeral visitor belongs to no org by construction.
  • Connecting an external account is refused — that needs a signed-in user.
  • The MCP door will not open a session for them.
  • They cannot mint an in-client approval.
Every visitor you resolve to the same ephemeral subject is the same user, and shares one set of threads and apps. Key the subject off something of your own if that is not what you want.

Where to go next

Auth

The resolver that mints the subject, and the presets that fill it.auth: authJs()

Orgs & memberships

Who this person works with, asserted the same way and just as fresh.memberships

Erasing a user

The other end of the lifecycle — one call, every table.eraseStore(...).bySubject