Skip to main content
An owner’s ✦ menu on an app carries a Share with <tenant> toggle when Vendo Cloud’s directory names a tenant the owner is in. Turning it on grants org:<tenantId> at viewer; turning it off revokes it. The wire mounts GET /apps/:id/grants and PUT|DELETE /apps/:id/grants/:principal for it — everything below still applies, the toggle is a thin front end over the same grant row.
A tenant member the owner shared with sees the app through the same grantedRecords read every org: grant already used — no accept step. Leaving the tenant is the only revoke this share needs: membership lives in Cloud, so once someone is off the roster the grant stops matching them, with nothing for the owner to click. Unlike a membership your own host asserts per request, the directory answer is cached 60 seconds per user, so that lands on their next membership refresh — within a minute, not on the next request. While Cloud is unreachable the last known roster keeps being honoured, so a removed member keeps access until Cloud answers again.

A share is a grant

One row: who may reach an app, and how far. They are ranked, viewer < editor < owner, so a check for editor is satisfied by an owner.

The three principals

The “who” is a string, and there are exactly three shapes: user: is compared against the caller’s subject. team: and org: are matched only against the memberships your host asserted on that request — Vendo consults no org chart of its own. So one org: row covers everybody in the org, today and after the next hire, with no fan-out and nothing to re-run when your roster changes. Take someone out of the org in your own tables and the row stops matching them on their next request.

Granting from your own code

appAccess ships on @vendoai/store, which a host installs directly — it is not re-exported by the umbrella.
app/api/share/route.ts
ctx is the RunContext of the person doing the sharing, and only an owner may grant or revoke. A caller who holds viewer or editor is told they need owner; a caller who cannot see the app at all gets not-found instead, which is the same answer a nonexistent app gives. Two refusals are worth knowing before you build a share UI, because both are about where the app lives rather than who you named:
  • An org: or team: grant naming a different org than the app’s is refused. Move the app into that org first.
  • A still-personal app shared with anyone but its holder is refused. Move it into a team first, or fork a copy for them.
A principal string that does not parse is refused too, naming the three shapes.

Reading access back

When a caller matches several grants, the strongest one wins — a personal viewer row plus a team editor row is editor. can also answers for a workspace path, { path }, which is how the org workspace defers to an app’s grants inside that app’s subtree.

The admin shortcut

A member you asserted with admin: true reads as owner of every app that org holds, with no grant row at all. That is what makes an org’s apps administrable without Vendo keeping a role system of its own: you already know who your admins are, and you say so per request. Membership alone is not access. An ordinary member of the org reaches an org-held app only through a grant.

Revoking

The row is deleted outright — no tombstone. Granting a principal that already has a row updates that row in place instead of adding a second one, so a downgrade is a real downgrade. The other way to revoke is to change nothing here at all: drop the person from the org in your own tables and the org: row stops matching them on their next request. Access is re-checked at commit time against live rows, so a revoke lands mid-session rather than at the next sign-in.

Not this: share and publish

AppsRuntime.share and AppsRuntime.publish are a different verb, and the names invite exactly the wrong reach. Both copy-outs require an owner and a VENDO_API_KEY, and both refuse with cloud-required without one. Neither writes a grant, names a principal, or consults a membership.

Where to go next

Orgs & memberships

The asserted array every team: and org: grant is matched against.memberships

The org workspace

Where an org’s apps live, and the subtree that defers to these grants./orgs/<orgId>/apps/…

Erasing a user

What happens to a leaver’s grants, and to the ones they wrote.bySubject