# rows that are part of another row

> Some platform shapes decompose into several tables — a schedule owns its rules, and a rule owns its times, weekdays and month-days. Those child rows cannot exist without their parent, so they take the parent's access rule, re-rooted onto themselves. You declare security once, on the entity you actually think about. A child may still state its own rule and then stops deriving entirely, so there is always exactly one source for one answer.

<!-- id: security-part-of-derived-access · area: security · stability: preview · html: https://osysharp.com/reference/security/part-of-derived-access/ -->

## Summary        {#summary}
A platform shape often decomposes into more than one table. A [schedule](https://osysharp.com/reference/scheduling/schedule/) owns its rules; a rule
owns its times, weekdays and month-days; a business-hours calendar owns its windows and exceptions. Those child rows
have no independent life — a time-of-day that belongs to no rule is not a thing.

So they **derive** their access: the parent's rule, re-rooted onto the child across the reference that owns it. You
declare security **once**, on the entity you actually have an opinion about.

## Signature      {#signature}
```osy syntax
// ONE decision. The rules, their times/weekdays/month-days, and the exclusions all follow it.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }
```

## Description    {#description}

### Why the count of tables is not a count of decisions   {#why}
Before this, an app declaring one nightly schedule wrote **six** near-identical blocks, and a business-hours calendar
cost three. That number followed how many tables the shape happens to decompose into — an implementation detail of
the shape, not a question anyone was answering six times. It also grew: every table a platform shape gained was a
block every app had to add, and the one people forgot was the last one.

**A row that is part of another row is governed by that row.** Whoever may read a schedule may read its rules;
whoever may edit it may edit them. A rule readable by someone who cannot read its schedule is a mistake far more
often than a policy.

### It reaches all the way down    {#transitive}
Derivation follows the whole chain, not one level. A `ScheduleRuleTime` is part of a `ScheduleRule`, which is part of
a `Schedule` — so the rule you wrote on the schedule reaches the time-of-day two hops away. That matters because most
of the tables in a shape are usually grandchildren; stopping at one level would leave most of the ceremony in place.

### The row filter comes too   {#row-filter}
The parent's `where` clause is not dropped on the way down — it is **re-rooted**, so it keeps meaning the same thing:

```osy syntax
// on the parent
partial entity Schedule { security { allow read where Owner == user; } }

// what the child gets, in effect — the same question asked through the hop
//   a ScheduleRule is readable when   Schedule.Owner == user
```

A predicate the platform cannot re-root this way is a **compile error**, never a weakened child rule. That direction
is deliberate: a derived rule that quietly dropped a clause would grant more than the parent does.

### Declaring your own rule replaces it   {#override}
A child that declares its own `partial entity … { security { } }` uses that and derives **nothing**:

```osy syntax
partial entity Schedule     { security { allow read, create, update, delete when IsAuthenticated; } }
partial entity ScheduleRule { security { allow read when IsAuthenticated; } }   // read-only, and it does NOT also derive
```

⚠ **This is the opposite of how [capability-owned rows](https://osysharp.com/reference/security/capability-row-ownership/) compose, and the
difference is load-bearing.** There, your block lands *alongside* the capability's rule and the grants **combine** —
you can add a support role, and you cannot take the owner's access away. Here your block **replaces** the derivation.
If it combined, the stricter rule above would be pointless: the derived `delete` grant would OR with it and hand back
exactly what you just refused.

The rule of thumb follows from that. Reach for an override when a child genuinely differs from its parent — and
expect that to be rare, because a child that needs its own policy is usually telling you it is not really *part of*
anything.

### Which tables these are   {#which-tables}
You do not have to track them. `osy explain` reports each one's posture as **part-of derived** and names the entity
whose rule governs it, so the answer is always available from the app rather than from a list to keep in your head.

### A principal check is COPIED; a row filter is HOPPED   {#copies-vs-hops}

The two halves of a rule derive differently, and the difference decides what is even possible.

A **`when`** asks about the CALLER. `when IsFinance` — where `IsFinance` is
`RoleGrant.Any(g => g.User == user && g.Role == Role.Finance)` — never mentions the parent's row at all, so there is
nothing to re-root: the child gets **the same predicate, unchanged**, and grants exactly what the parent grants.

A **`where`** is a row filter. `where Owner == user` reads the parent's own column, so it is re-rooted across the
reference — the child's copy reads `Schedule.Owner == user`.

```osy syntax
// Copied verbatim onto every child: it asks about the caller, not about the row.
partial entity Schedule { security { allow read when IsFinance; } }

// Re-rooted onto every child: it reads the schedule's own column.
partial entity Schedule { security { allow read when IsAuthenticated where Owner == user; } }
```

### A collection hop derives too — this is the normal multi-tenant shape   {#hop-derives}

A `where` that reads the parent's row through a **collection**, not a plain column, derives exactly the same way:
`where Shares.Any(s => s.User == user)` re-roots to `where Schedule.Shares.Any(s => s.User == user)` on the child —
the hop's own correlation keeps reading the parent's row, just one reference further out.

**This is not a rare shape — it is what almost every multi-tenant `where` looks like.** A row-scoped policy call such
as `where IsMember(Organization)` or `where IsAdmin(Organization)` — the ordinary way an app scopes a row to the
caller's organisation — expands to exactly this: `Membership.Any(m => m.User == user && m.Organization == o)`, a
collection hop correlated back to the row's own `Organization` column. So a `[PartOf]` child of an entity secured
this way — the commonest entity shape in a multi-tenant app — derives correctly, including through a
[`Markdown`](https://osysharp.com/reference/types/markdown/) field, whose sections are exactly this kind of child.

```osy title="an org-scoped Markdown field, secured by a hop-based tenancy rule" test app=security-part-of-derived-hop
entity Organization { [Required, MaxLength(120)] string Name; security { allow read when IsAuthenticated; } }
enum OrgRole { Member, Admin }
[Principal] entity Person { [Required, MaxLength(255)] string Email; security { allow read when IsAuthenticated; } }
entity Membership {
  [Required] Organization Organization;
  [Required] Person Person;
  OrgRole Role = OrgRole.Member;
  security { allow read when IsAuthenticated; }
}

policy IsMember(Organization o) => Membership.Any(m => m.Person == user && m.Organization == o);
policy IsAdmin(Organization o)  => Membership.Any(m => m.Person == user && m.Organization == o && m.Role == OrgRole.Admin);

// The Markdown field's sections derive from THIS rule — a member reads them, an admin edits them, nobody else does.
entity Handbook {
  [Required] Organization Organization;
  Markdown Text;
  security {
    allow read where IsMember(Organization);
    allow update where IsAdmin(Organization);
  }
}
```

⚠ **Only the shape above — a plain `Coll.Any(predicate)` existence check — derives.** A hop that also sorts, pages,
projects columns or reaches a second source is refused at compile time rather than derived incorrectly: nothing a
security rule mints today needs any of those, so hitting the refusal means the rule is doing something the derived
child cannot safely inherit yet. Rewrite the parent's filter to read only what a plain existence check needs, or
declare the child's rule yourself where the child is one you can name.

⚑ **A role check in a `when` is fine and always was intended to be** — it is the commonest rule there is, and it is
copied rather than hopped precisely because it has no row correlation to carry.

## Examples       {#examples}
A nightly digest, in full. One security decision, and everything the schedule decomposes into follows it:

```osy title="one decision for the whole shape" test app=security-part-of-derived
[Principal] entity Operator {
  [Required, MaxLength(80)] string Email;
  security { allow read, create when IsAuthenticated; }
}

entity DigestRun { }

// The ONE block. `ScheduleRule`, `ScheduleRuleTime`, `ScheduleRuleWeekday`, `ScheduleRuleMonthDay` and
// `ScheduleExclusion` all take this rule; none of them needs a block of its own.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }

void SeedNightly() {
  var s = new Osysharp.Scheduling.Schedule {
    Name          = "Nightly digest",
    Template      = new DigestRun { },
    Zone          = "Europe/Stockholm",
    EffectiveFrom = new DateTime(2026, 1, 1),
  };
  var rule = new Osysharp.Scheduling.ScheduleRule { Schedule = s, Every = ScheduleFrequency.Day, Interval = 1 };
  new ScheduleRuleTime { Rule = rule, At = TimeSpan.FromHours(2) };
}
```

## See also       {#see-also}
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture these rows are an exception to, and why
- [capability rows that belong to a user](https://osysharp.com/reference/security/capability-row-ownership/) — capability rows owned by a user, whose blocks COMBINE rather than replace
- [Schedule (recurring work)](https://osysharp.com/reference/scheduling/schedule/) — the shape this is most often met through
