Skip to content

Instantly share code, notes, and snippets.

@mitselek
Last active June 17, 2026 01:54
Show Gist options
  • Select an option

  • Save mitselek/75e4e1af48516632eeaa1982121b2fbb to your computer and use it in GitHub Desktop.

Select an option

Save mitselek/75e4e1af48516632eeaa1982121b2fbb to your computer and use it in GitHub Desktop.

Entu Sharing Model — Foundation Draft

Working document. Goal: clear, example-driven reference for how _sharing works across Entu's three entity tiers. Grounded in official docs (entu/www) and API source (entu/api).

1. Foundation: Everything Is an Entity

Entu has one building block: the entity. What distinguishes entities is their role in a three-tier hierarchy:

Tier Official term What it is Example
Tier 1 Entity type An entity that defines a category. It describes what kind of data its instances hold. The person entity type
Tier 2 Property definition An entity that is a child of an entity type (of type property). It defines one field that instances of that entity type carry. The name property definition under the person entity type
Tier 3 Entity instance (or just entity) An entity whose _type references an entity type. The actual data record. A specific person record — "Mihkel Putrinš"

Terminology source

  • "entity type"entu/www entity-types: "An entity type defines a category of objects in your database."
  • "property definitions" — same page: "The fields each entity carries are defined by property definitions, which are child entities of the entity type."
  • "entity instance" — same page (line 103/110); also just "entity" throughout the docs.
  • "Everything in Entu is an entity"entu/www overview.
  • In entu/api source: definitionEntity = the entity type entity; definition = the property definitions array (both in aggregate.js).

The critical mental model

Because all three tiers are entities, they all have _sharing, _inheritrights, _owner, _viewer, etc. — see Access Rights. The same system properties exist on all three tiers, but their effect differs by tier. This is the source of most confusion:

  • _sharing on a Tier 1 (entity type) entity controls who can see the entity type entity itself — AND it acts as a ceiling for property projection on Tier 3 instances (a separate mechanism, described in Section 2).
  • _sharing on a Tier 2 (property definition) entity controls which view tier (private/domain/public) the property value appears in on Tier 3 instances — subject to the Tier 1 ceiling.
  • _sharing on a Tier 3 (entity instance) controls who can access that specific instance.

When reading Entu documentation or code, always ask: which tier's entity am I looking at?


2. How _sharing Composes Across Tiers

_sharing on each tier does something different. The three tiers compose at two distinct times: aggregation time (when an entity is saved) and query time (when an entity is read).

2.1 Aggregation time — building the views

When any property on an entity instance changes, Entu re-aggregates the entire entity. This builds three sub-documents stored on the entity — private, domain, and public — each containing the properties visible at that tier. The process, in aggregate.js:

Step 1: System properties go everywhere. _type, _parent, _sharing are copied into all three sub-documents unconditionally (subject to Step 5 pruning — if a sub-doc is deleted, its system properties go with it).

Step 2: The entity type ceiling is applied. The entity type's own _sharing value is read (aggregate.js line 94). It acts as a ceiling on what property definitions can expose:

Entity type _sharing Effect on property definitions
absent (not set) All property-def _sharing values suppressed. No custom properties enter the domain or public sub-docs.
any string value ('private', 'domain', 'public') Property-def _sharing values are allowed through — see Step 3 for capping.

The mechanism (aggregate.js lines 115-120):

if (!definitionSharing) {          // falsy: undefined, null, ""
  sharing = undefined              // suppress — no projection
}

The check is JavaScript falsiness. The string 'private' is truthy, so it passes. This is not a hierarchy — it's a gate: has the entity type author engaged with sharing at all? If not (absent), the system conservatively suppresses projection. If yes (any explicit value), projection is allowed.

Step 3: Property-def capping (only when entity type is 'domain'). When the entity type _sharing is 'domain', a property definition set to 'public' is capped down to 'domain':

else if (definitionSharing === 'domain' && definition[d].sharing === 'public') {
  sharing = 'domain'              // cap public → domain
}

No other capping occurs. 'private' and 'public' on the entity type both allow property-def values through uncapped.

Step 4: Property placement into sub-documents. The private sub-doc already contains all properties from initial population. This step copies property values into additional sub-docs based on effective sharing (after ceiling + capping):

Effective property sharing Additionally placed in
absent / 'private' (nothing — stays in private only)
'domain' domain sub-doc
'public' domain + public sub-docs

Step 5: Instance _sharing prunes sub-documents. The entity instance's own _sharing determines which sub-documents survive (aggregate.js lines 269-275):

Instance _sharing domain sub-doc public sub-doc
absent / 'private' deleted deleted
'domain' kept (unless empty) deleted
'public' deleted kept (unless empty)

