// platform.permissions

Permissions

The single reference for who can do what — human roles, agent scopes, per-resource visibility, and how they compose.

The model in one paragraph

Every action in BoardHerald passes up to three checks: (1) does the human role of the actor permit the action at all, (2) if the action came via an agent, does the API key scope include it, and (3) for resources with per-item visibility (today just financials), is this specific object visible to the actor? All three are enforced server-side. Failure on any one is a 403.

Layer 1: Human roles

Every user in a tenant holds exactly one role. The role sets the ceiling for everything that follows — an API key issued by a MEMBER can never exceed a MEMBER's reach, no matter which scopes are checked on it.

Roles are scoped per tenant. The same email can be an ADMIN in one organization and a MEMBER in another — each tenant tracks its own role for you, and switching organizations via the tenant switcherre-resolves your role from the destination. A role badge in one tenant carries zero authority into another.

Role definitions

RoleTypical holderScope
ADMINFounder, CEO, CFO, executive assistantFull read + write on every surface. Can invite, promote, demote, rotate NDA, see audit.
MEMBERBoard member, investorRead all in-scope content, vote on resolutions, acknowledge updates, RSVP meetings.
OBSERVERAdvisor, early investor with observer seatRead-only. Cannot vote, doesn't count toward quorum.

Human permissions matrix

Every major action, row by row, per role:

ActionADMINMEMBEROBSERVER
Updates — read
Updates — publish / edit
Resolutions — read
Resolutions — draft / open
Resolutions — vote
Meetings — read / RSVP
Meetings — schedule / minute
Financials — read (visibility-gated)
Financials — enter / restate
Users — invite / promote / demote
NDA — manage / rotate
Audit log — read
Branding / white-label
API keys — issue / revoke✓ (own + tenant-wide)self only

Layer 2: Agent scopes

API keys carry a set of fine-grained scope strings. Every MCP tool declares a required scope and fails closed with a 403 if the key doesn't carry it. The scope check is independent of the human role check — both have to pass.

Naming convention

Scopes follow the shape <resource>:<action>. Resources are top-level domains (updates, meetings, financials, etc.); actions are one of read, write, or a resource-specific verb (vote for resolutions, manage for users).

Complete scope reference

Every scope the server recognises today. The tools column lists the MCP tool names that scope unlocks — see Agents → Capabilities for what each tool does.

ScopeTools coveredNotes
audit:readaudit_listSensitive — exposes who-did-what across the whole tenant. Never bundled into built-in personas below full admin.
categories:readcategories_listTenant-defined tag / category metadata used to organise updates and resolutions.
financials:readfinancials_list, financials_get, kpis_list, kpis_get, kpis_sparklineCovers both financial updates (encrypted HTML reports) and the KPI surface. Respects per-KPI visibility (Layer 3 below).
financials:writefinancials_create, financials_publish, financials_update, financials_delete, kpis_create, kpis_update, kpis_delete, kpis_record_period, kpis_restate, kpis_target_create, kpis_target_update, kpis_target_deleteAuthor financials, record and restate KPI periods, manage time-varying targets. Role ceiling still applies — a MEMBER key with financials:write still can't write (not a MEMBER action).
kpis:webhookkpis_webhook_ingestNarrow transport scope for sync connectors. POST a single period value per call via /api/kpis/webhook; identify the KPI by id or by (sourceSystem, externalId). Separate from financials:write so a sync key can't rewrite KPI metadata. See Agents → Sync KPIs.
meetings:readmeetings_list, meetings_get
meetings:writemeetings_create, meetings_publish, meetings_update, meetings_cancelSchedule, amend, cancel.
notifications:readnotifications_listAgent-visible notification feed for the issuing human.
notifications:writenotifications_mark_readMark items read on the human's behalf.
resolutions:readresolutions_list, resolutions_get
resolutions:voteresolutions_voteDeliberately separate from write — an agent can vote on the human's behalf without being able to author new resolutions.
resolutions:writeresolutions_create, resolutions_close, resolutions_update
search:readsearch_queryFull-text search across updates, resolutions, meetings. Results still filtered by the issuing human's visibility.
updates:readupdates_list, updates_get
updates:writeupdates_create, updates_publish, updates_update, updates_deleteDraft, publish, amend, retract.
users:readusers_list, users_getList members and inspect their profiles.
users:writeusers_inviteInvite new members. Role ceiling applies — only ADMIN keys can meaningfully use this.
users:manageusers_update, users_disable, users_grant_access, users_revoke_accessRole changes + access grants + disable. ADMIN-only in practice. Both grant/revoke tools take a group parameter accepting FINANCE or INVESTOR; users_invite also takes optional grantFinance + grantInvestor booleans.
*(every tool)Admin escape. Bypasses every scope check. Use sparingly — if a key only needs to read updates, don't mint it with *.
Empty scope list = legacy full access
Keys created before fine-grained scopes shipped have an empty scopes array, which the server treats as full access for backwards compatibility. Re-issue these keys with explicit scopes when you can.

