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

When policy says no

Your code never handles a policy refusal itself. The compiler adds policy to the SQL WHERE of every query and write, and indexes every field a policy condition filters on. jadpo sql shows the policy each statement carries. What a refused caller sees depends on what they asked for:

Situation The client sees
Not signed in, on a route that needs it 401 unauthenticated (Routes: signed in by default)
A record the caller may not see, read or changed by id the code's own not_found failure, as if it did not exist
A list only the records the caller may see, filtered before paging
A create, or an action they may not call 403 not_permitted

A hidden record reports the same not_found failure the code already maps, so a caller can't tell it from one that doesn't exist. The log records that policy concealed it (Concealing records).

not_permitted is raised by the runtime and never listed in fails. The generated API description lists the 403 wherever it can happen.

Writes are checked in the same statement as the change, so a membership removed a moment earlier can't slip through.

A token issued to another application (Identity) carries scopes, which say what that application may do. Scopes only narrow: the token may do what both the person's grants and its scopes allow.

Policy, end to end

A whole application whose customers are companies. Members sign up, found a company and give each other roles in it, and a company's projects are seen and changed only by its members.

First, the principal, its one sign-in method, and root policy.jadpo. The policy names the tenant, and the one action that needs other people than its entities' grants: founding a company, since a company with no members yet has no owner to grant anything:

example
use work/entities { Member }application {}principal {    user: {        subject: Member.handle    }}
example
use work/entities { Company }use workpolicy {    tenant: Company    actions: {        work.found_company: [Access.authenticated]    }}

A member may read only their own record, and anyone may sign up. Members are shared by every company. The membership says where its role is held, who holds it and which field holds it. A company's records follow the roles held in it:

example
use work/types { Handle }/// Someone who signs in.entity Member {    id: Uuid    handle: Handle {        unique: true    }    policy: {        create: [Access.public]        read: [id]        tenant: Tenancy.shared    }}
example
use work/types { CompanyName, CompanyRole }/// A customer: the tenant every other record belongs to.entity Company {    id: Uuid    name: CompanyName    policy: {        read: [CompanyRole.owner, CompanyRole.editor, CompanyRole.viewer]    }}
example
use work/entities { Company, Member }use work/types { CompanyRole }/// A member's role in a company.entity CompanyMembership {    id: Uuid    company_id: Company.id {        on_delete: OnDelete.cascade    }    member_id: Member.id {        on_delete: OnDelete.cascade    }    role: CompanyRole    membership: {        scope: company_id        member: member_id        role: role    }    policy: {        create: [CompanyRole.owner]        read: [CompanyRole.owner, member_id]        delete: [CompanyRole.owner, member_id]    }}
example
use work/entities { Company }use work/types { CompanyRole, ProjectName }/// A piece of work in a company.entity Project {    id: Uuid    company_id: Company.id {        on_delete: OnDelete.cascade    }    name: ProjectName    policy: {        create: [CompanyRole.owner, CompanyRole.editor]        read: [CompanyRole.owner, CompanyRole.editor, CompanyRole.viewer]        update: [CompanyRole.owner, CompanyRole.editor]        delete: [CompanyRole.owner]    }}

Founding a company is the listed action: anyone signed in may call it, and it makes its founder the owner. Adding a member is an ordinary create of a membership, so only the company's owners may do it. The code says nothing about who may do either:

example
use work/entities { Company, CompanyMembership }use work/types { CompanyName, CompanyRole }/// Starts a company with its founder as its owner.action found_company(name: CompanyName, founder: Principal.user) -> Company {    var company: Company = Company.create {        set: {            name: name        }    }    var owner: CompanyMembership = CompanyMembership.create {        set: {            company_id: company.id            member_id: founder.id            role: CompanyRole.owner        }    }    assert owner.role == CompanyRole.owner    return company}
example
use work/entities { Company, CompanyMembership, CompanyNotFound, Member, MemberNotFound }use work/types { CompanyRole }/// Gives a member a role in a company; only its owners may.action add_member(    company_id: Company.id,    member_id: Member.id,    role: CompanyRole) fails CompanyNotFound, MemberNotFound -> CompanyMembership {    return attempt CompanyMembership.create {        set: {            company_id: company_id            member_id: member_id            role: role        }    }}

The query names no caller and no company check: policy adds both to its WHERE, so a caller sees only the projects of companies where they hold a role that may read them:

example
use work/entities { Company, Project }use work/types { ProjectView }/// A company's projects, by name.query company_projects(company_id: Company.id) -> List<ProjectView> {    from: Project    where: Project.company_id == company_id    order: [Project.name asc]    limit: 100}
example
use work/entities { Company, Project }use work/types { ProjectView }use work/actions { RenameProject, StartProject, rename_project, start_project }use work/queries { company_projects }/// A company's projects.routes Projects {    prefix: /projects    /// Starts a project.    POST / {        input: StartProject        output: ProjectView        status: SuccessStatus.created        run: start_project(input)    }    /// A company's projects the caller may see.    GET /{company_id: Company.id} {        output: List<ProjectView>        run: company_projects(company_id)    }    /// Renames a project.    PATCH /{project_id: Project.id} {        input: RenameProject        output: ProjectView        run: rename_project(project_id, input)    }}

A route test signs in as each member, so what it checks is exactly what a browser would see. The test sets up its data by calling actions without a caller, which runs them without policy. found_company takes its founder, so it runs as the founder and its grants apply:

example
use work/entities { Company, CompanyMembership, Member }use work/types { CompanyRole, ProjectView }use work/actions { RenameProject, StartProject, add_member, found_company, register, start_project }test "a company's projects are listed only to its members" {    var ann: Member = attempt register("ann")    var bob: Member = attempt register("bob")    var cy: Member = attempt register("cyd")    var northwind: Company = found_company("Northwind", Principal.user(ann))    var viewer: CompanyMembership = attempt add_member(northwind.id, bob.id, CompanyRole.viewer)    assert viewer.role == CompanyRole.viewer    var launch: ProjectView = attempt start_project(        StartProject {            company_id: northwind.id            name: "Launch"        }    )    var seen: Response = request {        method: HttpMethod.get        path: "/projects/{company_id}"        params: {            company_id: northwind.id        }        as: Principal.user(bob)    }    assert seen.status == 200    assert seen.output == [launch]    var outsider: Response = request {        method: HttpMethod.get        path: "/projects/{company_id}"        params: {            company_id: northwind.id        }        as: Principal.user(cy)    }    assert outsider.output == []}test "a viewer can't rename a project, which reads as missing" {    var ann: Member = attempt register("ann")    var bob: Member = attempt register("bob")    var northwind: Company = found_company("Northwind", Principal.user(ann))    var viewer: CompanyMembership = attempt add_member(northwind.id, bob.id, CompanyRole.viewer)    assert viewer.role == CompanyRole.viewer    var launch: ProjectView = attempt start_project(        StartProject {            company_id: northwind.id            name: "Launch"        }    )    var by_bob: Response = request {        method: HttpMethod.patch        path: "/projects/{project_id}"        params: {            project_id: launch.id        }        as: Principal.user(bob)        input: RenameProject {            name: "Plans"        }    }    assert by_bob.status == 404    assert by_bob.code == "project_not_found"    var by_ann: Response = request {        method: HttpMethod.patch        path: "/projects/{project_id}"        params: {            project_id: launch.id        }        as: Principal.user(ann)        input: RenameProject {            name: "Plans"        }    }    assert by_ann.status == 200}test "someone outside a company can't start a project in it" {    var ann: Member = attempt register("ann")    var cyd: Member = attempt register("cyd")    var northwind: Company = found_company("Northwind", Principal.user(ann))    var response: Response = request {        method: HttpMethod.post        path: "/projects"        as: Principal.user(cyd)        input: StartProject {            company_id: northwind.id            name: "Takeover"        }    }    assert response.status == 403    assert response.code == "not_permitted"}

Markdown for agents: /docs/when-policy-says-no.md.Built from Jadpo main at a707efb.