constructive-safegres
Original:🇺🇸 English
Translated
Safegres is Constructive's security protocol for expressing authorization as Authz* policy nodes (types + JSON configs). This skill defines each Authz* type, its config shape, semantics, and when to use it. No SQL and no SDK/grant/RLS steps.
20installs
Added on
NPX Install
npx skill4agent add constructive-io/constructive-skills constructive-safegresTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Safegres (Authz* Security Protocol)
Safegres is the protocol layer behind Constructive authorization.
- Safegres is expressed as a policy type (e.g. ) plus a JSON config (policy
AuthzEntityMembership).data - 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):* -- defines each Data* nodeType, what fields it creates, and which Authz* policy it pairs with
constructive-data-modules
Core vocabulary (used in every Safegres policy)
Actor
The actor is the authenticated user performing the query.
- (conceptually) = the actor's user id.
current_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: identifies the org/group.
entity_id - 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- = App
1 - = Org
2 - = Group
3
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 can often unify:
owner_id- "user owns it personally" and
- "org owns it and user is a member"
...under a single policy.
AuthzEntityMembershipThe critical distinction: AuthzMembership
vs AuthzEntityMembership
AuthzMembershipAuthzEntityMembershipAuthzMembership
(UNBOUND)
AuthzMembershipMeaning: "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 on an entity-scoped table and expecting it to mean "member of this row's org".
AuthzMembership(membership_type=2)- 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)
AuthzEntityMembershipMeaning: "Does the actor have membership in the specific entity referenced by this row's field?"
- It binds membership evaluation to an on the protected row.
entity_field - It is the default choice for entity-scoped resources.
Rule of thumb:
- If your row has an ,
entity_id, ororganization_idthat should scope access: you almost always want EntityMembership, not Membership.owner_id
Safegres policy node types (leaf types)
Below are the 14 leaf policy node types.
Each policy is described as:
- Intent: what it's for
- Config: JSON shape (keys)
- Semantics: what it authorizes (in words)
- Use when / Avoid when
Note:exists as an advanced meta-node for boolean trees; see the section at the end.AuthzComposite
1) AuthzDirectOwner
AuthzDirectOwnerIntent: Direct personal ownership.
Config:
json
{ "entity_field": "owner_id" }Semantics: Authorize when the row's equals the actor's user id.
{entity_field}Use when:
- The row is owned by exactly one user, and ownership is represented directly on the row.
Avoid when:
- Ownership can be an organization (or user-as-org) and you want "org members can access." Prefer (org scope) instead.
AuthzEntityMembership
2) AuthzDirectOwnerAny
AuthzDirectOwnerAnyIntent: Multi-owner OR logic.
Config:
json
{ "entity_fields": ["sender_id", "receiver_id"] }Semantics: Authorize when the actor id matches any of the fields.
Use when:
- A record has multiple relevant user id columns and any of them confer access.
3) AuthzMembership
AuthzMembershipIntent: Unbound membership gate (app/org/group), optionally permissioned.
Config (minimal):
json
{ "membership_type": 1 }Config (permissioned):
json
{ "membership_type": 1, "permission": "admin_permissions" }Optional keys (depending on policy needs):
- (string)
permission - (string[])
permissions - (boolean)
is_admin - (boolean)
is_owner
Semantics: "The actor has at least one membership record in the given scope, optionally matching permission/admin flags."
Use when:
- App-level admin checks.
- Global feature gating.
Avoid when:
- Entity-scoped resources (anything that should be constrained by a row's field).
4) AuthzEntityMembership
AuthzEntityMembershipIntent: Bound membership-to-row.
Config (minimal):
json
{ "entity_field": "entity_id", "membership_type": 2 }Optional keys:
- /
permissionpermissions - /
is_adminis_owner
Semantics: "The actor is a member of the entity referenced by this row's ."
{entity_field}Use when:
- Org-owned or group-owned resources.
- that may refer to either a user or an org (because users are orgs via personal orgs).
owner_id
5) AuthzRelatedEntityMembership
AuthzRelatedEntityMembershipIntent: Entity membership where the entity isn't directly on the protected row, but reachable via a join.
Config (typical):
json
{
"entity_field": "post_id",
"membership_type": 2,
"obj_schema": "public",
"obj_table": "posts",
"obj_field": "organization_id"
}Semantics: "Look up the related row and authorize based on membership in the entity referenced there."
Use when:
- Protected rows reference another table (FK), and that related table carries / org id.
entity_id
6) AuthzPeerOwnership
AuthzPeerOwnershipIntent: Peer visibility via shared entity membership (direct owner field on protected row).
Config (typical):
json
{ "owner_field": "owner_id", "membership_type": 2 }Optional keys:
- /
permissionpermissions - /
is_adminis_owner
Semantics (in words):
- Find the entities (orgs/groups) the actor belongs to.
- Find other users who belong to those same entities.
- Allow access when the row's is one of those peer user ids.
{owner_field}
Use when:
- "People in the same org can see each other's user-owned objects."
Avoid when:
- The owner is not directly on the protected row (then use ).
AuthzRelatedPeerOwnership
7) AuthzRelatedPeerOwnership
AuthzRelatedPeerOwnershipIntent: Peer visibility via shared entity membership through a related table.
Config (typical):
json
{
"entity_field": "message_id",
"membership_type": 2,
"obj_schema": "public",
"obj_table": "messages",
"obj_field": "sender_id"
}Optional keys:
- (defaults to
obj_ref_field)id - /
permissionpermissions - /
is_adminis_owner
Semantics (in words):
- Find peers of the actor (as in ).
AuthzPeerOwnership - Join the related table where each peer is the related row's owner ().
obj_field - Allow access when the protected row's matches those related rows.
{entity_field}
Use when:
- Protected row points at another object, and that object's owner is what should be peer-visible.
8) AuthzOrgHierarchy
AuthzOrgHierarchyIntent: Visibility via org hierarchy (manager/subordinate relationships).
Config (typical):
json
{ "direction": "down", "anchor_field": "owner_id", "entity_field": "entity_id" }Semantics: Authorize based on hierarchy closure relationships anchored at a user field (often ).
owner_idUse when:
- Manager sees subordinate-owned records.
- Subordinate sees manager-owned records.
9) AuthzTemporal
AuthzTemporalIntent: Time-window constraints.
Config (typical):
json
{ "valid_from_field": "valid_from", "valid_until_field": "valid_until" }Either field can be omitted (at least one is required):
- only -> "accessible from this time onward" (open-ended future)
valid_from_field - only -> "accessible until this time" (open-ended past)
valid_until_field - Both -> classic time window
Additionally, a NULL column value in is treated as "no expiry" (), making the window dynamic per row.
valid_untilvalid_until IS NULL OR valid_until > now()Optional keys:
- (default
valid_from_inclusive)true - (default
valid_until_inclusive)false
Semantics: Authorize only when "now" is within the configured time window. Omitting a field removes that boundary.
Use when:
- Scheduled content.
- Expiring invites.
- Open-ended "accessible after publish date" (use only).
valid_from_field
Combination guidance:answers when access is valid, not who has access. On its own it means "anyone can access within the time window." In practice, always combine it with an identity-based policy — either as a restrictive top-level policy (ANDed with a permissive identity policy) or inside anAuthzTemporalAuthzComposite.BoolExpr
Overlap with: You could approximate published-content gating withAuthzPublishable(e.g.AuthzTemporalwith novalid_from_field: "published_at"). However,valid_until_fieldadditionally provides theAuthzPublishableboolean toggle, which lets authors unpublish content independently of time. Useis_publishedwhen you need an explicit on/off switch; useAuthzPublishablewhen access is purely time-driven.AuthzTemporal
10) AuthzPublishable
AuthzPublishableIntent: Draft/published gating.
Config (default fields):
json
{}Optional keys:
- (default
is_published_field)"is_published" - (default
published_at_field)"published_at" - (default
require_published_at)true
Semantics: Authorize when a record is published (and, if , when ).
require_published_at=truepublished_at <= nowUse when:
- Public content that is only visible after publishing.
Combination guidance:answers whether content is published, not who can see it. On its own it means "anyone can see published content." In practice, always combine it with an identity-based policy — either as a restrictive top-level policy (ANDed with a permissive identity policy likeAuthzPublishable) or inside anAuthzEntityMembershipAuthzComposite. See the "Permissive vs Restrictive policies in RLS" section for examples.BoolExpr
Overlap with: The time component ofAuthzTemporal(AuthzPublishable) is a subset of whatpublished_at <= nowcan express. The key difference is theAuthzTemporalboolean -- a deliberate on/off toggle thatis_publisheddoes not provide. If you only need time-window access with no manual toggle,AuthzTemporalis sufficient.AuthzTemporal
11) AuthzMemberList
AuthzMemberListNot recommended. This policy relies on a UUID array column rather than a proper foreign-key relationship. It does not scale well and bypasses normal relational integrity. PreferorAuthzEntityMembershipwith proper FK-based membership tables when possible.AuthzPeerOwnership
Intent: Actor is present in a UUID array column on the same row.
Config:
json
{ "array_field": "member_ids" }Semantics: Authorize when the actor id appears in .
{array_field}Use when:
- Legacy share lists stored as arrays (supported but not recommended for new designs).
12) AuthzRelatedMemberList
AuthzRelatedMemberListNot recommended. Same concern as-- relies on a UUID array column in a related table rather than proper FK-based membership. Prefer FK-based policies when possible.AuthzMemberList
Intent: Actor is present in a UUID array column in a related table.
Config (conceptual):
json
{
"owned_schema": "public",
"owned_table": "documents",
"owned_table_key": "member_ids",
"owned_table_ref_key": "document_id",
"this_object_key": "id"
}Semantics: "Follow a reference to a related row that contains an array of member ids."
Use when:
- Legacy membership lists stored as arrays in a related table (supported but not recommended for new designs).
13) AuthzAllowAll
AuthzAllowAllWARNING:is almost never what you want. It grants unconditional access to every authenticated user for the specified privilege. Before using it, ask yourself: "Should literally every authenticated user be able to read/write this data?" If the answer is no (and it usually is), use a scoped policy likeAuthzAllowAllorAuthzDirectOwnerinstead.AuthzEntityMembershipEspecially avoidon junction tables. When creating ManyToMany relations with security, match the junction table's policy to the parent tables' policies. If parents useAuthzAllowAll, the junction should too. UsingAuthzDirectOwneron a junction table means any authenticated user can create/delete links between rows they don't own. See theAuthzAllowAllskill for junction table security patterns.constructive-relations
Intent: Unconditional allow.
Config:
json
{}Semantics: Always authorizes.
Legitimate use cases (rare):
- Truly public reference data (e.g., a lookup table that any user should read)
countries - Public read-only access (combine with restrictive write policies)
Common misuses:
- Using as a "just make it work" default -- this bypasses all access control
AuthzAllowAll - Using on junction tables when parent tables have scoped policies -- the junction should match the parents
AuthzAllowAll - Using for both read AND write on any table with user-generated content
AuthzAllowAll
14) AuthzDenyAll
AuthzDenyAllIntent: Unconditional deny.
Config:
json
{}Semantics: Never authorizes.
Use when:
- Explicitly blocking a privilege.
Advanced: AuthzComposite
(meta-node, not a leaf type)
AuthzCompositeAuthzCompositeThe for an is itself an AST node that the system recursively evaluates. It can be either a single Authz* leaf node or a combining multiple nodes.
dataAuthzCompositeBoolExprSingle leaf node wrap — delegates to one Authz* node:
json
{
"AuthzEntityMembership": {
"entity_field": "owner_id",
"membership_type": "Organization Member"
}
}BoolExprjson
{
"BoolExpr": {
"boolop": "AND_EXPR",
"args": [
{ "AuthzTemporal": { "valid_from_field": "publish_at" } },
{ "AuthzDirectOwner": { "entity_field": "owner_id" } }
]
}
}BoolExprjson
{
"BoolExpr": {
"boolop": "OR_EXPR",
"args": [
{
"AuthzEntityMembership": {
"entity_field": "owner_id",
"membership_type": "Organization Member"
}
},
{
"AuthzMembership": {
"membership_type": "App Member",
"permission": "create_invites"
}
}
]
}
}When to use :
AuthzComposite- 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.
- Non-authz conditions in the same expression tree (e.g., column value checks combined with auth checks).
Permissive vs Restrictive policies in RLS
When Safegres policies compile to PostgreSQL RLS, their interaction depends on whether they are permissive or restrictive:
- Permissive (default): Multiple permissive policies on the same table and privilege are ORed together. If any permissive policy passes, the row is accessible.
- Restrictive (): Restrictive policies are ANDed with the result of permissive policies. All restrictive policies must pass in addition to at least one permissive policy.
permissive := false
OR composition (permissive + permissive):
"Owner OR org admin can see" — add two separate permissive policies. PostgreSQL automatically ORs them:
Policy 1 (permissive): AuthzDirectOwner { entity_field: "owner_id" }
Policy 2 (permissive): AuthzEntityMembership { entity_field: "organization_id", membership_type: 2, is_admin: true }
Effective rule: row.owner_id = actor OR actor is admin of row.organization_idAND composition (permissive + restrictive):
"Org members can access, but only while the row's time window is active" — add membership as permissive and the time constraint as restrictive:
Policy 1 (permissive): AuthzEntityMembership { entity_field: "entity_id", membership_type: 2 }
Policy 2 (restrictive): AuthzTemporal { valid_from_field: "starts_at", valid_until_field: "ends_at" }
Effective rule: actor is member of row.entity_id AND now() is within [starts_at, ends_at)3 policies (2 permissive + 1 restrictive):
"Owner OR org member can access, but only if the row is published":
Policy 1 (permissive): AuthzDirectOwner { entity_field: "owner_id" }
Policy 2 (permissive): AuthzEntityMembership { entity_field: "organization_id", membership_type: 2 }
Policy 3 (restrictive): AuthzPublishable {}
Effective rule: (row.owner_id = actor OR actor is member of row.organization_id) AND row.is_published = true4 policies (2 permissive + 2 restrictive):
"Owner OR org member can access, but only if published AND within the time window":
Policy 1 (permissive): AuthzDirectOwner { entity_field: "owner_id" }
Policy 2 (permissive): AuthzEntityMembership { entity_field: "organization_id", membership_type: 2 }
Policy 3 (restrictive): AuthzPublishable {}
Policy 4 (restrictive): AuthzTemporal { valid_from_field: "available_from", valid_until_field: "available_until" }
Effective rule: (P1 OR P2) AND R3 AND R4
= (owner OR org member) AND is_published AND now() in time windowNotice the pattern: permissive/restrictive composition always produces . This is powerful but limited to a single grouping shape.
(P1 OR P2 OR ... Pn) AND R1 AND R2 AND ... RmWhen AuthzComposite
is necessary
AuthzCompositePermissive/restrictive composition cannot express arbitrary boolean groupings. Consider:
"Access is allowed if (org member AND published) OR (direct owner AND within time window)":
Desired: (AuthzEntityMembership AND AuthzPublishable) OR (AuthzDirectOwner AND AuthzTemporal)This requires OR-ing two AND-groups — impossible with flat permissive/restrictive policies (which always produce a single shape). Use :
(any P) AND (all R)AuthzCompositejson
{
"BoolExpr": {
"boolop": "OR_EXPR",
"args": [
{
"BoolExpr": {
"boolop": "AND_EXPR",
"args": [
{ "AuthzEntityMembership": { "entity_field": "organization_id", "membership_type": 2 } },
{ "AuthzPublishable": {} }
]
}
},
{
"BoolExpr": {
"boolop": "AND_EXPR",
"args": [
{ "AuthzDirectOwner": { "entity_field": "owner_id" } },
{ "AuthzTemporal": { "valid_from_field": "starts_at", "valid_until_field": "ends_at" } }
]
}
}
]
}
}Prefer multiple top-level policies over whenever possible. They are simpler, easier to read, and easier to maintain. Reserve for cases that genuinely require nested boolean trees like the one above.
AuthzCompositeAuthzComposite