Entity access
Which of your own tables the app may touch, and how. Shipped, and the grant every app writes first.
{ "entity": "device_serials", "access": ["read", "create", "update"] }
| Access | Allows |
|---|---|
read | Reading rows |
create | Inserting rows |
update | Updating rows |
delete | Deleting rows |
Declare one entry per entity in your schema.json, and grant only what the app actually does.
The grant shapes the generated client
This is the part that makes the grant more than paperwork. revenexx apps generate reads manifest.json alongside schema.json, and only generates the methods you granted:
| Granted | The client has |
|---|---|
read | list, page, get |
create | create |
update | update |
delete | delete |
{ "entity": "audit_entries", "access": ["read", "create"] }
await db.audit_entries.create({ … }); // fine
await db.audit_entries.update(id, { … }); // TypeError: not a function
So the boundary is enforced in the types, not only at runtime. An append-only ledger cannot be rewritten by a bug, a bad merge, or a Cockpit action, because there is no method to call. That is a real safety property and it costs you one array.
Widen the grant, regenerate, and the methods appear:
revenexx apps generate
Forgetting that step is the usual explanation for db.x.update is not a function right after you edited the manifest.
Grant per operation
The asymmetry is the useful part. Read each entity's purpose before you grant on it:
| The entity is | Grant |
|---|---|
| An append-only log, ledger or audit trail | read, create |
| Reference data your app maintains | read, create, update |
| Rows an operator genuinely deletes | all four |
| Something you only read | read |
["read", "create", "update", "delete"] on everything is the shape to avoid: it tells the merchant nothing, and it removes the one guard that costs you nothing to keep.
It stops at the app boundary
An entity grant is not how you reach another app's data. You cannot name another app's entity here, and there is no grant that would let you. Foreign keys and grants both stop at the app boundary — the way in is that app's capabilities:
"permissions": [
{ "entity": "device_serials", "access": ["read", "create", "update"] },
{ "capability": "products.get", "compatible": "^1.0" }
]
See Calling another app and Relationships.
What it does not do
It is not tenant isolation. Scoping to the calling tenant is unconditional and applies whatever you grant — see Tenant isolation.
It is not a public route. A grant lets your code touch the table. Whether anything outside can is a capability, and mountCrud's only option is a second, independent fence — see CRUD.
It is not an operator permission. Whether this Cockpit user may edit a row is the view's own permissions array and provides_permissions, not this.
The three fences compose, and it is worth setting all of them deliberately for an entity that matters:
manifest.permissions → what your code may do at all
mountCrud only → what is reachable over the public API
cockpit view permissions → what an operator sees and can act on
Next steps
- The typed client — the methods a grant produces.
- Adapters — the same gating under every adapter.
- Permissions — the rest of the register.
- CRUD — publishing these operations as routes.