This is strict equality, not hierarchical. A 'public' instance does not keep the domain sub-doc. A 'domain' instance does not keep the public sub-doc. They are parallel alternatives. Additionally, even when _sharing matches, an empty sub-doc (no properties after Steps 1-4) is deleted.

2.2 Query time — selecting the view

When a user requests an entity, access filtering and view selection happen in a single fallthrough chain (entity.js cleanupEntity()). Logically this is two concerns:

Concern 1: Can this user access the entity at all? The entity's access array (built at aggregation time) is matched against the requester. The _sharing value maps to access entries as follows:

Instance _sharing Entries added to access array
absent / 'private' (none from sharing — only explicit rights-holder IDs)
'domain' 'domain'
'public' 'public'

Matching:

  • Anonymous → must match 'public' in the access array
  • Authenticated (no explicit rights) → must match 'domain' or 'public'
  • Rights-holder → matches by user ID

If no match, the entity is not returned (403 / filtered from list).

Concern 2: Which view does this user get? The first available sub-document wins, checked via access array membership:

  1. private — if requester's user ID is in the access array (rights-holder)
  2. domain — if 'domain' is in the access array and the sub-doc exists
  3. public — if 'public' is in the access array and the sub-doc exists
  4. Access denied

A domain-authenticated user viewing a _sharing:'public' entity gets the public view (because the domain sub-doc was deleted at aggregation time — Step 5 above). They do not get a richer domain view.


3. Complete Sharing Matrix

All 12 combinations of entity type _sharing × instance _sharing, traced through the pipeline from Section 2. Each cell shows the entity as that viewer would receive it from the API.

3.1 Sample entity: bulletin

A fictional entity type with three property definitions — one at each sharing level:

Property Prop-def _sharing Example value
title 'public' "Spring concert announced"
summary 'domain' "Rehearsal schedule attached"
draft_notes absent (private) "Check with conductor re: venue"

3.2 The matrix

Every accessible response also includes system properties (_type, _parent, _sharing) — shown as sys.

ET _sharing Instance _sharing Rights-holder Authenticated Guest
absent private sys title summary draft_notes 403 403
absent 'domain' sys title summary draft_notes sys 403
absent 'public' sys title summary draft_notes sys sys
'private' private sys title summary draft_notes 403 403
'private' 'domain' sys title summary draft_notes sys title summary 403
'private' 'public' sys title summary draft_notes sys title sys title
'domain' private sys title summary draft_notes 403 403
'domain' 'domain' sys title summary draft_notes sys title summary 403
'domain' 'public' sys title summary draft_notes sys sys
'public' private sys title summary draft_notes 403 403
'public' 'domain' sys title summary draft_notes sys title summary 403
'public' 'public' sys title summary draft_notes sys title sys title

3.3 What the matrix reveals

Pattern 1 — Rights-holders always see everything. The private sub-doc is never pruned. All 12 rows show the full entity.

Pattern 2 — ET 'private' = ET 'public'. Rows 4-6 and 10-12 are identical. Both are truthy strings that pass the ceiling gate without capping. The only difference is who can see the entity type entity itself (Tier 1) — irrelevant to instance projection.

Pattern 3 — Instance private always means 403 for non-rights-holders. All four ET values produce the same result when the instance is private. The ET ceiling is irrelevant if nobody can reach the entity.

Pattern 4 — Guest never sees summary. The summary property-def has _sharing: 'domain'. It is placed in the domain sub-doc (Step 4). For guest access, the instance must be 'public' — which deletes the domain sub-doc (Step 5). summary always vanishes for guests. This is Trap 3.

Pattern 5 — The leaky-bucket. ET 'domain' + instance 'public' (row 9): authenticated and guest both see only sys. The ceiling capped title from 'public' to 'domain', placing it in the domain sub-doc. Then instance 'public' pruned the domain sub-doc. Nothing survived. This is Trap 6.

Pattern 6 — Only two rows produce guest-visible custom properties. Rows 6 and 12: ET truthy (not 'domain') + instance 'public'. These are the only combinations where a guest sees anything beyond system properties.


4. How _inheritrights Works

_inheritrights is simpler than _sharing — it has one rule, documented in one sentence:

"When _inheritrights: true is set on a child entity, it inherits the access rights from its parent."entu/www entities page

4.1 The rule

_inheritrights is a property on the entity being accessed (the child). It says: "I inherit my parent's rights."

  • _inheritrights: true → this entity inherits _owner, _editor, _expander, _viewer from its parent(s)
  • _inheritrights: false (or absent) → this entity does NOT inherit from its parent

That's it. The parent's own _inheritrights value is irrelevant. A parent can have _inheritrights: false and its children can still inherit from it — if the children have _inheritrights: true.