Layer 3: Per-resource visibility

Some resources have an additional visibility layer on top of role + scope. Today this applies only to financials — individual KPIs carry a visibility setting independent of the base surface permission.

Financials — KPI visibility

Each KPI is tagged with one of three visibility classes (configured by an ADMIN on the KPI itself):

  • Full-board — visible to every member who has financials:read at the role level. MRR, runway, headcount typically live here.
  • Investor-only — visible only to members who hold the INVESTOR access group. Use for metrics the investor audience on the board should see but not the rest (ownership percentages, option pool remaining, fundraise-round size, and similar).
  • Leadership-only — visible only to ADMINs plus an explicit allow-list of members. Individual comp breakdowns, multi-scenario runway projections.

Visibility is enforced server-side — a member who shouldn't see a KPI doesn't get it in the API response at all. Same for agents: a key with financials:read sees exactly the KPIs the issuing human can see, never more.

Visibility ≠ encryption
Visibility hides data from members who lack permission. It does NOT prevent an ADMIN (who has access to every KPI) from reading it. If something is truly need-to-know, don't enter it into a shared tenant — use a narrower tenant or keep it out of the system.

How roles and scopes combine

For an agent tool call to succeed, all of the following must be true:

  1. The human user who owns the API key has a role that permits the action (per the matrix above).
  2. The human has signed the current active NDA (see NDA).
  3. The API key carries the scope that the tool declares as required.
  4. If the target resource has per-item visibility (financials today), the human is on the visibility allow-list.

Failure on any step returns a 403 with a message identifying which check failed:

  • role_insufficient — the role can't do this, regardless of scope.
  • nda_required — human hasn't signed the current NDA.
  • scope_missing — role would permit it, but the key's scopes don't include the required one.
  • visibility_denied — scope check passed, but this specific item isn't visible to this human.

See Agents → Troubleshooting for error-code specifics.

Built-in agent personas

For convenience, the API-keys UI offers four pre-composed persona bundles. They're scope lists, not roles — the role ceiling of the issuing human still applies. Pick the persona that matches the job; every persona is a subset of what an ADMIN human could do themselves.

See the full table with scopes on Agents → Capabilities. Summary:

  • read-only — brief/query only; can't write anything.
  • meeting secretary — schedule, draft agendas, capture minutes.
  • board secretary — drafts and routes resolutions, casts recorded votes, posts updates.
  • full admin — full write surface plus user and audit visibility. Grant sparingly.
Custom scope sets
You don't have to use a pre-composed persona. The /admin/api-keys create form lets an ADMIN tick individual scope checkboxes, so you can mint a key with exactly "updates:read + financials:read" — or any combination — for a narrow-purpose agent.

Managing permissions in practice

Promoting / demoting

Any ADMIN can change another user's role. The exception: demoting yourself requires a second ADMIN to confirm — the platform won't let the last remaining ADMIN demote themselves and leave the tenant locked. Every role change writes a role.changed event to the audit log.

Issuing an API key

Humans issue their own keys under Profile → API Keys. The UI surfaces the four personas plus a custom-scopes option. Key values show exactly once — store them in a secret manager.

Revoking

A human can revoke any key they own. An ADMIN can revoke any key in the tenant (Settings → API Keys). Revocation is immediate — in-flight tool calls using the revoked key fail with key_revoked on their next round trip.

Auditing

Every role change, key issuance, key revocation, and tool call is in the audit log with actor + timestamp + IP. Filter by action prefix role.* / key.* / tool.* to scope the view.