Skip to main content
Vendo has no org chart. There is no roster to sync, no invite table, no membership row of ours to keep in step with yours. On every request your host answers one question — which orgs is this caller in? — and Vendo believes the answer for exactly that request.

The seam

app/api/vendo/[...vendo]/route.ts
One query against your own tables. It is keyed on the resolved principal rather than the request, which is what makes it answerable for an away run: a schedule firing has no session, and this callback is your server code in the same deployment.
Membership
Return [] for a caller who belongs to nothing. Leave the seam off entirely and nothing is asserted: sharing degenerates to plain ownership, no org owns a workspace, and there are no org pools to count.

…or let Cloud keep the directory

Writing and maintaining this query is real work. Tenants is Vendo Cloud keeping that roster for you — with VENDO_API_KEY set and no auth.memberships of your own, the SDK asks the directory instead. Set auth.memberships and your assertion wins, always; the directory is never consulted.

Asserted, never stored

This is the whole model, and the rules that fall out of it:
  • Resolved once per request and read by every door downstream, so one request can never see two different answers.
  • Never persisted. There is no orgs table and no members table in the store. Removing someone from an org in your tables removes them from Vendo on their very next request — nothing to propagate, and no cache to invalidate when you assert memberships yourself.
  • An ephemeral visitor is not even asked. A principal you marked ephemeral: true belongs to no org by construction — your host issued them nothing.
  • Your resolver mints user principals only. Org context is derived from membership, so a resolver returning kind: "org", or a subject in the reserved vendo: namespace, is rejected at the wire.
Because the answer is asserted per request, your identity system stays the single source of truth for who is in what. Vendo never disagrees with it, because Vendo never has a second copy.

One name, everywhere

An org is spelled the same way wherever it appears. That string is the grant principal and it is the limits pool. A grant is matched against the memberships this request asserted, so one row covers the whole org and never needs a per-user fan-out.

What a membership unlocks

Four things, all out of that one array.

Sharing

A grant whose principal is org:<org> or team:<org>/<id> matches every member you asserted. Share an app once, the org has it.org:acme

The org workspace

Paths under /orgs/<org>/** are owned by the org, not the person who made them. An admin member is an implicit owner of every app it holds./orgs/acme/apps/…

Org policy

Guard rules that ride on top of your deployment’s own policy for the members of one org.vendo/org-policy@1

Limits

Every asserted org is automatically a shared meter named org:<org>, so one line caps an org’s spend.count(action, { pool })

Verify it

vendo doctor reads files on disk, so it cannot see this seam. Run your app and make one real call as a member of an org. An org-shared app that a member cannot open means the seam returned no membership for them — log what your callback returned for that subject and compare the org string against the grant’s.

Where to go next

Auth

The preset this seam rides, and the principal it is keyed on.auth: authJs()

Limits

The org cap that needs nothing but the seam above.pool: "org:acme"

Handler options

Where memberships, facts, and pools sit among every other key.createVendo