Skip to content

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.

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.

global
└── org:acme
├── space:cadastre
│ ├── group:acme/cadastre/backend
│ └── group:acme/cadastre/frontend
├── space:platform
│ └── group:acme/platform
└── project:internal-tools
user:acme:alice ← personal, only Alice can see

A 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.

When a client calls plur_recall or plur_inject:

  1. Server resolves the user’s readable scope set.
  2. Server passes that set to the engine as a filter.
  3. The engine only considers engrams whose scope is 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.

When a client calls plur_learn or POST /api/v1/engrams:

  1. Server resolves the user’s writable scope set (typically narrower than readable).
  2. Server checks the target scope against the writable set.
  3. Mismatch → 403 Insufficient permission.

Writable defaults to “everything the user is a direct member of.” So:

  • A user in group:acme/platform can write to group:acme/platform.
  • They can read space:platform (which contains the group) but not write to it directly — write-up requires explicit admin grant.

Within a scope a user can hold one of three roles:

RoleReadWriteManage 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.

Groups arrive in three ways:

  1. Manual — admin creates them in /admin/groups.
  2. SCIM — your IdP provisions them (Okta, Entra). See SCIM.
  3. GitHub/GitLab sync — when GitHub or GitLab is the IdP, repository/team memberships sync automatically. The mapping is path-preserving: org/team in GitHub becomes group:org/team in 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).

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.

The full grant matrix is verified by the test suite (test/security/). Two suites worth knowing about:

  • test/security/postgres-roles.test.ts — asserts the plur_app Postgres 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.

  • 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.