Permissions & scopes
PLUR Enterprise enforces permissions server-side, on every call. The client tells the server what scope it wants to read or write; the server checks the user’s resolved scopes against that target. There is no client-side check that matters.
The model
Section titled “The model”Two structures:
- Scopes are namespaces —
global,org:acme,space:cadastre,group:acme/platform,project:guardian,user:acme:alice. - Memberships are graph edges — user X belongs to group Y; group Y has access to scope Z.
Resolution walks the graph: starting from the authenticated user, gather every membership, then expand to every scope reachable from those memberships. The result is a set of scopes the user can read; a second pass marks which they can also write.
The graph lives in Apache AGE (a Postgres extension) and is exposed via src/db/graph.ts. Permission decisions go through src/permissions/resolver.ts.
Standard scope hierarchy
Section titled “Standard scope hierarchy”global └── org:acme ├── space:cadastre │ ├── group:acme/cadastre/backend │ └── group:acme/cadastre/frontend ├── space:platform │ └── group:acme/platform └── project:internal-toolsuser:acme:alice ← personal, only Alice can seeA user’s reachable set always includes global plus their org plus every space, group, and project they’re a member of, plus their own user: scope. Scopes they don’t belong to are simply invisible.
How recall filters
Section titled “How recall filters”When a client calls plur_recall or plur_inject:
- Server resolves the user’s readable scope set.
- Server passes that set to the engine as a filter.
- The engine only considers engrams whose
scopeis in the set.
You can’t recall what you can’t see — and you can’t tell what you can’t see exists. A user querying for “deploys” gets results from group:acme/platform (where they belong) and global, but not from group:other-org/x.
How writes are gated
Section titled “How writes are gated”When a client calls plur_learn or POST /api/v1/engrams:
- Server resolves the user’s writable scope set (typically narrower than readable).
- Server checks the target scope against the writable set.
- Mismatch →
403 Insufficient permission.
Writable defaults to “everything the user is a direct member of.” So:
- A user in
group:acme/platformcan write togroup:acme/platform. - They can read
space:platform(which contains the group) but not write to it directly — write-up requires explicit admin grant.
Three roles
Section titled “Three roles”Within a scope a user can hold one of three roles:
| Role | Read | Write | Manage members |
|---|---|---|---|
viewer | ✓ | – | – |
editor | ✓ | ✓ | – |
admin | ✓ | ✓ | ✓ |
Roles are scope-local. You can be admin of group:acme/platform but only viewer of space:cadastre.
There’s also a process-level org_admin role — set in the admin dashboard, grants admin everywhere within the org. Reserved for actual administrators.
Group provisioning
Section titled “Group provisioning”Groups arrive in three ways:
- Manual — admin creates them in
/admin/groups. - SCIM — your IdP provisions them (Okta, Entra). See SCIM.
- GitHub/GitLab sync — when GitHub or GitLab is the IdP, repository/team memberships sync automatically. The mapping is path-preserving:
org/teamin GitHub becomesgroup:org/teamin PLUR.
You can mix all three. SCIM-provisioned groups are flagged as such; manual edits to them are rejected (the IdP would overwrite them anyway).
Pinning
Section titled “Pinning”pinned is a server-side flag on individual engrams, not a scope mechanism. Admins can pin an engram so it bypasses the relevance gate at injection — useful for safety conventions and meta-rules. Every pin event is audited.
Validation
Section titled “Validation”The full grant matrix is verified by the test suite (test/security/). Two suites worth knowing about:
test/security/postgres-roles.test.ts— asserts theplur_appPostgres role can do what it must do, and cannot do what it must not.test/security/permissions.test.ts— exercises the resolver across every membership shape, including the tricky multi-org case.
If you change the permission model, both suites must pass. They’re the contract.
What you can’t do
Section titled “What you can’t do”- Cross-org reads — even with admin rights in two orgs, you can’t read across org boundaries without explicit pack export. By design.
- Write-up without grant — being in a group does not let you write to the parent scope.
- Anonymous access — every request is authenticated; there is no public-read scope.
If you need any of those, raise a discussion — there are probably better ways to model what you’re trying to do.