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.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:orteam: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.
Reading access back
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 withadmin: 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
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.membershipsThe 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