4.2 What inherits, what doesn't

From the entu/www docs:

"Right evaluation order: explicit rights on the entity → inherited rights from parent → _sharing level. _noaccess overrides all other rights on the same entity — including direct positive rights. It is not propagated to children via _inheritrights; only _viewer, _expander, _editor, and _owner are inherited."

Inherits via _inheritrights Does NOT inherit
_owner _noaccess
_editor _sharing
_expander
_viewer

_sharing is explicitly per-entity — it is not propagated via _inheritrights. Each entity has its own _sharing value, independent of its parent's.

4.3 The cascade

Rights cascade through the hierarchy as long as each child in the chain has _inheritrights: true:

Organization (has _viewer: Alice)
  └── Season (_inheritrights: true)     ← Alice has _viewer (inherited)
        └── Event (_inheritrights: true) ← Alice has _viewer (inherited from season, 
                                            which inherited from org)

If any link in the chain has _inheritrights: false (or absent), inheritance stops at that entity. Entities below it receive nothing from the broken link, because the stopped entity has no inherited rights to pass down:

Organization (has _viewer: Alice)
  └── Season (_inheritrights: false)    ← Alice has NO rights (inheritance blocked)
        └── Event (_inheritrights: true) ← Alice has NO rights (season has nothing to pass down)

The season doesn't inherit from the org, so it has no rights for Alice. The event inherits from the season — but the season has nothing. The chain is only as strong as its weakest link.

4.4 Worked example: the v4E org rights island

The v4E schema sets _inheritrights: false on organization. This is the rights island pattern:

Federation / Umbrella (has _owner: FederationAdmin)
  └── Collective A (_inheritrights: false)  ← FederationAdmin has NO rights here
        ├── Season 1 (_inheritrights: true)  ← inherits from Collective A
        ├── Section "Soprano" (_inheritrights: true) ← inherits from Collective A
        └── Member (Alice) (_inheritrights: true) ← inherits from Collective A

The org's _inheritrights: false means the org does not inherit from its parent (the federation). FederationAdmin's _owner stops at the federation — it does not cascade into the collective.

But within the collective, everything has _inheritrights: true. When the BFF writes _viewer: Alice on the org at member-approval time, that _viewer cascades to seasons, sections, events — the entire agenda subtree. The org is an island from above, but transparent from within.

4.5 _noaccess and inheritance: indirect blocking

From the docs: "Rights defined directly on the child override inherited ones." And: "_noaccess overrides all other rights on the same entity — including direct positive rights. It is not propagated to children via _inheritrights."

The _noaccess property itself is not inherited — children never receive a _noaccess entry from their parent. However, _noaccess has an indirect effect on inheritance. When Entu stores an entity's rights (in rights.js combineRights()), it strips _noaccess'd users from the positive rights before storage. When children later inherit from that parent (via getParentRights()), they read the already-filtered stored rights — so the blocked user is absent.

Practical effect: _noaccess: Alice on an entity effectively blocks Alice from that entity's children too, as long as those children inherit only from that entity.

Exception — multi-parent inheritance hole: If a child inherits from multiple parents (via multiple _parent values), and one parent has _noaccess: Alice but another ancestor in the chain grants Alice rights, Alice regains access on the child. The _noaccess only filtered the rights stored on the entity where it was set — other inheritance paths are unaffected.

Example:

