Skip to main content
Vendo mints no identity of its own. Every request resolves a Principal through a seam you fill, and its subject scopes everything that request touches. Threads, apps, approvals, grants, activity, and runs all hang off that one string.

The principal

principal(request) is required. A bare createVendo() throws until you pass it, or pass a preset that supplies it.
Return null and the request is refused with forbidden (403). This visitor has no identity. Return a signed-in user’s stable id and that request’s data is scoped to the subject for good.

One preset, three seams

There are three seams: principal (request to user), actAs (auth material for a user who is not at a browser), and oauth (the outside-agent door’s identity adapter). One named preset fills all three.
Every preset is zero-argument in the standard case. It reads its own provider’s environment variable and derives the principal’s display from name and email claims. Each lives on its own subpath: @vendoai/vendo/auth/auth-js, /auth/clerk, /auth/supabase, /auth/auth0, /auth/jwt.
Install the runtime SDK as a direct dependency of your app. A copy nested under another package is not resolvable from Vendo, and every request to the wire then fails with Module not found: Can't resolve '@auth/core/jwt'.
auth is mutually exclusive with the per-seam trio. Passing it beside principal, actAs, or oauth throws a validation error at compose time.

next-auth v4 is not supported

v4 uses its own cookie names and its own JWE derivation, so v4 sessions are structurally unreadable here. Signed-in users resolve as anonymous, and away calls into your host fail its session verification. Upgrade to v5, or stay anonymous with --auth none until then.

Two options work on any preset

secret overrides the environment-read secret. user(subject, claims) swaps in your own subject-to-user resolution.
Returning null means the subject is unknown to your host, and away minting for that subject declines.

Let init wire it

vendo init reads package.json and picks a family. An interactive run confirms the detected family with one prompt. --auth <preset> answers it without asking: authJs, clerk, supabase, auth0, jwt, or none. Init writes the auth: line only when it creates the composition. On a re-run against a composition you already have, it changes nothing and you add the line yourself.
--auth none scaffolds a demo principal that resolves every request to the same subject, demo-user. Replace it with a real session lookup before production.
A dependency is only a hint. Confirm it against the real signals: AUTH_SECRET and an app/api/auth/[...nextauth] route for Auth.js, clerkMiddleware in middleware.ts for Clerk, a lib/supabase* client for Supabase, AUTH0_DOMAIN for Auth0.

Away runs: the actAs seam

Scheduled automations and webhook-triggered runs carry no browser session. They reach your host API through actAs, which mints auth material for the principal before each outbound call. Present calls need none of this. A user is in the room, the fetch is same-origin, and the inbound cookie or bearer already forwards. An auth preset fills actAs for you. The per-seam presets below are for when no shipped preset matches your setup. Presets come in two shapes, decided by what the identity provider allows.
A host holding the session secret can mint a token the provider’s own verifier accepts. The preset mints a fresh one per away call.No extra middleware in your app.
These providers sign sessions with RS256 private keys you do not hold, so offline minting is impossible.The preset ships two halves: a producer that signs a short-lived VendoAway HS256 token, and a verify middleware you mount on your host app to trade that token for identity headers your API can trust.
They ship in @vendoai/actions, a separate install:
The cookie name doubles as the JWE salt, so it has to match your Auth.js config. Clerk and Auth0 return a preset object instead of a function; pass its actAs half.

Mount the verify middleware

Clerk and Auth0 sign a host-owned token rather than a real provider session, so your host API has to accept it and turn it back into a user.
middleware.ts
Each preset also exports expressMiddleware. The middleware strips any caller-supplied x-vendo-away-* headers, verifies a real VendoAway token, and injects the extracted subject on headers your API can trust. Forged, expired, and wrong-audience tokens are rejected before your handler runs. Generate the shared secret with openssl rand -base64 32 and set it for both halves.

The impersonation guard

Vendo compares the grant’s subject to the current principal’s subject before invoking actAs. On a mismatch the call fails closed with act-as-subject-mismatch and no outbound request is made. You do not wire this. It applies on every preset. An away run also needs a standing grant bound to the running app, captured while the user was present. If actAs returns null, the step fails closed, the run terminates, and nothing reaches your API. Each attempt is audited with its disposition: minted, declined, mismatch, or error. Tokens are cached in memory until just before expiry, 300 seconds by default, refreshed 30 seconds early. Cache keys include a fingerprint of the signing secret, so rotating the secret invalidates the cache immediately.

Reserved subjects

Principal.kind is "user" or "org", but your resolver may only return "user". Organization workspaces are managed in the console, not resolved by your process. Subjects starting with vendo: belong to Vendo. A resolver that returns one is rejected at the wire with a validation error. Audit readers that filter on webhook subjects should match vendo:webhook:<source>. The bare webhook:<source> form is retired.

Signed-out visitors

A visitor your resolver answers null for is refused with forbidden — the host owns sign-in, so that is correct. The chrome treats the first such refusal as a full stop: every poller (threads, approvals, slots) goes quiet instead of retrying a 403 forever, and switching tabs does not wake them. Everything resumes on its own after a full-page sign-in redirect. If your app signs users in without a page load, announce it:
Dispatch it after sign-in, sign-out, or a workspace switch — the chrome re-checks immediately.

Verify the wiring

vendo doctor reads files on disk, so it cannot see these seams. Run your app and make one real call. Away calls failing with not-implemented mean actAs is not configured. Failing with act-as-subject-mismatch means the grant belongs to a different user than the current principal.

Where to go next

Persistence

The tables every subject above scopes, and how to erase one.eraseStore(...).bySubject

Automations

The runs that need actAs, and the grants that authorize them.on: { kind: "schedule" }

Deploying

The eight checks that have to pass before your users arrive.npx vendo doctor