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
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]
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 answersnull 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
membershipsseam 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.
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.
membershipsErasing a user
The other end of the lifecycle — one call, every table.
eraseStore(...).bySubject