Permissions

The access register in manifest.json — the nine kinds of grant an App can declare, how an entry is shaped, deny-by-default, install-time consent, and what changes when you add a grant.

An App gets no access by default. Everything it may reach — its own tables, another app's capabilities, an external host — is one entry in manifest.permissions. That array is the app's access register: a single declarative list, read by the platform to enforce access at runtime and read by the merchant at install time as the consent screen.

There is no policies block. If you have seen one in an older example or a generated file, it is wrong — no shipped app has one, and the manifest schema rejects the key.

manifest.json
"permissions": [
  { "entity": "device_serials", "access": ["read", "create", "update"] },
  { "identity": "users", "access": ["read"] },
  { "capability": "orders.get", "compatible": "^1.0" },
  { "outbound": "api.acme-service.example", "paths": ["/v2/*"] }
]

Deny by default

There is nothing to switch off, because nothing is on. An app with no permissions array can read none of its own tables, call no other app, and reach no external host — its generated data client does not even have the methods.

That is worth understanding as a design property rather than a chore. The register is the complete, machine-readable answer to "what can this app touch?", and it is complete because there is no other way in.

How an entry is shaped

Each entry carries exactly one resource key. That key both names the kind of grant and carries its target. An entry with two resource keys, or with a key that is not one of the nine, fails validation.

Resource keyGrants access toExtra fieldsStatus
entityOne of the app's own entitiesaccess: [read, create, update, delete]Shipped
identityThe tenant's users, teams or sessionsaccess: [read, write]Shipped
capabilityAnother app's capability, through the gatewaycompatible (optional)Shipped
outboundAn external HTTP hostpaths (optional)Declared; enforcement being built
storageA file bucketaccess: [read, write]Not yet proven
aiA model or model familyNot yet proven
mailA sending domainNot yet proven
secretA secret pathaccess: [read]Not yet proven
inboundAn inbound webhook sourceNot yet proven

The first three are shipped and exercised by every production app. outbound is declared and honoured as intent, with enforcement still being built. The last five are declarable today but no shipped app uses them — treat them as unproven and expect the details to firm up.

One page per kind:

The register is read by a human

At install time the merchant sees this list rendered in plain language, and decides whether to say yes. That is the single most useful thing to know about the file: it is not a configuration detail, it is the text of an agreement.

Two consequences:

An entry your app never exercises makes the decision harder for no benefit. It also widens your blast radius for nothing. Claim the least you can.

A reason written for a human beats a permission written for a machine. billing.json's listing.scopes is where you explain why — "link a registered serial to the order that sold it", not "requires read on orders". See Consent.

Grant per operation, not per app

["read", "create"] on a ledger entity is a real safety property: the generated client has no update and no delete, so a bug cannot erase history. ["read", "create", "update", "delete"] everywhere throws that away and tells the merchant nothing.

The asymmetry is the useful part. Look at what each entity is for before you grant on it:

The entity isGrant
An append-only log or ledgerread, create
Reference data your app maintainsread, create, update
Rows an operator genuinely deletesall four
Something you only ever read fromread

Review the register on every upgrade. A new grant is a new thing the merchant is consenting to, and adding one is not a patch-level change in spirit, whatever the version number says.

The failure mode is gradual: a grant added for a feature that shipped, a host added for a vendor you no longer use, an identity grant left behind after the code that needed it was removed. Six versions later the consent screen describes an app that does not exist, and a merchant reviewing it cannot tell which entries are load-bearing.

So treat the register the way you treat a dependency list:

  • When you add a grant, say so in the release notes. A merchant who is asked to re-consent deserves to know what changed.
  • When you remove the code, remove the grant. In the same version.
  • Before a major release, read the whole array and justify every line. If you cannot say which route or handler needs an entry, delete it and see what breaks in staging.

See Consent for what the merchant actually sees, and when they are asked again.

What permissions is not

Three neighbouring things are easy to confuse with this one, and all three point the other way.

provides_permissions declares what a person may do — permission keys this app defines and the gateway enforces against the acting principal. permissions is app-to-resource; provides_permissions is person-to-operation. See Person permissions.

provides_scopes declares a scope dimension this app supplies to the platform, so other apps can slice their rows by it. See Scoping.

A Cockpit view's own permissions array["device_serials.read"], ["device_serials.write"] — controls whether a Cockpit user sees the view. That is a UI gate on the operator, not a grant to your app. See How the Cockpit renders.

Next steps

Was this page helpful?