Skip to content

Layering example + guidance: Foundation.Domain + multi-tenant authz/permission matrix #46

Description

@wangkanai

Layering example + guidance: Foundation.Domain + multi-tenant authz/permission matrix

Filing target: github.com/wangkanai/foundation (and cross-linked from
github.com/wangkanai/federation)
Enhancement + docs, Priority P2 (per research
009 §8
P2 row "Foundation.Domain + Audit", downstream of P0 license
wangkanai/federation#40).

Motivated by: research
agile/research/009-refactoring-shared-federation-domain-from-account/report.md
§8 P2 row. Drafted from epic
031-oss-contribution-parallel-track/031-05-p2-p3-items-tracker-small-contributions.

Sibling P2 issues in this series: 031-05-T02 (P2 AccessScope /
PermScope discriminated + only-narrowing Intersect + AppKey
entitlement shapes), 031-05-T09 (P2 cross-tenant / view-as +
AccessGrant primitives). Filed in parallel.


Problem statement

Wangkanai.Foundation.Domain ships usable DDD bases today —
Entity<T>, AuditableEntity<T>, UserAuditableEntity<T> (Created /
Updated / Deleted + By), IAggregateRoot, IRepository /
IAsyncRepository + UoW, DomainEvent types, Result<T> — plus a
full Audit module and EF integration. Wangkanai.Federation is
positioned (per its README "Planned Features" list) to ship the OIDC /
Identity / Permission / Tenant vocabulary on top of those bases.

What is missing from the public ecosystem today is a worked example
of layering the two together
for a multi-tenant authz / permission
matrix. As research 009 §3 row 10 (consensus MEDIUM, Investigator E7

  • web search S9) documents:

No evidence of external/public consumers successfully combining
Foundation.Domain + Federation packages; no samples layering them
for multi-tenant scoped permissions + OIDC.

For a consuming platform trying to build a hosted multi-tenant OIDC
identity provider on top of Foundation + Federation, this gap is
the onboarding cliff — every consumer has to re-derive the
layering pattern from scratch, with no canonical reference, and most
get the FK-scalar convention, the composite-unique identity, or the
scope-narrowing semantics wrong on the first attempt. Research 009
§8 P2 row makes this explicit: the gap is "Zero public samples
combining Foundation.Domain + Federation auth".

What RiverSync needs (and what the spike demonstrated)

The thin Common strategy being executed in
epic 029 — Common OSS Foundation Layer
(IMPLEMENTED 2026-06-18) and
epic 030 — Extract Federation Domain Models To Common
(IN PROGRESS) needs a canonical worked example upstream that
demonstrates:

  1. RiverSyncAuditableEntity (or a generic platform wrapper) extending
    Foundation.Domain.AuditableEntity<T> — adds platform-wide audit
    semantics without re-implementing the Foundation audit trail.
  2. AccessScope (a PermScope-shaped discriminated value type with
    OrgWide | Region(id) | Site(id) cases, plus an Intersect
    only-narrowing contract) implemented as a Common.Domain value
    type
    in
    feature 029-03 — generalized scope/claim primitives
    — and the bridge to the existing PermScope claim contract in
    Common.Claims.
  3. TenantInfo (a non-persisted projection: Guid Id, display name,
    string TenantType with canonical values Internal | Customer | Partner, optional scope context) implemented in
    feature 029-04 — non-persisted type spike
    — the spike that demonstrates the layering works end-to-end
    without taking a dependency on Account.Domain.
  4. External consumption demonstration in
    Common/test/Unit per
    029-04-U2 AC1:
    a test file that references TenantInfo without a project
    reference to Account.Domain.

The 030 thin shapes — TenantInfo, PermissionInfo,
RolePermissionInfo, ApplicationInfo as Common.Domain value
types with no EF mapping — are the consumer-side proof point that
this layering works.

The current state of RiverSync's authz wiring (worked-example data point)

The RiverSync account IdP today implements the multi-tenant authz /
permission matrix with raw POCOs (no inheritance from
Foundation.Domain):

  • Account/src/Domain/Authorization/Permission.cs:9 — bare entity,
    no AuditableEntity base.
  • Account/src/Domain/Authorization/RolePermission.cs:22-71 — bare
    POCO with Allowed + ScopeRegionId + ScopeSiteId + one-way
    narrowing contract.
  • Account/src/Domain/Tenancy/Tenant.cs:14-23 — bare POCO with
    Guid Id, TenantType TenantType, string Name.
  • Account/src/Domain/Identity/ApplicationUser.cs:28-33 — bare
    POCO with scalar Guid TenantId.

The repeated FR-006 scalar-FK convention and the
"RolePermission_Has_No_ClaimsCompiler_Logic" test pattern across
epics 013-018 are exactly the layering convention the OSS example
needs to document. Without it, every Foundation + Federation consumer
re-invents the wheel.


RiverSync usage context

RiverSync is a six-app product platform with a centralized OIDC
identity provider (Account) and five relying-party apps (Portal,
Admin, Partners, Pipeline, Field). The four anchors below
explain why the upstream layering example matters to us — and why
we are asking for it rather than quietly forking.

  • Thin Common foundation (epic 029, IMPLEMENTED).
    Epic 029 — Common OSS Foundation Layer
    establishes RiverSync.Common.Domain as a thin shell layered on
    Wangkanai.Domain + Wangkanai.System + Wangkanai.Foundation.Domain

    • Wangkanai.Audit.Domain (alpha-02 pair). It introduces
      RiverSyncAuditableEntity (extending Foundation.Domain.AuditableEntity<T>)
      and generalized scope / claim primitives in
      029-03
    • the
      029-04 TenantInfo spike.
      This is the Option A recommendation from research 009 §7: thin
      extraction in Common, not a full internal fork.
  • Thin federation overlay with consumable value types (epic 030, IN PROGRESS).
    Epic 030 — Extract Federation Domain Models To Common
    pushes consumable model shapes (TenantInfo, PermissionInfo,
    RolePermissionInfo, ApplicationInfo) into Common.Domain as
    thin value types — non-persisted, no EF mapping, stringly-typed
    where it must be (e.g. string TenantType) for WASM/RP consumer
    safety. The 030 thin shapes are the
    demonstration that the upstream layering is consumable; the
    worked example upstream is what makes the demonstration
    canonical.

  • Foundation spike plan in 029.
    Per
    epic 029 plan §3
    (action 3 — "Smoke-adopt the Foundation bases in Common.Domain shell"):
    "add Wangkanai.Foundation.Domain + Wangkanai.Audit +
    Wangkanai.EntityFramework.Postgres pins (if needed) to
    Directory.Packages.props; create a thin RiverSyncAuditableEntity
    or similar in Common.Domain and migrate one non-persisted type as
    spike. Rationale: proves layering (F5) with almost zero blast;
    surfaces real gaps early." The spike is exactly the
    Foundation + multi-tenant authz wiring the OSS ask in this issue
    requests as a public example.

  • ARCH:VI — Account is the sole writer.
    Per architecture rule
    .claude/rules/architecture/federation-auth.md
    (extract ARCH:VI):

    • Account is the sole platform OIDC/OAuth2 identity provider —
      the only issuer of the federated token every app trusts.
    • The other five product apps (Portal, Admin, Partners, Pipeline,
      Field) participate strictly as OIDC relying parties; none runs
      its own authorization server or credential store.
    • Common provides consumable shapes (TenantInfo, AccessScope,
      PermScope, RP wiring, ViewAsContext); it does NOT host a
      token issuer.
    • Even if upstream publishes a Foundation + Federation layering
      example, Account remains the only writer of identity and the
      only writer of the persisted permission matrix
      . The OSS
      example is consumed (read-only projection in RP apps, write
      path only in Account), never re-implemented per-app.

No timeline promises (explicit)

To be unambiguous about what this issue is not asking:

  • No timeline promise. This issue does not commit RiverSync to
    any particular release date for adopting any upstream layering
    example. Internal sequencing is governed separately by our own
    epic planning and may change without notice to this issue.
    Concretely: no timeline or commitment by RiverSync is tied to
    the resolution of this issue.
  • No adoption commitment. RiverSync has not committed to taking
    a hard Wangkanai.Foundation.Domain dependency (or
    Wangkanai.Federation) even after this issue is resolved. The
    thin Common strategy in 029/030 is OSS-agnostic and is being
    executed on the alpha-02 pair today; any deeper Federation
    adoption is contingent on legal/architectural review on our side
    (license clarity first — see P0 issue wangkanai/federation#40).
  • No licensing-philosophy request. This issue is filed under
    the assumption that the P0 license work in
    wangkanai/federation#40 is
    resolved first. The ask here is for worked example + docs, not
    for a license change of philosophy. The layering example is
    authored in Markdown + code snippets, and would land under the
    existing Foundation license (Apache-2.0, per research 009 §10) —
    no new license surface.

Request

We respectfully ask the Wangkanai.Foundation (and cross-linked
Wangkanai.Federation) maintainers to publish a layering example +
guidance doc
demonstrating Foundation.Domain + Federation auth for
a multi-tenant authz / permission matrix. The ask decomposes into three
coordinated deliverables:

  1. A worked code sample combining:

    • Foundation.Domain.AuditableEntity<T> (audit trail + soft-delete
      timestamps).
    • RiverSyncEntity<T>-style platform wrapper extending it.
    • A Tenant aggregate with Guid Id + TenantType (Internal /
      Customer / Partner enum or string discriminator) + Name.
    • A scalar-FK User.TenantId foreign-key convention (the build-
      parallelism pattern documented in 013-02 spec line 36-37 AC2).
    • A composite (Email, TenantId) UNIQUE identity invariant (the
      multi-tenant email invariant from 013-05-U2 AC1).
    • A RolePermission with Allowed + ScopeRegionId + ScopeSiteId
      • one-way narrowing contract.
    • The AccessScope discriminated value type with OrgWide | Region | Site cases and an Intersect only-narrowing contract.
    • A non-persisted TenantInfo projection demonstrating the
      thin-overlay shape.

    The sample should be compilable (with the platform RiverSync*
    prefix swapped out for upstream namespaces) and independently
    consumable
    — i.e. a downstream project referencing only
    Foundation.Domain (and Federation, if available) should be
    able to compile against it without taking a dependency on
    Account.Domain or any other RiverSync-private package.

  2. A guidance document (docs/layering.md or equivalent)
    covering the conventions the example demonstrates:

    • Why the FK is scalar (User.TenantId : Guid) and not a
      navigation property — build parallelism, WASM-safety, layered
      projection overlay.
    • Why the composite UNIQUE(Email, TenantId) replaces the
      default Identity index — multi-tenant email invariant.
    • Why the scope narrowing contract is one-way (no widening) —
      least-privilege intersection.
    • Why "no compiler logic in domain entities" is enforced as a
      guard (e.g. the
      RolePermission_Has_No_ClaimsCompiler_Logic test pattern) —
      keeps domain pure, isolates token emission in the Application
      layer.
    • Why the TenantInfo projection is non-persisted and stringly-
      typed (e.g. string TenantType) — WASM/RP consumer path
      without server-side dependencies.
  3. A documented recipe for migrating an existing raw-POCO entity
    (the shape of Account/src/Domain/Authorization/Permission.cs:9
    and RolePermission.cs:22-71 today) to the layered pattern. The
    recipe should be small enough to be a "1-day spike" (per research
    009 §8 P2 row rightmost column: "1-day spike in Common.Domain;
    then migrate timestamps; surfaces real gaps") and produce a
    runnable before/after diff.

These three deliverables together close the "Zero public samples
combining Foundation.Domain + Federation auth" gap on the OSS side and
give downstream consumers (RiverSync + any similar multi-tenant OIDC
platform) a canonical reference for the layering pattern.


Why now

This is a pre-existing gap, but the timing is right for three
converging reasons that make the platform's multi-tenant authz wiring
visible upstream:

  1. The thin Common strategy is already in flight (epic 029
    IMPLEMENTED, epic 030 IN PROGRESS), with the 029-03 scope /
    claim primitives and the 029-04 TenantInfo spike acting as the
    consumer-side proof point that the layering works. The OSS
    worked example is what makes that proof point canonical rather
    than RiverSync-internal.
  2. The cross-tenant ViewAsContext precedent (epic 018) plus
    the 013-05 composite-unique invariant make the multi-tenant
    authz wiring a real, shipped pattern on our side — not a roadmap
    sketch. That makes the layering example a current-day
    reference need rather than a future-tense one.
  3. The P0 license issue (wangkanai/federation#40) is filed
    and being driven in parallel. Until that resolves, this issue
    is filed against Wangkanai.Foundation first
    (Apache-2.0 per research 009 §10 — license clarity already
    exists for Foundation) and can land independently of the
    federation-side P0 work. Once P0 resolves, the same pattern
    can be extended into Wangkanai.Federation's docs.

Per research 009 §8, this P2 row is adjacent to the other P2
rows: P2 AccessScope/PermScope discriminated + only-narrowing
Intersect + AppKey entitlement shapes (031-05-T02); P2 cross-tenant /
view-as + AccessGrant primitives (031-05-T09). The three P2 issues
together cover the "what does a Foundation + Federation layered
multi-tenant IdP actually look like?" question that no public sample
answers today.


References


Drafted by RiverSync for upstream filing against Wangkanai.Foundation
(with a cross-link note on Wangkanai.Federation); not yet filed. Once
filed, the upstream URL will replace this header note and the tracker
row in docs/oss-contributions.md will
be updated to Filed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions