foir-io/demesne

An RLS-compiled ReBAC + topology authorization engine — one spec → Postgres RLS + an app PDP

7

stars

233

commits

Go

primary language

Aug 29, 2026

updated

README

Demesne, Zanzibar-style authz framework compiled to RLS

Demesne


Write your authorization rules once, in a single spec file. Demesne compiles them into Postgres Row-Level Security, so the database enforces access on every query — a forgotten WHERE clause, a background job, or an ad-hoc psql session can't reach data the rules forbid.

It takes the idea behind Google's Zanzibar — a declarative schema of who-relates-to-what — but skips the separate authorization service. There's no Check API to call, no second datastore to keep in sync, no consistency tokens. The policy lives in the one place it can't be bypassed: the data path.

The problem

Authorization usually lives in application code — a service you call, or if checks spread across handlers. Both only protect the paths that remember to ask. Miss one and the rule isn't there.

Demesne moves the decision into Postgres, so access is a property of the data rather than a step in the request. One .demesne file compiles to two layers:

  • Row-Level Security — the enforcement floor. Demesne generates the policies and the trusted SECURITY DEFINER functions they call. Every query is filtered by the same rules, whether it comes from your app, a cron job, or a database console.
  • A verb gate — for actions RLS can't see. Some permissions aren't about rows ("can this user publish?"). For those, Demesne generates a Go and TypeScript capability map you check at the request boundary.

The same spec also produces the JWT claims your sessions carry. Change the spec, regenerate, and the database floor and the application code move together — nothing to hand-write and keep in sync.

How it works

A spec describes four things:

  • a topology — your tenancy shape, e.g. tenant → project;
  • the subjects that act — users, customers, staff;
  • the objects they act on — your tables;
  • the relations and permissions that connect them — ownership, roles, sharing, group membership.

From those, Demesne emits the RLS policies, the SECURITY DEFINER kernel, the verb-gate map, and the claims contract. Every trusted function is generated, so there's no opaque hand-written SQL to audit. CI byte-compares each generated artifact against a committed golden file, and asserts that every Can<Verb> point-check inlines the matching policy's USING clause verbatim — so the application surface cannot drift from the floor without failing the build. Set $DEMESNE_PG_URL and the suite additionally installs the emitted kernel in a real Postgres and checks the SQL agrees with the Go and TypeScript resolvers case for case. For a database you have already deployed to, demesne diff <spec> <dsn> --exit-code reports drift between the spec and the live policies.

import "github.com/foir-io/demesne"

spec, _ := demesne.Parse(src)      // text → AST
demesne.Validate(spec)             // static checks

rls, _    := spec.EmitRLS()        // Postgres RLS policies
pdp, _    := spec.EmitPDP()        // Go capability map
defs, _   := spec.EmitDefiners()   // the SECURITY DEFINER kernel
claims, _ := spec.ClaimsContract() // the JWT claims the policies read

Adopting Demesne on an existing database is a short loop: introspect the schema, scaffold a starter spec, edit it, emit the SQL, apply it as a migration. GUIDE.md walks through it end to end.

The spec language

examples/example.demesne is a complete worked spec — a small document app. The building blocks:

BlockDeclares
topologythe tenancy hierarchy; a virtual root sits above tenancy
vocabularypermissions, presets, and a rank ladder
rolestorewhere role assignments live, so the role checks can be generated
subjectwho acts: where they sit in the hierarchy, how far they reach, how they're identified
objecta governed table — its relations, permissions, and optional per-record sharing
granta scoped, revocable, expiring grant of reach into part of the hierarchy

Permissions are a small boolean algebra over those terms — union, intersection, and fail-closed negation — so viewer and not banned or (owner or shared) and not banned compile straight to an RLS predicate. @holds(<perm>) gates a branch on the caller holding an admin permission, matched against the rolestore's materialized permission arrays at query time, so role edits change the floor without a re-emit.

A permission grants; a require constrains. Because Postgres ORs permissive policies together, every term in a permission line can only ever widen. require <verb> = <expr> emits the same predicate AS RESTRICTIVE, which Postgres ANDs with the permissive set — a floor under the permission rather than another branch beside it, per verb, and carried into the generated app surface as well as the policy. See require.demesne.

Spec introspection

The compiled spec is the single source of your vocabulary, so you can build a role-management or permission-admin UI from it without re-declaring the permission list. spec.Vocabularies() returns each declared vocabulary and its permissions, and marks each parameterized one: a permission that carries the open * model segment, like docs:read:* rather than a concrete docs:read. spec.ExpandedPresets(rolestore) maps each preset of that rolestore's vocabulary to its fully resolved permission set, and expands both + references and the = * wildcard. Demesne returns generic data. How you bucket, label, and lay it out is your UI's job.

for _, v := range spec.Vocabularies() {
	for _, p := range v.Permissions {
		// p.Name, p.Parameterized → drive a permission picker
	}
}
presets, _ := spec.ExpandedPresets("staff") // preset → resolved permissions, for a role editor

Worked examples

The patterns that are easy to get subtly wrong by hand are each a few lines of spec, and each ships with a test that asserts the generated policy actually enforces the intended reach:

PatternSpec
Folder → document inheritance, unbounded nestinginheritance.demesne
Groups within groups (transitive membership)groups.demesne
Role-based access controlrbac.demesne
viewer ∩ member − bannedboolean.demesne
Narrowing a permission you cannot take back (AS RESTRICTIVE)require.demesne

Run them with go test . -run TestCanonical.

How it compares

Demesne is a Zanzibar-class relationship model, but it compiles into Postgres instead of running as a separate Check service. CAPABILITIES.md has the full matrix and an honest comparison with Zanzibar, Ory Keto, OpenFGA, Cerbos, and Oso — including where each of those is the better fit.

What to expect

  • Authorization, not authentication. Demesne reads the session your auth provider issues — Clerk, BetterAuth, Supabase Auth — and decides what each user can reach. Signing users in stays with your provider; Demesne sits alongside it.
  • Postgres only. Compiling to RLS is the whole idea; a Supabase deployment profile ships (SUPABASE.md).
  • A library and CLI, not a service — nothing extra to run or scale next to your database.
  • Every rule must be expressible as a SQL predicate. Reverse "who can see this?" queries are supported but deliberately conservative (fail-closed), not exhaustive.
  • No dependencies in the core. The engine module is standard-library only and never opens a connection; the CLI is a separate module that links a Postgres driver for its live-database commands.

Development

go build ./...
go vet ./...
go test ./...

License

Apache 2.0 — see LICENSE and NOTICE.

Contributors

mattblr

233 commits

foir-io/demesne

An RLS-compiled ReBAC + topology authorization engine — one spec → Postgres RLS + an app PDP

7

stars

233

commits

Go

primary language

Aug 29, 2026

updated

README

Demesne, Zanzibar-style authz framework compiled to RLS

Demesne


Write your authorization rules once, in a single spec file. Demesne compiles them into Postgres Row-Level Security, so the database enforces access on every query — a forgotten WHERE clause, a background job, or an ad-hoc psql session can't reach data the rules forbid.

It takes the idea behind Google's Zanzibar — a declarative schema of who-relates-to-what — but skips the separate authorization service. There's no Check API to call, no second datastore to keep in sync, no consistency tokens. The policy lives in the one place it can't be bypassed: the data path.

The problem

Authorization usually lives in application code — a service you call, or if checks spread across handlers. Both only protect the paths that remember to ask. Miss one and the rule isn't there.

Demesne moves the decision into Postgres, so access is a property of the data rather than a step in the request. One .demesne file compiles to two layers:

  • Row-Level Security — the enforcement floor. Demesne generates the policies and the trusted SECURITY DEFINER functions they call. Every query is filtered by the same rules, whether it comes from your app, a cron job, or a database console.
  • A verb gate — for actions RLS can't see. Some permissions aren't about rows ("can this user publish?"). For those, Demesne generates a Go and TypeScript capability map you check at the request boundary.

The same spec also produces the JWT claims your sessions carry. Change the spec, regenerate, and the database floor and the application code move together — nothing to hand-write and keep in sync.

How it works

A spec describes four things:

  • a topology — your tenancy shape, e.g. tenant → project;
  • the subjects that act — users, customers, staff;
  • the objects they act on — your tables;
  • the relations and permissions that connect them — ownership, roles, sharing, group membership.

From those, Demesne emits the RLS policies, the SECURITY DEFINER kernel, the verb-gate map, and the claims contract. Every trusted function is generated, so there's no opaque hand-written SQL to audit. CI byte-compares each generated artifact against a committed golden file, and asserts that every Can<Verb> point-check inlines the matching policy's USING clause verbatim — so the application surface cannot drift from the floor without failing the build. Set $DEMESNE_PG_URL and the suite additionally installs the emitted kernel in a real Postgres and checks the SQL agrees with the Go and TypeScript resolvers case for case. For a database you have already deployed to, demesne diff <spec> <dsn> --exit-code reports drift between the spec and the live policies.

import "github.com/foir-io/demesne"

spec, _ := demesne.Parse(src)      // text → AST
demesne.Validate(spec)             // static checks

rls, _    := spec.EmitRLS()        // Postgres RLS policies
pdp, _    := spec.EmitPDP()        // Go capability map
defs, _   := spec.EmitDefiners()   // the SECURITY DEFINER kernel
claims, _ := spec.ClaimsContract() // the JWT claims the policies read

Adopting Demesne on an existing database is a short loop: introspect the schema, scaffold a starter spec, edit it, emit the SQL, apply it as a migration. GUIDE.md walks through it end to end.

The spec language

examples/example.demesne is a complete worked spec — a small document app. The building blocks:

BlockDeclares
topologythe tenancy hierarchy; a virtual root sits above tenancy
vocabularypermissions, presets, and a rank ladder
rolestorewhere role assignments live, so the role checks can be generated
subjectwho acts: where they sit in the hierarchy, how far they reach, how they're identified
objecta governed table — its relations, permissions, and optional per-record sharing
granta scoped, revocable, expiring grant of reach into part of the hierarchy

Permissions are a small boolean algebra over those terms — union, intersection, and fail-closed negation — so viewer and not banned or (owner or shared) and not banned compile straight to an RLS predicate. @holds(<perm>) gates a branch on the caller holding an admin permission, matched against the rolestore's materialized permission arrays at query time, so role edits change the floor without a re-emit.

A permission grants; a require constrains. Because Postgres ORs permissive policies together, every term in a permission line can only ever widen. require <verb> = <expr> emits the same predicate AS RESTRICTIVE, which Postgres ANDs with the permissive set — a floor under the permission rather than another branch beside it, per verb, and carried into the generated app surface as well as the policy. See require.demesne.

Spec introspection

The compiled spec is the single source of your vocabulary, so you can build a role-management or permission-admin UI from it without re-declaring the permission list. spec.Vocabularies() returns each declared vocabulary and its permissions, and marks each parameterized one: a permission that carries the open * model segment, like docs:read:* rather than a concrete docs:read. spec.ExpandedPresets(rolestore) maps each preset of that rolestore's vocabulary to its fully resolved permission set, and expands both + references and the = * wildcard. Demesne returns generic data. How you bucket, label, and lay it out is your UI's job.

for _, v := range spec.Vocabularies() {
	for _, p := range v.Permissions {
		// p.Name, p.Parameterized → drive a permission picker
	}
}
presets, _ := spec.ExpandedPresets("staff") // preset → resolved permissions, for a role editor

Worked examples

The patterns that are easy to get subtly wrong by hand are each a few lines of spec, and each ships with a test that asserts the generated policy actually enforces the intended reach:

PatternSpec
Folder → document inheritance, unbounded nestinginheritance.demesne
Groups within groups (transitive membership)groups.demesne
Role-based access controlrbac.demesne
viewer ∩ member − bannedboolean.demesne
Narrowing a permission you cannot take back (AS RESTRICTIVE)require.demesne

Run them with go test . -run TestCanonical.

How it compares

Demesne is a Zanzibar-class relationship model, but it compiles into Postgres instead of running as a separate Check service. CAPABILITIES.md has the full matrix and an honest comparison with Zanzibar, Ory Keto, OpenFGA, Cerbos, and Oso — including where each of those is the better fit.

What to expect

  • Authorization, not authentication. Demesne reads the session your auth provider issues — Clerk, BetterAuth, Supabase Auth — and decides what each user can reach. Signing users in stays with your provider; Demesne sits alongside it.
  • Postgres only. Compiling to RLS is the whole idea; a Supabase deployment profile ships (SUPABASE.md).
  • A library and CLI, not a service — nothing extra to run or scale next to your database.
  • Every rule must be expressible as a SQL predicate. Reverse "who can see this?" queries are supported but deliberately conservative (fail-closed), not exhaustive.
  • No dependencies in the core. The engine module is standard-library only and never opens a connection; the CLI is a separate module that links a Postgres driver for its live-database commands.

Development

go build ./...
go vet ./...
go test ./...

License

Apache 2.0 — see LICENSE and NOTICE.

Contributors

mattblr

233 commits

Languages

Go

85.5%

TypeScript

13.4%