Working document. Goal: clear, example-driven reference for how
_sharingworks across Entu's three entity tiers. Grounded in official docs (entu/www) and API source (entu/api).
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š" |
- "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/apisource:definitionEntity= the entity type entity;definition= the property definitions array (both inaggregate.js).
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:
_sharingon 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)._sharingon 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._sharingon 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?
_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).
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.
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:
private— if requester's user ID is in the access array (rights-holder)domain— if'domain'is in the access array and the sub-doc existspublic— if'public'is in the access array and the sub-doc exists- 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.
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.
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" |
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 |
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.
_inheritrights is simpler than _sharing — it has one rule, documented in one sentence:
"When
_inheritrights: trueis set on a child entity, it inherits the access rights from its parent." — entu/www entities page
_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,_viewerfrom 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.
From the entu/www docs:
"Right evaluation order: explicit rights on the entity → inherited rights from parent →
_sharinglevel._noaccessoverrides all other rights on the same entity — including direct positive rights. It is not propagated to children via_inheritrights; only_viewer,_expander,_editor, and_ownerare 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.
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.
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.
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)
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
_sharingvalues 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_sharingvalues 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.
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.
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.
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:
- Who can see the entity type entity? (Relevant for
resolveTypeId— can non-owners discover this type?) - What's the projection ceiling for instances? (Relevant only if property-defs have non-private
_sharing.)
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.
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)