Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferencePolicy

APIsPolicy

A membership role

On this page

Use a role when people get access by belonging to something, such as the editors of a company. A role is stored in a record of its own: a membership entity, whose membership: section names three of its fields:

  • scope: the record the role is held in, such as the company;
  • member: who holds it, a reference to the user entity;
  • role: the field that holds the role, typed with an enum.

The role enum lives beside the membership in the same domain, and other domains use it by its full name. There is no global roles file.

domains/accounts/types/company_role.jadpo
// domains/accounts/types/company_role.jadpotype CompanyRole = Enum {    owner    editor    viewer}// domains/accounts/entities/company_membership.jadpoentity CompanyMembership {    id: Uuid    company_id: Company.id {        on_delete: OnDelete.cascade    }    user_id: User.id {        on_delete: OnDelete.cascade    }    role: CompanyRole    membership: {        scope: company_id        member: user_id        role: role    }    policy: {        create: [CompanyRole.owner]        read: [CompanyRole.owner, user_id]        update: [CompanyRole.owner]        delete: [CompanyRole.owner, user_id]    }}

Here only a company's owners may add, change or remove members. Each member may also read their own membership, and delete it to leave.

Using a role on other records

A role applies to another entity through that entity's reference to the scope, the same reference tenancy uses. A Project with a company_id takes its grants from the roles held in its company:

domains/projects/entities/project.jadpo
// domains/projects/entities/project.jadpoentity Project {    id: Uuid    company_id: Company.id {        on_delete: OnDelete.cascade    }    name: ProjectName    policy: {        create: [CompanyRole.editor]        read: [CompanyRole.viewer, CompanyRole.editor]        update: [CompanyRole.editor]        delete: [CompanyRole.owner]    }}

How roles combine

  • Roles have no ranking. A role lets in only where a list names it, so an owner who may also edit is listed under both.
  • Only current memberships count. A membership its lifecycle hides grants nothing, and removing one takes effect on the next request.
  • A role held across the whole application, such as staff, has a membership with scope: MembershipScope.application.
  • An entity can have two references to the scope, such as a handover with a from_team_id and a to_team_id. Then each grant names the reference it means: TeamRole.lead(from_team_id) is the lead of the from-team.

Roles that need a way in

Staff often sign in through their company's provider, such as Google or Microsoft, so that switching their account off there ends their access everywhere. If they could also sign in by magic link and keep their roles, it wouldn't. A membership's requires: says, for each role, how its holder must have signed in for the role to count:

domains/desk/entities/staff_member.jadpo
// domains/desk/entities/staff_member.jadpoentity StaffMember {    id: Uuid    patron_id: Patron.id {        on_delete: OnDelete.cascade    }    role: StaffRole    membership: {        scope: MembershipScope.application        member: patron_id        role: role        requires: {            StaffRole.admin: {                sign_in: [SignInMethod.google]                strength: AuthenticationStrength.multi_factor            }            StaffRole.front_desk: {                sign_in: [SignInMethod.google]            }        }    }}
  • sign_in: lists the ways in, any of which will do: a provider that sign_in: { providers: } names (Providers), or SignInMethod.external_token. A magic link or a passkey proves an address or a device, not that the company still vouches for the person, so neither can be listed.
  • strength: AuthenticationStrength.multi_factor asks for a second factor: a passkey, or a provider whose ID token says the person used one (How strongly a person signed in).
  • StaffRole.front_desk: {} needs nothing. Once one role has a line, every value of the enum has one, so a role added later is decided rather than open by default.

A role counts on a request only when the caller's current session meets every line of its block. Otherwise the person is just themselves, without that role: Ann, signed in by link, is a customer like any other. Nothing is refused when she signs in. The requirement applies wherever the role is named: grants, a field's own policy:, lines under actions:, listed queries and what the proofs count on. You write it once.

A membership whose role doesn't count doesn't make its holder a member of its tenant for that request either, so tenancy fails closed (Tenancy).

A session started through a provider that any role's requirement names lasts a day, not 30 days, so a person the company switches off loses the role within a day (How a session started).

The permissions report lists each role's requirement, and access.jadpo_record records it. Any change to a role's requirement, tighter or looser, and adding or removing a role's line, needs a person's acceptance, PolicyChange.role_requirements_changed (Acceptances).

What the compiler checks:

  • A role or a way in the enum doesn't have is type.unknown_enum_variant.
  • A role left out once another has a line is policy.requirements_incomplete.
  • Anything else a role's line can't hold, an empty sign_in:, strength: AuthenticationStrength.primary, a way Jadpo's own sign-in gives, or a role given twice is policy.requirement_invalid.
  • A line no way the app declares could ever meet, such as SignInMethod.external_token without external_token:, is policy.requirement_unsatisfiable.
  • A route whose API can't carry the sign-in that every role letting anyone in needs is policy.effect_ungranted, which names the requirement. A provider's sign-in comes as a browser session and external_token's as its token; an API token or a service key carries neither.

A route test says how its user signed in with sign_in: and strength: beside as:; without them, as: is someone who signed in by magic link with one factor (Signing in for a route test).

Markdown for agents: /docs/a-membership-role.md.Built from Jadpo main at aee2e8f.