Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Policy

Policy

Background work

On this page

Schedules and event subscribers are the work your app does without a request (Jobs and events). They run as the system caller, which gets exactly what Access.system grants. To let them read or change an entity's records, name Access.system in its lists, such as update: [owner_id, Access.system].

The system is nobody signed in, and it holds no membership. So:

  • a user field, a role and Access.authenticated never let it in;
  • a grant list, a field's own list or an actions: line lets it in only by naming Access.system.

Access.public doesn't let background work in

An entity grants background work explicitly. A schedule, a subscriber or a batch that reaches a statement only Access.public grants is a compile error, policy.system_grant_missing, in the same way that policy.effect_ungranted refuses such a route.

Which tenant background work reaches

On a tenant's records (Tenancy), a grant naming Access.system lets in:

  • a schedule's run in every tenant, since no tenant's event starts it;
  • a subscriber within the tenant its event names, in the event's field of the tenant's id type;
  • a batch's run within the tenant its saved values of that type name.

A subscriber or a batch whose value names no tenant reaches no tenant's records, only those every tenant shares. A value names no tenant when there is none, when it is none, or when its values name different tenants. So "no tenant" means every tenant for a schedule, which names none by design, and none for work that names its tenant by a value. Jobs and events covers this under "Background work and tenants".

The system never moves a record to another tenant, which needs a membership there, and never founds a tenant's first membership.

The system and actions: lines

A line under actions: that names Access.system lets the system in beside whoever else the line names. A line naming only Access.system still lets in no request.

No request is ever the system. Only the runtime's runner makes the system caller. Every request, and every call a test makes, with a caller or without one, runs as someone else.

Actions the system runs inside a request

The runtime runs three of your app's actions as the system, even inside a request:

  • the magic-link send: action (Magic links), delivered as a subscriber of jadpo.MagicLinkEmail with its delivery's tenant, which is none;
  • a webhook's reply action (Webhooks that need a reply);
  • the on_first_sign_in: action, which Identity runs for a person the app hasn't seen before.

None of them reaches a tenant's records, only those every tenant shares, since no tenant's event starts them and "no tenant" means every tenant only for a schedule. Each is let in only by Access.system. So when one of them reaches a grant, or a line under actions:, that names Access.public and not Access.system, that is policy.system_grant_missing, reported where the action is named.

Proving which tenant work is for

A subscriber or a batch's run works in the tenant a value names: the event's field of the tenant's id type, or what the run saves of it. A caller could pass the id of a tenant they don't belong to. So that value must come from a record of that tenant the action read or wrote under its caller's policy. Otherwise it is policy.event_tenant_unproved, reported at the value.

The usual fix is to read the tenant first, attempt Household.get(household_id), or to name the tenant of a record the action changes. In full, a value proves its tenant when it is one of these:

  • the tenant's own id, or the tenancy reference of one of the tenant's records, that the action, or an action or query it calls, read, created or changed. The tenancy reference is the field that ties a record to its tenant, such as company_id.
  • the same, of a record the action is handed whole, which counts as read. A route passes path placeholders, input and the caller, and a subscriber passes its event's fields. So only a body that read a record with the same caller, or a run limited to its tenant, can hand one to an action. For example, a batch's action invoice_round(rent_round: RentRound) names its run's society from rent_round.society_id.
  • the value a create sets a record's tenancy reference from, when the create runs whenever the action does. The create needs the caller's membership in that tenant, and the action fails with it. A create inside an if, a match or a loop, or after a return that may leave first, proves nothing.

Anything else is refused, including a parameter that holds an id. Only where the value came from counts, not what decided it.

The check runs in the action whose emit or batches.each uses the value, never at its callers. An action that emits from a parameter is refused even when every action calling it passes a proved id, since a route may call it too. Pass it the record instead. The same rule holds for every action, so a subscriber's action that passes its event's tenant on, to another emit or a batch, reads a record of that tenant first, as Declaring an event advises for anything more than ids.

What a batch's run stores, from its items, what it saved and its summary, is proved as a call's arguments are, by the tenant the run is limited to. What batches.status gives is held to what decides every batch's failures. Jobs and events covers this under "What a run reaches under policy".

Markdown for agents: /docs/background-work.md.Built from Jadpo main at a707efb.