constructive-safegres
Safegres (Authz* Security Protocol)
Safegres is the protocol layer behind Constructive authorization.
- Safegres is expressed as a policy type (e.g.
AuthzEntityMembership) plus a JSON config (policydata). - The system compiles these policy nodes into enforcement mechanisms (most notably PostgreSQL RLS), but Safegres itself is not SQL.
If you are writing automation that provisions security, treat Safegres as the vocabulary of "what access means".
Related skills:
- TypeScript SDK secure provisioning:
constructive-security - Relation provisioning:
constructive-relations - Data modules (field generators):*
constructive-data-modules-- defines each Data* nodeType, what fields it creates, and which Authz* policy it pairs with
Core vocabulary (used in every Safegres policy)
Actor
The actor is the authenticated user performing the query.
current_user_id()(conceptually) = the actor's user id.- In membership resolution tables you'll see this represented as
actor_id.
Entity
An entity is the scope a membership belongs to.
- For org/group memberships:
entity_ididentifies the org/group. - For app memberships: membership is global, so there is typically no per-row entity_id binding.
Membership types (scopes)
Safegres policies commonly take membership_type:
1= App2= Org3= Group
This can be provided as an integer or a string name (resolved via the membership types module).
Users ARE Organizations (personal orgs)
A key identity property:
- Every user also has an "org identity".
- Each user automatically has an org-level membership to themselves ("personal org").
This matters because an org-level membership check against a field like owner_id can often unify:
- "user owns it personally" and
- "org owns it and user is a member"
...under a single AuthzEntityMembership policy.
The critical distinction: AuthzMembership vs AuthzEntityMembership
AuthzMembership (UNBOUND)
Meaning: "Is the actor a valid member of some scope (app/org/group), optionally with a permission/admin flag?"
- It does not bind to any field on the row being accessed.
- Therefore it is primarily an app-level gate.
Typical correct uses:
- "Is this request coming from any authenticated/approved user?"
- "Is the actor a super app admin?"
- "Can the actor access a global administrative table that is not entity-scoped?"
Typical incorrect uses:
- Using
AuthzMembership(membership_type=2)on an entity-scoped table and expecting it to mean "member of this row's org".- It does not.
- It means "member of any org" (or, more precisely, "has at least one org membership row"), which is almost always too broad.
AuthzEntityMembership (BOUND)
Meaning: "Does the actor have membership in the specific entity referenced by this row's field?"
- It binds membership evaluation to an
entity_fieldon the protected row. - It is the default choice for entity-scoped resources.
Rule of thumb:
- If your row has an
entity_id,organization_id, orowner_idthat should scope access: you almost always want EntityMembership, not Membership.
Safegres policy node types — Quick reference
There are 14 leaf policy node types plus AuthzComposite (a meta-node for boolean trees).
| # | Type | Intent | Key config |
|---|---|---|---|
| 1 | AuthzDirectOwner |
Direct personal ownership | entity_field |
| 2 | AuthzDirectOwnerAny |
Multi-owner OR logic | entity_fields (array) |
| 3 | AuthzMembership |
Unbound membership gate | membership_type, optional permission/is_admin |
| 4 | AuthzEntityMembership |
Bound membership-to-row | entity_field, membership_type |
| 5 | AuthzRelatedEntityMembership |
Entity membership via join | entity_field, obj_schema/obj_table/obj_field |
| 6 | AuthzPeerOwnership |
Peer visibility (direct) | owner_field, membership_type |
| 7 | AuthzRelatedPeerOwnership |
Peer visibility via join | entity_field, obj_schema/obj_table/obj_field |
| 8 | AuthzOrgHierarchy |
Hierarchy (manager/subordinate) | direction, anchor_field, entity_field |
| 9 | AuthzTemporal |
Time-window constraints | valid_from_field, valid_until_field |
| 10 | AuthzPublishable |
Draft/published gating (READ-only) | is_published_field, published_at_field |
| 11 | AuthzMemberList |
Actor in UUID array (not recommended) | array_field |
| 12 | AuthzRelatedMemberList |
Actor in related UUID array (not recommended) | owned_schema/owned_table/owned_table_key |
| 13 | AuthzAllowAll |
Unconditional allow (use sparingly) | {} |
| 14 | AuthzDenyAll |
Unconditional deny | {} |
See authz-types.md for full documentation of each type including config shapes, semantics, use/avoid guidance, and code examples.
AuthzComposite (meta-node)
AuthzComposite lets you build a boolean expression tree (AND/OR/NOT) over Safegres nodes. The data is an AST node that the system recursively evaluates — either a single Authz* leaf or a BoolExpr.
When to use:
- Genuinely nested boolean logic that cannot be expressed with separate top-level policies.
- Mixing AND/OR at different levels (e.g.,
(A OR B) AND (C OR D)). - NOT expressions.
Prefer multiple top-level policies over AuthzComposite whenever possible. Reserve it for cases that genuinely require nested boolean trees.
See authz-types.md for BoolExpr examples.
Permissive vs Restrictive policies in RLS
When Safegres policies compile to PostgreSQL RLS:
- Permissive (default): Multiple permissive policies are ORed — if any passes, the row is accessible.
- Restrictive (
permissive := false): ANDed with permissive results — all restrictive policies must pass in addition to at least one permissive.
The pattern: (P1 OR P2 OR ... Pn) AND R1 AND R2 AND ... Rm.
| Composition | Example |
|---|---|
| OR (permissive + permissive) | Owner OR org admin can see |
| AND (permissive + restrictive) | Org members, but only within time window |
| Mixed (2P + 1R) | Owner OR org member, but only if published |
| Mixed (2P + 2R) | Owner OR org member, but only if published AND within time window |
When this flat shape is insufficient (e.g., (A AND B) OR (C AND D)), use AuthzComposite.
See authz-types.md for detailed composition examples.
More from constructive-io/constructive-skills
drizzle-orm
Drizzle ORM patterns for PostgreSQL schema design and queries. Use when asked to "design Drizzle schema", "write Drizzle queries", "set up Drizzle ORM", or when building type-safe database layers.
21planning-blueprinting
In-repo planning and specification system for software projects. Use when asked to "create a plan", "write a spec", "document a proposal", "blueprint a feature", or when doing architectural planning work.
20pgsql-parser-testing
Test the pgsql-parser repository (SQL parser/deparser). Use when working in the pgsql-parser repo, fixing deparser issues, running parser tests, or validating SQL round-trips. Scoped specifically to the constructive-io/pgsql-parser repository.
18constructive-graphql-codegen
Generate type-safe React Query hooks, Prisma-like ORM client, or inquirerer-based CLI from GraphQL endpoints, schema files/directories, databases, or PGPM modules using @constructive-io/graphql-codegen. Also generates documentation (README, AGENTS.md, skills/, mcp.json). Use when asked to "generate GraphQL hooks", "generate ORM", "generate CLI", "set up codegen", "generate docs", "generate skills", "export schema", or when implementing data fetching for a PostGraphile backend.
17constructive-server-config
Configure and run the Constructive GraphQL server (cnc server), GraphiQL explorer (cnc explorer), and code generation (cnc codegen). Use when asked to "start the server", "run cnc server", "start GraphQL API", "run GraphiQL", "configure API routing", "generate types", or when working with the Constructive CLI and PostGraphile.
17constructive-boilerplate-nextjs-app
Set up and develop with the Constructive App frontend boilerplate — a Next.js application with authentication, organization management, invites, members, and a GraphQL SDK. Use when scaffolding a new Constructive frontend application from the boilerplate.
17