# Permissions (high level)

Atrium app developer spec, section 8: Permissions (high level).

At connect, COHO mints a **background app token** — a machine principal credential (`AppConnection`) delivered once in `lifecycle.setup` as `apiKey`. It is a Public API key backed by a private role holding exactly the requested catalogue scopes, and it is revoked on disconnect. It is **not** a fake manager user (`ATRIUM_USER` is not the model). Schedules and other unattended work use this key with no manager signed in.

- Declare `requestedCapabilities` as catalogue scope ids (e.g. `conversations.write`, `tenants.read`). Legacy short names (`conversations`, `settlements`, …) still expand.
- Connect fails if the connecting person cannot grant every resolved scope. The credential is minted with exactly that set.
- The Store shows requested, granted, pending, and unsupported scopes.
- A manifest update never expands an existing key automatically. Existing grants keep working and newly requested calls return `403` until a manager who can grant those scopes reviews and approves the exact additive delta in Store.
- Approval updates the existing key's role in place, is audited, and takes effect on the next API request. It does not rotate credentials or rerun `lifecycle.setup`. Disconnect/connect remains the credential-rotation path.
- Scope removal is not automatic. A future explicit downgrade/revoke flow owns that case.

**Interactive open (quick view / external configure):** COHO also mints a short-lived **usage JWT** (`atriumUsageToken`) bound to the signed-in manager, organisation, and connection. Public API accepts that Bearer as well as the connection API key Guid. On each usage-token request, effective scopes are recomputed as **connection scopes ∩ person scopes** (so a restricted manager cannot outrun their role through an org-installed app). While a quick view stays open, COHO remints via the postMessage bridge (`atrium.usageToken`) so interactive sessions can last as long as the manager’s COHO session — apps must swap the Bearer; they cannot mint. Background triggers keep using the connection `apiKey` only.

**Install scope:** Connect can be organisation-wide (default) or **personal** (installer only). Organisation installs may also set a person allow-list; when the allow-list is empty, any member with Store access can open the app. Personal installs keep the org background `apiKey` for schedules/webhooks, but only the installing manager appears in My apps and can open the interactive UI. That is the dual-credential model: background org key + interactive person usage JWT.

**COHO staff signal:** When the opening manager is a COHO super-admin, the usage JWT and Public API principal include claim `isCohoStaff=true`. This is a staff-only UX signal for in-app admin tools — not a grantable catalogue scope, and never present on the machine `apiKey`.

You cannot outrun the connecting manager's organisation permissions on connect, and you cannot outrun the *opening* manager's permissions on interactive usage.

Managers can review which Public API routes an install has called with its background connection key under **My apps → Data access** (last 30 days).

## Scope ids for common Public API routes

Missing scope returns **403**. Stacked `[RequirePublicApiScope]` attributes on one action **OR**. Authoritative per-action attributes live on the V11 controllers; Scalar lists required scopes on each operation (description + `x-required-scopes`).

| Route | Scope(s) |
|---|---|
| `GET /conversations`, `GET /conversations/{reference}`, `GET /conversations/{reference}/messages`, `GET /conversations/recipients`, `GET /conversation-templates`, `GET /conversation-merge-tags` | `conversations.read` |
| `POST /conversations`, `POST /conversations/{reference}/messages`, `PATCH /conversations/{reference}/messages/{messageGuid}`, `POST /conversations/{reference}/read`, `DELETE /conversations/{reference}/messages/{messageGuid}` | `conversations.write` |
| `GET /organisation/credits` | `organisation.read` **or** `conversations.read` |
| `POST /organisation/sms-credit-quote` | `conversations.read` |
| `GET /maintenance`, `GET /maintenance/categories` | `maintenance.read` |
| `POST /maintenance`, `POST /maintenance/convert-from-conversation` | `maintenance.write` (convert also requires `conversations.write` in-action) |
| `POST /actions` | `actions.write` |
| `GET /calendar/items` | `calendar.read` |
| `GET /calendar/maintenance-issues` | `maintenance.read` |
| `GET /properties`, `GET /properties/{propertyReference}/info-boxes/overview` | `properties.read` / `info_boxes.read` (info-boxes) |
| `GET /properties/{propertyReference}/tenants` | `tenants.read` |
| `GET /tenancies/{guid}` | `tenancies.read` |
| `GET /rent-due-records` | (core Public API key; rent payment writes need `rent_collection.write`) |
| `METHOD /apps/{appReference}/{operationName}` (app operations gateway, §3.5) | `apps.operations` |

**Living full catalogue:** atrium-requested-capabilities.md lists every grantable scope id, group, and label. The Store guided manifest editor uses the same catalogue (Read/Write by area). Prefer catalogue ids over legacy short names. That markdown file is CI-checked against `CohoScopes` so new scopes show up there when they ship.

## Messaging notes for apps

- Create mediums: `COHO_MESSENGER` (default), `EMAIL`, `SMS`, `WHATSAPP`. Email and SMS are broadcasts (`AllowReplies` false). SMS send prices itself (segments + credit debit). WhatsApp requires an active org WhatsApp connection.
- WhatsApp is billed by Meta to the landlord (no COHO credit balance). COHO credits (SMS and other types) are on `GET /organisation/credits` (optional `?type=SMS`). Optional SMS cost preview: `POST /organisation/sms-credit-quote` with telephone numbers and `body` (COHO returns `numSmsSegments` and `creditsRequired`).
- Tenant `messagingRestricted` is exposed on tenants and conversation recipients. Create and sent replies refuse restricted housemates (maintenance threads stay writable).
- Drafts: `POST .../messages` with `status: draft` returns 201. List with `includeDrafts=true`. Delete with `DELETE .../messages/{messageGuid}` (drafts only). Promote/send a draft with `PATCH .../messages/{messageGuid}` (`status: sent`, optional body update). Drafts do not bump thread activity until promoted and stay out of the default Sent-only list.