Org (has _viewer: Alice, _noaccess: Alice)
  ├── Season (_inheritrights: true)
  │     └── Event (_inheritrights: true)
  │           ← Alice has NO access (org's stored rights exclude her)
  │
  └── Section (_inheritrights: true, also has direct _viewer: Alice)
        └── Event2 (_inheritrights: true, _parent: Section)
              ← Alice HAS access (inherited from Section's direct grant,
                 which bypasses the org's _noaccess filtering)

5. Common Traps

Trap 1: Entity type _sharing absent vs 'private'

The trap: You create an entity type and don't set _sharing, assuming the default is 'private'. Later you set _sharing: 'private' explicitly, thinking it's the same. It's not.

What happens:

  • Absent (not set): all property-def _sharing values are suppressed. Non-rights-holders see only system properties at best. (Matrix rows 1-3.)
  • 'private': the entity type entity itself is only visible to rights-holders. But for the property projection ceiling, the string 'private' is truthy — property-def _sharing values pass through uncapped. If property-defs have _sharing: 'public' and an instance has _sharing: 'public', those properties are publicly visible. (Matrix rows 4-6.)

Why: The ceiling code uses a JavaScript falsiness check (!definitionSharing). The string 'private' is truthy, so it passes. The check is a gate — "has the entity type author engaged with sharing?" — not a hierarchy.

Rule of thumb: If you want no property projection on instances, leave entity type _sharing unset. If you set it to any value (including 'private'), you're opting into the projection system.

Trap 2: 'domain' and 'public' are parallel, not hierarchical

The trap: You read that 'domain' = authenticated users and 'public' = anyone, and assume public is a superset of domain. You set _sharing: 'public' on an instance and expect authenticated users to get a richer domain view than anonymous users.

What happens: They get the same view. The domain sub-document is deleted at aggregation time for _sharing: 'public' entities. Authenticated users without explicit rights fall through to the public view. (Matrix rows 6, 12 — authenticated sees the same as guest.)

Why: Sub-document pruning uses strict equality, not a hierarchy. _sharing: 'public' keeps only the public sub-doc. _sharing: 'domain' keeps only the domain sub-doc. They are mutually exclusive.

Consequence: If you want authenticated users to see MORE properties than anonymous users, you need _sharing: 'domain' on the instance — but then anonymous users see nothing. You cannot have both a public view and a richer domain view on the same entity.

Trap 3: Property-def 'domain' on a 'public' instance — the vanishing property

The trap: You set _sharing: 'domain' on a property definition (e.g., summary — visible to authenticated users, not to the public). The instance has _sharing: 'public'.

What happens: The property value is placed in the domain sub-doc at aggregation time (Step 4). But the instance's _sharing: 'public' deletes the domain sub-doc (Step 5). The property vanishes from all non-private views. Neither guest nor authenticated users see it. (Matrix rows 6, 12 — summary is missing from both.)

Why: Property placement (Step 4) and sub-doc pruning (Step 5) are independent. Placement says "this value goes in the domain sub-doc." Pruning says "this entity is public, delete the domain sub-doc." Nobody checks for conflicts.

Rule of thumb: On a _sharing: 'public' instance, only property-defs with _sharing: 'public' are visible to non-rights-holders. 'domain'-level property-defs silently disappear.

Trap 4: Confusing the entity type's own _sharing with its effect on instances

The trap: The rsvp entity type has _sharing: 'private'. You read this and conclude "rsvp instances are private." Or the person entity type has _sharing: 'public' and you conclude "person instances are public."

What happens: The entity type's _sharing controls who can see the entity type entity itself — the Tier 1 entity. Instance visibility is controlled by each instance's own _sharing — the Tier 3 entity. They are independent.

Why: Everything is an entity (Section 1). The entity type is an entity with its own _sharing. Instances are separate entities with their own _sharing. The only cross-tier effect is the property projection ceiling (Section 2.1, Step 2) — and that's about property visibility, not entity access.

Rule of thumb: When you see _sharing on an entity type, ask two questions separately:

  1. Who can see the entity type entity? (Relevant for resolveTypeId — can non-owners discover this type?)
  2. What's the projection ceiling for instances? (Relevant only if property-defs have non-private _sharing.)

Trap 5: Assuming _inheritrights: false blocks downward cascade

The trap: An organization has _inheritrights: false. You conclude that rights granted on the org cannot cascade to its children (seasons, sections, events).

What happens: Rights cascade normally. The org's children have _inheritrights: true, so they inherit from the org. The org's _inheritrights: false only means the org doesn't inherit from its parent.

Why: _inheritrights is per-entity and looks upward only: "Do I inherit from my parent?" The parent's own _inheritrights is irrelevant to its children's inheritance. Verified in aggregate.js line 168 (child checks own flag), rights.js getParentRights() (reads parent's stored rights regardless of parent's _inheritrights), and aggregate.js startRelativeAggregation (queries children's flag, not parent's).

Rule of thumb: To know if entity X inherits rights, look at X's _inheritrights. Not its parent's. Not its grandparent's. X's.

Trap 6: The leaky-bucket — ET 'domain' + instance 'public'

The trap: You set entity type _sharing: 'domain' (intending to cap visibility at authenticated users). But instances default to _sharing: 'public'.

What happens: The entity type caps all property-def 'public' values to 'domain'. Properties are placed in the domain sub-doc. But instance _sharing: 'public' deletes the domain sub-doc and keeps the public sub-doc — which has only system properties (nothing achieved effective 'public' after capping). The entity is discoverable by anonymous users but shows no custom properties to anyone except rights-holders. (Matrix row 9.)

Why: The ceiling (Step 3) and pruning (Step 5) work independently. The ceiling lowers property visibility to 'domain', then pruning removes the 'domain' sub-doc because the instance is 'public'. The two mechanisms cancel each other out.

Rule of thumb: If the entity type is 'domain', instances should also be 'domain' — not 'public'. Mismatching these produces an entity that's publicly accessible but publicly empty.


(MVOX:Palestrina)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment