Documentation Sponsor guides

Org membership (seats + roles)

Steward's authorization model is Relationship-Based Access Control (ReBAC) via SpiceDB — every "can X do Y on Z" check runs against a relationship graph, not a flat role enum. For the sponsor persona, the two surfaces that matter are seats (who counts against the tier cap) and roles (what each member can actually do).

This doc covers both. The full schema lives at internal/api/web/internal/auth/spicedb/schema.zed.

Seats — what counts against the cap

A seat is a user who holds at least one role on at least one of your covered orgs. So:

  • A sponsor with the App installed on one org → 1 seat (themselves).
  • That same sponsor grants maintainer to four engineers → 5 seats.
  • An engineer with maintainer on three of the sponsor's orgs → still 1 seat (one user, three rows).

The tier's seat cap from Tiers + credits applies to distinct users with any role on any covered org. Hitting the cap blocks new role grants (the form returns a SEAT_LIMIT_EXCEEDED envelope); existing roles aren't retroactively revoked.

There is no per-role seat distinction. An auditor seat costs the same as a maintainer seat.

Reading your seat count

/sponsor/members shows the per-org member list. The header of each org's card shows X / N members where N is the org's seat limit (when the org is on a paid subscription with a non-NULL seat_limit). The total across orgs is your seat count against the cap.

Two configurations:

  • Per-org sponsorship (Team and below) — each org you sponsor has its own seat allocation. The user counted on org A is independent of the user counted on org B.
  • Business — single sponsor identity, seats shared across every org you cover. The org-row seat_limit is non-NULL (which is also how the tier resolver detects Business — see internal/api/web/internal/contributorreports/tier.go).

The role taxonomy

Seven org-level roles. They're SpiceDB relations, so a user can hold any combination on any org.

Role What it grants
owner Full org administration. Almost always the installer. Manages everything; usually exactly one per org.
sponsor Equivalent power to owner for operating purposes. The buyer's identity on a Business-tier setup. Both owner + sponsor satisfy the manage permission — the highest authority.
project_manager Org-level read + repo-level write across every repo the org covers. The "I orchestrate but don't own" role. Surface lives at /app/pm.
maintainer Repo-level read across the org's covered repos via the org#repo_reader cascade. Plus contributor:read on every contributor (does NOT grant contributor:write — that's mentorship-only).
billing_admin Org-level billing_view + member_manage. Finance can manage seats + see invoices without org-manage. The right grant for a finance lead who isn't an engineer.
auditor Org-level read + audit-log + usage-report read. No write. The right grant for compliance / SOC2 review.
member_manager Just member_manage. The narrowest grant — the user can invite + remove + change roles for this org but holds no read / write / integration_manage powers. Held by the installer (auto-granted) and by anyone they delegate to via the Members tab.

How they compose

Three patterns to recognise:

1. Single grant. A user gets auditor on org A. They can read the org's audit log + usage report; they can't write.

2. Multi-role on one org. A user gets maintainer + auditor. They get the union of permissions — read access across covered repos AND audit-log access. Still 1 seat.

3. Same role across multiple orgs. A user gets maintainer on three different orgs covered by the same sponsor. Still 1 seat (the user, not the relationships).

What owner vs sponsor actually differs on

In day-to-day operation, nothing. Both satisfy org#manage, which gates everything that isn't explicitly delegated elsewhere. The distinction is:

  • owner is the GitHub identity that installed the App. Auto-granted on installation.
  • sponsor is the identity that pays the bill. Granted explicitly when the buyer is not the installer (common in enterprise — IT installs, Finance pays).

A small team usually has one person with both. A larger team benefits from splitting them so the buyer's GitHub identity can change without re-installing.

Where roles cascade

Two cascades the schema codifies:

Org → repo (the most common case)

A user with org#sponsor / org#owner / org#project_manager holds repo#write on every repo the org covers, without needing per-repo grants. A user with org#maintainer / org#auditor / org#billing_admin holds repo#read on every covered repo via org#repo_reader.

This is the day-one setup that makes Steward feel "natural" — you grant maintainer once at the org level and everyone has read access where they need it.

Repo → child resources

A repo's evaluation, governance, cla child objects inherit through parent_repo->read / parent_repo->write / parent_repo->manage. So a maintainer with repo#read can read every evaluation, governance file, and Contributor License Agreement (CLA) on that repo for free.

There's one exception: the PR author has a direct evaluation#author relation, so a contributor can read their own PR's evaluation report even without any org or repo role.

Subtractive override (explicit_deny)

The schema's repo#explicit_deny lets you carve a single repo out of a user's cascade access. Use it when:

  • A repo contains code the org otherwise has access to, but one user shouldn't (a security-sensitive subsystem, an inherited legacy with an HR-flag attached).
  • You're testing access patterns and want to revoke without removing the underlying role.

Mechanic: even if parent_org->read would grant access, an explicit_deny: user row on the repo subtracts that user out.

This is rarely needed on day one. Mention it exists; reach for it when the precise need arises.

Granting + revoking via /sponsor/members

The surface lives at /sponsor/members. Per-org cards list current members with their role + a Source column:

Source Meaning
install The row matches app.installations.installed_by_user. The installer's auto-grant.
grant Everything else — org-admin webhook auto-grant, explicit GrantRole from this page, or a Members-tab delegation.

The per-org grant form lets you pick a GitHub login and a role. Owner is excluded from the default dropdown — every install already has one, and granting a second is rare (it's allowed, but typed manually).

Last-manager protection

You cannot revoke the last user with member_manage on an org. The backend returns FAILED_PRECONDITION and the form surfaces a help line telling you to grant the role to someone else first.

This guard exists for a reason: an org with zero member-managers becomes unmanageable through the UI; recovery requires DB-side intervention. The protection makes sure that never happens by accident.

Who can see this page

/sponsor/members checks org#member_manage server-side and silently drops orgs the caller can't manage from the per-org cards. So a member_manager on one of three orgs only sees that one org's card; they don't see permission-denied errors for the other two.

What's next

  • Tiers + credits — the tier's seat cap your member count gates against.
  • Audit + compliance (forthcoming in slice 4) — how every role-change shows up in the audit trail for Service Organization Control 2 (SOC2) review.
  • Set up your sponsorship — the first-time setup that produces the initial seat (the installer's member_manager).