AIM · Identity Model Deep Dive · v3.1 Complete Edition

The Six-Layer Identity Stack — Schemas, APIs, Lifecycles

A focused supplement to Architecture-v3.1.html. Covers the table schemas (per pool), the API surface owned by sdk-identity · sdk-persona · sdk-engagement · sdk-rebac, the lifecycle states of every layer, and the compliance mapping (HIPAA · DPDP · GDPR · PCI · SOC 2 · RBI/SEBI) of where each PII / PHI / payment field lives.

Audience · backend & identity engineers
Companion · Architecture-v3.1.html · SDK-Build-Plan-v3.1.html
Pre-reads · ../Architecture-v2.html §5A (Identity, MDM & Consent)

1 · The Problem We're Modeling

A single human can be — simultaneously — a patient at three hospitals (with multiple doctors and admissions per hospital), a buyer at five eCommerce stores, an investor in two funds, a donor at one ashram, a homeowner working with a contractor, and an employee at her own clinic. We need this to work with:

No single-layer identity model solves all of this. The six-layer stack does, by giving each requirement a dedicated entity with its own lifecycle, audit, and key tier.

2 · The Six Layers

L1

Master Person · person_id

One row per human. Lives in the admin pool of the person's home region. Carries identity alias graph (emails · phones · biometric refs · gov-IDs), home-region pointer, master encryption key handle, and a compliance summary. Encrypted under the Person Key.

  • Cardinality: 1 per human.
  • Pool: admin pool · home region only.
  • Key: Person Key (shred = global erasure).
  • Owns: the master shred power.
L2

App Identity · app_identity_id

The person's projection into one app. One row per (person_id × app_id). Carries the app-scoped display profile, app preferences, app-scoped notification routing (push tokens, WhatsApp number used in this app).

  • Cardinality: N per person, where N is the count of distinct apps the person has logged into.
  • Pool: admin pool of any tenant the person belongs to in this app (typically the first one they joined, or their home admin pool if they're a platform-direct user).
  • Key: Person Key (envelope) + App-scope salt.
  • Owns: per-app preferences and channel routing.
L3

Tenant Membership · tenant_membership_id

The app identity's membership in one tenant. One row per (app_identity_id × tenant_id). Carries the tenant-scoped role, the membership status (active · suspended · departed), tenant-scoped attributes, and joining metadata.

  • Cardinality: N per app identity, where N is the count of tenants the person has joined in this app.
  • Pool: tenant's admin pool.
  • Key: Tenant Key.
  • Owns: tenant-scoped role assignment and lifecycle.
L4

Persona · persona_id

The "hat" the person wears in this tenant membership. Carries a kind (Patient · Doctor · Buyer · Donor · Investor · …), persona-scoped status, and join keys into the persona-extension tables (donor giving history, patient chart root, investor stake records).

  • Cardinality: typically 1 per tenant membership, occasionally 2+ (e.g., doctor-who-is-also-patient).
  • Pool: identity facet in the tenant's admin pool; extension data in the tenant's app pool.
  • Key: Tenant Key + persona-scope salt.
  • Owns: the join from identity layers into the domain.
L5

Encounter · encounter_id

A bounded interaction between one persona and one or more counterpart personas inside a tenant context. Has time bounds, a location, a list of participants with roles, encounter-scoped consent, and an encounter-level encryption key.

  • Cardinality: M per persona (many encounters over time).
  • Pool: tenant's app pool.
  • Key: Encounter Key (shred = encounter-only erasure).
  • Owns: time- and scope-bounded grants; the billable / regulatory work unit.
L6

Relationship · relationship_id

A long-running bond between two personas. Spans many encounters. Carries kind (primary-care · sales-rep · loyalty · advisor), grant scope, status, and an attestation cadence.

  • Cardinality: R per persona (typically small — < 50).
  • Pool: tenant's app pool (unless cross-tenant, then a special cross-tenant relationship pool with re-encryption).
  • Key: Tenant Key (intra-tenant) or Cross-tenant Relationship Key.
  • Owns: ReBAC longitudinal access.

2A · Extended Hierarchies v3.1

The six-layer stack handles per-person identity. Real enterprises also need hierarchies above the tenant (resellers, sub-tenants, org charts), orthogonal to identity (geographic, fiscal-period), and refinements of role identity (role templates with inheritance). v3.1 makes each one a first-class typed entity so verticals stop inventing flat-string substitutes.

Reseller · reseller_id

A partner organization that owns and bills a portfolio of customer tenants under a contract with Projexlight. White-labels the platform under their own brand and domain; sees consolidated billing across their portfolio; gets scoped admin access (read-mostly) into their customer tenants.

  • Cardinality: small (10s–100s of resellers per region).
  • Pool: reseller's own Admin Pool (a tenant of the platform in its own right) + read-projections into each customer tenant's Admin Pool.
  • Carries: commission rate; brand override (logo, palette, domain CNAME); support escalation contact; portfolio-level kill switches; invoice-aggregation preference (reseller-billed vs pass-through to end-tenant).
  • Owns: the GTM channel layer above tenant; consolidated billing; white-label control.
  • Event: reseller.tenant.attached.v1.

Sub-tenant · recursive tenant.parent_tenant_id

A tenant whose parent is also a tenant. Healthcare HIE (50 hospitals under one network), franchise chain (500 locations under one brand), property-management company (200 buildings under one operator), conglomerate (subsidiaries under one parent).

  • Cardinality: arbitrarily deep tree (parent.parent.parent...); typical depth 2–3.
  • Pool placement policy: hybrid — sub-tenants share parent's pool by default (cheap, simple routing, audit roll-up trivial); any sub-tenant can opt out to Tier-P/G and get its own pool (premium isolation for flagship hospital, residency for a German sub-tenant of an EU parent).
  • Audit roll-up: CTE traversal up the chain; parent sees aggregated audit; sub-tenant sees only its own.
  • Quota inheritance: sub-tenant inherits parent's quotas unless explicitly overridden; usage events tag both tenant_id and root_tenant_id for showback.
  • Owns: the multi-location / network operating model.

Business Unit (recursive) · bu.parent_bu_id

v3 introduced a flat bu_id. v3.1 makes BU a tree so enterprise org charts (Global VP → Regional VP → Country Manager → Site Director → Department Lead) cascade ABAC policy correctly. Matches Analyze.txt's mandated scope hierarchy (Org → BU → Region → Property → Dept → User).

  • Cardinality: tree within a tenant; typical depth 3–5.
  • Pool: tenant's Admin Pool (pure metadata).
  • Policy inheritance: ABAC rules cascade down the chain; explicit overrides supported at any node; IQL exposes subject.bu.ancestors() as a queryable set.
  • Owns: enterprise org-chart inheritance for policies, roles, visibility, reporting.

Geographic Node · geo_node_id

A typed tree of geographic units, separate from pool_index. Pool indexes encode where data lives; geo nodes encode what real-world locality something refers to. Levels: Region → Country → State/Province → City → Locality.

  • Cardinality: platform-global static-ish tree (~100k nodes globally).
  • Pool: Global Catalog (read-replicated everywhere).
  • Used by: tenants (registered locality), encounters (event location), addresses (canonical resolution), policies (sub-regional data residency — "DE-only data stays in a German pool even within EU"), reporting ("all encounters in California in Q3").
  • Owns: the locality reference dimension. Distinct from address — geo nodes are political/administrative; addresses are physical points within them.

Role Template · role_template_id

v3 stored persona roles as flat strings (['patient'], ['doctor']). v3.1 promotes roles to typed templates with inheritance. junior_doctor < senior_doctor < chief_of_medicine — junior is a subset of senior is a subset of chief. ABAC policies bind to templates, not strings, so a "senior_doctor can write Rx" rule automatically grants chief.

  • Cardinality: per-app registry of templates (~50 per vertical) + per-tenant overrides.
  • Pool: per-app platform-global templates in Global Catalog; per-tenant overrides in tenant's Admin Pool.
  • Inheritance: single-parent DAG; reasoner caches the closure.
  • Owns: readable, maintainable role-based policies at six-layer scale.

Fiscal Period · fiscal_period_id

Per-tenant fiscal calendar so billing, reporting, and contract renewals align with the tenant's actual fiscal year — not the platform's calendar year. Decomposes into Year → Quarter → Month → Week. Defaults to calendar year for tenants who haven't customized.

  • Cardinality: 1 active period set per tenant; historical periods retained for reporting.
  • Pool: tenant's Admin Pool.
  • Used by: sdk-billing (invoice cycle alignment), sdk-analytics (per-fiscal-quarter rollups), sdk-tenant-lifecycle (renewal triggers).
  • Owns: the temporal hierarchy for finance and reporting.

Agent Identity · agent_id Analyze.txt #5

AI agents acting in the system get their own typed identity — not a special-case bypass of the human identity stack but a parallel structure. An agent has an agent_id, runs on behalf of an agent_persona (the human persona it represents), has a typed agent_scope (allow-list of methods it can invoke), and an agent_chain (provenance: human → meta-agent → executing agent).

  • Cardinality: N agents per tenant (typically < 50).
  • Pool: agent definitions in tenant's Admin Pool; memory namespaces in Vector Store partition per tenant (hard physical isolation).
  • Audited as: actor.kind = 'agent' with the full chain recorded on every action.
  • Beyond-scope actions: route through sdk-approval for human sign-off.
  • Owns: AI-agent containment, prompt-leakage prevention, agent action attribution.
How these compose with the six layers. A persona's identity (who they are) is L4. Their scope (where they sit in the org) is the BU tree. Their locality (where their work happens) is the geo node. Their role authority (what they can do) is the role template. Their tenant context (whose data they see) is L3 + parent-tenant chain. The reseller relationship governs who bills whom. Fiscal period governs when. All seven dimensions stack independently — a clinician (L4 + senior_doctor template) at a sub-tenant hospital (parent-tenant chain) of a healthcare HIE in California (geo node) under Reseller X (reseller_id) during FY24-Q3 (fiscal period) acting on patient Z (encounter participation) is a fully addressable subject for ABAC, audit, billing, and reporting.

2B · Identity Projection System v3.1

The six layers plus the extended hierarchies make identity expressive. They also make identity expensive to resolve on demand — at hyperscale, walking person → app identity → tenant membership → persona → encounter → relationships → role-templates → consent → policy for every authenticated call is the platform's worst hot-path. v3.1's answer is the Identity Projection System: precomputed flattened read models, refreshed by change events, consulted in sub-ms.

Why this exists. Architectural review (../../Analyze2.txt #1) called runtime identity resolution at PB scale "the platform's first bottleneck": it must become projected/cached. This section is the canonical model — the SDK that implements it (sdk-identity-resolver) lives in P2 of the build plan.

2B.1 · The subject_view projection

For every active (person_id, app_id, tenant_id) triple, the projection store holds one materialized subject_view row that flattens everything authz / audit / personalization needs:

CREATE TABLE subject_view (
  -- Composite key
  person_id            uuid not null,
  app_id               text not null,
  tenant_id            uuid not null,

  -- Flattened identity layers (read-only snapshots of L1-L4)
  app_identity_id      uuid not null,
  tenant_membership_id uuid not null,
  primary_persona_id   uuid not null,
  all_persona_ids      uuid[] not null,             -- the person may hold multiple personas at this tenant

  -- Extended hierarchies (read-only snapshots)
  bu_id                uuid,                         -- current BU
  bu_ancestors         uuid[],                       -- full ancestry up to org
  parent_tenant_id     uuid,                         -- if sub-tenant
  root_tenant_id       uuid not null,                -- top of the tenant chain
  reseller_id          uuid,                         -- if attached to a reseller
  geo_node_id          text,                         -- current locality
  role_template_ids    text[] not null,              -- effective roles after inheritance
  effective_role_closure text[] not null,            -- transitive role closure (junior < senior < chief)

  -- Authorization summary
  abac_attributes      jsonb not null,               -- snapshot of attributes the ABAC engine reads
  rebac_edges          jsonb not null,               -- relationships keyed by kind (subset, depth-bounded)
  active_consents      jsonb not null,               -- consent receipts by (purpose, processor)
  effective_scopes     text[] not null,              -- IQL-evaluated effective access scopes

  -- Pool routing
  admin_pool_index     text not null,
  app_pool_index       text not null,
  evidence_pool_index  text,

  -- Snapshot metadata
  projection_version   bigint not null,              -- monotonic, used by audit and policy decisions
  projected_at         timestamptz not null,
  source_event_id      uuid not null,                -- the event that triggered this refresh
  ttl_expires_at       timestamptz not null,         -- safety: force re-projection beyond this

  primary key (person_id, app_id, tenant_id)
);

-- Indexes for the common access patterns
CREATE INDEX subject_view_by_persona ON subject_view USING gin (all_persona_ids);
CREATE INDEX subject_view_by_role ON subject_view USING gin (effective_role_closure);
CREATE INDEX subject_view_by_geo ON subject_view (geo_node_id);
CREATE INDEX subject_view_by_reseller ON subject_view (reseller_id) WHERE reseller_id IS NOT NULL;

Storage: Redis for live hot reads (sub-ms); Postgres in the person's home Admin Pool for durability and replay. The projector worker writes both atomically (Postgres → Redis pub/sub invalidate). See Architecture §8B Polyglot Persistence and §8A Pool Placement Matrix for the doctrine behind these placement choices.

2B.2 · How projections stay fresh

The projector worker subscribes to a small set of change events. When any of these fires, the affected subject_view rows are recomputed within 1s and pushed to Redis:

Triggering eventWhich subject_views to refresh
identity.persona.created.v1 · identity.persona.shred.v1All views for the person
identity.membership.suspended.v1 · identity.membership.reactivated.v1The (person, app, tenant) view
identity.role.assigned.v1 · identity.role.revoked.v1The (person, app, tenant) view
rebac.relationship.created.v1 · rebac.relationship.terminated.v1Both endpoint persona's views
consent.granted.v1 · consent.revoked.v1All views for the consenting person
tenant.bu.moved.v1All views for personas under the BU subtree
tenant.role-template.updated.v1All views whose effective_role_closure references the template
TTL expiry (default 1h)The view (safety re-projection)

2B.3 · How the resolver consumes the projection

resolveIdentityContext(token) from sdk-identity-resolver is the only sanctioned entry point for reading identity. It executes in this order:

// pseudocode — actual implementation in sdk-identity-resolver
async function resolveIdentityContext(token: JWT): Promise<IdentityContext> {
  const claims = verifyJwt(token);                          // (person_id, app_id, tenant_id, ...)
  const key = `${claims.person_id}:${claims.app_id}:${claims.tenant_id}`;

  // 1) Hot path — Redis projection store, p99 ≤ 0.5ms warm
  const view = await redis.get(`subject_view:${key}`);
  if (view) return materializeContext(claims, view);

  // 2) Cold path — Postgres projection store, p99 ≤ 5ms
  const view = await pg.query('SELECT * FROM subject_view WHERE ...', key);
  if (view) {
    await redis.set(`subject_view:${key}`, view, { EX: 60 });
    return materializeContext(claims, view);
  }

  // 3) Fallback — live six-layer resolve (rare; emits an alert)
  emitMetric('identity.projection.miss', { reason: 'cold-not-found' });
  const view = await liveSixLayerResolve(claims);            // expensive — typically > 50ms
  await projector.materializeOutOfBand(view);                // schedule a backfill
  return materializeContext(claims, view);
}

The fallback exists so the platform degrades to live resolution under projector failure rather than hard-failing. CI chaos-tests killing the projector verify the fallback works without errors.

2B.4 · Guarantees

The trade. Some staleness (up to 1s in pathological cases) in exchange for sub-ms identity resolution and predictable latency at PB scale. Hot-path latency becomes a Redis lookup, not a six-layer graph walk — the difference between a usable platform and one that hits 5ms of identity overhead on every call.

3 · Table Schemas (per pool)

Conceptual Postgres DDL. Every table's primary key is non-sequential (ULID); every PII column is envelope-encrypted at the column level under the appropriate key.

3.1 · Master person (admin pool · home region)

CREATE TABLE person (
  person_id              text PRIMARY KEY,         -- pers_01HXYZ...
  home_admin_pool_index  text NOT NULL,            -- admin-014
  home_region            text NOT NULL,            -- ap-south-1
  person_key_arn         text NOT NULL,            -- HSM key handle
  status                 text NOT NULL,            -- ACTIVE | SUSPENDED | ERASED
  alias_graph_version    int  NOT NULL DEFAULT 0,
  created_at             timestamptz NOT NULL,
  erased_at              timestamptz                -- set on cryptographic erasure
);

CREATE TABLE person_alias (
  person_id              text REFERENCES person,
  alias_kind             text NOT NULL,            -- email | phone | govid | biometric_ref
  alias_value_enc        bytea NOT NULL,           -- envelope-encrypted
  alias_fingerprint      bytea NOT NULL,           -- HMAC(value) for lookup
  verified_at            timestamptz,
  PRIMARY KEY (person_id, alias_kind, alias_fingerprint)
);

CREATE TABLE person_secure_data (
  person_id              text REFERENCES person,
  band                   text NOT NULL,            -- 'dl' | 'pan' | 'aadhaar' | 'ssn' | 'passport' | 'pci'
  field_label            text NOT NULL,
  field_value_enc        bytea NOT NULL,           -- per-field envelope
  field_last4_clear      text,                     -- masked, for display
  jurisdiction           text,                     -- 'IN' | 'US' | ...
  consent_ref            text NOT NULL,
  PRIMARY KEY (person_id, band, field_label)
);

3.2 · App identity (admin pool)

CREATE TABLE app_identity (
  app_identity_id        text PRIMARY KEY,         -- apid_01HXYZ...
  person_id              text NOT NULL,            -- references person in home pool
  app_id                 text NOT NULL,            -- 'healthcare' | 'ecommerce' | ...
  display_name_enc       bytea,
  locale                 text,
  preferences_enc        jsonb,                    -- envelope-encrypted JSON
  notification_routing_enc bytea,                  -- channel routing (push tokens, etc.)
  created_at             timestamptz NOT NULL,
  status                 text NOT NULL,            -- ACTIVE | SUSPENDED | DEPRECATED
  UNIQUE (person_id, app_id)
);

CREATE INDEX ix_app_identity_person ON app_identity (person_id);

3.3 · Tenant membership (admin pool · tenant's pool)

CREATE TABLE tenant_membership (
  tenant_membership_id   text PRIMARY KEY,         -- tmb_01HXYZ...
  app_identity_id        text NOT NULL,
  tenant_id              text NOT NULL,
  roles                  text[] NOT NULL,          -- ['patient'] or ['doctor', 'admin']
  status                 text NOT NULL,            -- ACTIVE | SUSPENDED | DEPARTED | INVITED
  joined_at              timestamptz NOT NULL,
  departed_at            timestamptz,
  attributes_enc         jsonb,                    -- tenant-scoped, ABAC inputs
  UNIQUE (app_identity_id, tenant_id)
);

-- Row-level security still applied within a pool
ALTER TABLE tenant_membership ENABLE ROW LEVEL SECURITY;
CREATE POLICY pol_membership ON tenant_membership
  USING (tenant_id = current_setting('app.tenant_id')::text);

3.4 · Persona (admin + app pool split)

-- Identity facet (admin pool of the tenant)
CREATE TABLE persona (
  persona_id             text PRIMARY KEY,         -- pers_01HXYZ_patient
  tenant_membership_id   text NOT NULL,
  kind                   text NOT NULL,            -- 'patient' | 'doctor' | 'buyer' | ...
  status                 text NOT NULL,            -- ACTIVE | SUSPENDED | ARCHIVED
  display_label_enc      bytea,
  extension_table_ref    text,                     -- 'healthcare_chart_root' | 'donor_giving_root' | ...
  extension_pool_index   text,                     -- pool that holds extension rows
  created_at             timestamptz NOT NULL
);

-- Extension data (tenant's app pool — example: healthcare chart root)
CREATE TABLE healthcare_chart_root (
  chart_root_id          text PRIMARY KEY,
  persona_id             text NOT NULL,            -- back-reference; resolved at runtime
  mrn                    text,                     -- medical record number (per-tenant)
  blood_type             text,
  allergies_enc          bytea,
  primary_care_provider  text,                     -- relationship_id reference
  encounter_count        int DEFAULT 0,
  status                 text DEFAULT 'ACTIVE',
  created_at             timestamptz NOT NULL
);

3.5 · Encounter (app pool)

CREATE TABLE encounter (
  encounter_id           text PRIMARY KEY,         -- enc_01HXYZ_visit_42
  tenant_id              text NOT NULL,
  app_id                 text NOT NULL,
  kind                   text NOT NULL,            -- 'visit' | 'admission' | 'order' | 'appointment' | ...
  state                  text NOT NULL,            -- OPEN | IN_PROGRESS | CLOSED | SEALED
  scheduled_at           timestamptz,
  opened_at              timestamptz NOT NULL,
  closed_at              timestamptz,
  sealed_at              timestamptz,              -- on retention-driven seal
  address_id             text,                     -- L5 location anchor
  encounter_key_arn      text NOT NULL,            -- payload key
  payload_enc            bytea,                    -- encrypted payload (visit notes, order body)
  metadata               jsonb                     -- non-PII (counts, refs)
);

CREATE TABLE encounter_participant (
  encounter_id           text REFERENCES encounter,
  persona_id             text NOT NULL,
  role                   text NOT NULL,            -- 'patient' | 'doctor' | 'nurse' | 'buyer' | ...
  joined_at              timestamptz NOT NULL,
  left_at                timestamptz,
  PRIMARY KEY (encounter_id, persona_id, role)
);

CREATE TABLE encounter_grant (
  grant_id               text PRIMARY KEY,
  encounter_id           text REFERENCES encounter,
  grantee_persona_id     text NOT NULL,
  scope                  text[] NOT NULL,          -- ['vitals.read', 'labs.read']
  granted_by             text NOT NULL,            -- persona_id of issuer
  expires_at             timestamptz NOT NULL,
  revoked_at             timestamptz,
  reason                 text                      -- 'shift_assignment' | 'consult_request'
);

3.6 · Relationship (app pool, optionally cross-tenant)

CREATE TABLE relationship (
  relationship_id        text PRIMARY KEY,
  kind                   text NOT NULL,            -- 'primary_care' | 'loyalty' | 'advisor' | ...
  persona_a_id           text NOT NULL,
  persona_b_id           text NOT NULL,
  scope                  text[] NOT NULL,          -- which actions persona_a may take on persona_b's data
  status                 text NOT NULL,            -- ACTIVE | SUSPENDED | TERMINATED
  consent_ref            text NOT NULL,
  created_at             timestamptz NOT NULL,
  attested_at            timestamptz,              -- last annual attestation
  terminated_at          timestamptz,
  cross_tenant           boolean DEFAULT false,
  source_tenant_id       text,                     -- when cross_tenant=true
  target_tenant_id       text                      -- when cross_tenant=true
);

CREATE INDEX ix_rel_a ON relationship (persona_a_id, status);
CREATE INDEX ix_rel_b ON relationship (persona_b_id, status);

4 · SDK API Surface · key endpoints

v3.1 The canonical entry point for reading identity is resolveIdentityContext(token) from sdk-identity-resolver. Every other layer-attribute read in any non-resolver SDK is forbidden by lint (Opinionated Constraint OC-4, Architecture §3A). The resolver reads from the Identity Projection store (§2B) on the hot path with p99 ≤ 0.5ms warm; falls back to live six-layer resolve only on miss. The endpoints below are the mutation surfaces — reads happen through the resolver.
// The one read endpoint that matters
import { resolveIdentityContext } from '@projexlight/sdk-identity-resolver';

const ctx = await resolveIdentityContext(token);
// ctx.person_id · ctx.persona_id · ctx.tenant_id · ctx.bu_ancestors ·
// ctx.effective_role_closure · ctx.active_consents · ctx.rebac_edges ·
// ctx.admin_pool_index · ctx.projection_version · ...
// — snapshot is stable for the request lifetime; audit + policy reference projection_version

4.1 · sdk-identity

// First login per app — mints app_identity if missing
POST /v1/identity/login
  { credential, app_id }
  → { jwt, person_id, app_identity_id }

// JWT carries the full tuple
GET /v1/identity/me
  → { person_id, app_identity_id, memberships: [...], personas: [...] }

// Alias graph stitch (admin only)
POST /v1/identity/merge
  { primary_person_id, alias_person_id }
  → { primary_person_id, merged_alias_count }

4.2 · sdk-persona

// Create a tenant membership (e.g., patient joins hospital)
POST /v1/persona/membership
  { app_identity_id, tenant_id, roles: ['patient'] }
  → { tenant_membership_id, persona_id }

// Add a persona to an existing membership
POST /v1/persona/{tenant_membership_id}/persona
  { kind: 'doctor' }
  → { persona_id }

// List a person's complete stack (across apps and tenants)
GET /v1/persona/stack?person_id=pers_001
  → {
      app_identities: [...],
      memberships:    [...],
      personas:       [...]
    }

4.3 · sdk-engagement

// Open an encounter (issues encounter key)
POST /v1/engagement/encounters
  {
    tenant_id, app_id, kind: 'visit',
    participants: [
      { persona_id: 'pers_001_patient', role: 'patient' },
      { persona_id: 'pers_002_doctor',  role: 'attending' }
    ],
    address_id, scheduled_at
  }
  → { encounter_id, encounter_key_arn, state: 'OPEN' }

// Time-bounded grant for non-participants
POST /v1/engagement/encounters/{encounter_id}/grants
  {
    grantee_persona_id, scope: ['vitals.read'],
    expires_at, reason: 'shift_assignment'
  }
  → { grant_id }

// Close an encounter (payload retained, encounter still readable until seal)
POST /v1/engagement/encounters/{encounter_id}/close
  → { state: 'CLOSED', closed_at }

// Seal (retention expiry — shreds the encounter key)
POST /v1/engagement/encounters/{encounter_id}/seal
  → { state: 'SEALED', sealed_at, key_shredded: true }

4.4 · sdk-rebac

// Open a relationship (e.g., PCP assignment)
POST /v1/rebac/relationships
  {
    kind: 'primary_care',
    persona_a_id: 'pers_dr_smith',
    persona_b_id: 'pers_ravi_patient',
    scope: ['chart.read', 'rx.write'],
    consent_ref: 'cnst_01HXYZ'
  }
  → { relationship_id, status: 'ACTIVE' }

// Check if a relationship allows an action (used by Policy SDK)
POST /v1/rebac/check
  {
    actor_persona_id, target_persona_id, action: 'chart.read'
  }
  → { allow: true, via_relationship: 'rel_01HXYZ' }

// Terminate a relationship (e.g., patient changes PCP)
POST /v1/rebac/relationships/{relationship_id}/terminate
  { reason: 'patient_request' }
  → { status: 'TERMINATED', terminated_at }

5 · Lifecycle States

5.1 · Encounter

stateDiagram-v2
  [*] --> OPEN: encounter created
  OPEN --> IN_PROGRESS: first activity logged
  IN_PROGRESS --> CLOSED: closed by participant
  CLOSED --> SEALED: retention expires
  CLOSED --> IN_PROGRESS: reopened (within window)
  SEALED --> [*]
  note right of SEALED
    Encounter Key shredded
    Payload mathematically irrecoverable
    Metadata retained for analytics counts
  end note
    

5.2 · Relationship

stateDiagram-v2
  [*] --> ACTIVE: relationship created with consent
  ACTIVE --> SUSPENDED: anomaly · attestation overdue
  SUSPENDED --> ACTIVE: re-attested
  ACTIVE --> TERMINATED: either party terminates
  SUSPENDED --> TERMINATED: timeout
  TERMINATED --> [*]
    

5.3 · Tenant Membership

stateDiagram-v2
  [*] --> INVITED: invitation issued
  INVITED --> ACTIVE: accepted
  INVITED --> [*]: declined
  ACTIVE --> SUSPENDED: admin action
  SUSPENDED --> ACTIVE: reinstated
  ACTIVE --> DEPARTED: person leaves tenant
  SUSPENDED --> DEPARTED: timeout
  DEPARTED --> [*]
    

5.4 · Master Person

stateDiagram-v2
  [*] --> ACTIVE: created
  ACTIVE --> SUSPENDED: admin · fraud
  SUSPENDED --> ACTIVE: reinstated
  ACTIVE --> ERASED: erasure request honored
  SUSPENDED --> ERASED: erasure request honored
  ERASED --> [*]
  note right of ERASED
    Person Key shredded
    All downstream layers undecryptable
    Tombstone retained for audit
  end note
    

6 · Compliance Mapping

Where each regulated field type lives — by layer, pool, and key.

RegulationField classLayerPoolKeyErasure mechanism
HIPAA (US PHI)Clinical notes, vitals, labs, dx codesL5 Encounter payloadApp pool (Healthcare)Encounter KeySeal encounter → key shred
HIPAAMRN, allergies, blood typeL4 Persona extensionApp poolTenant KeyTenant offboarding
DPDP (India)Name, phone, email aliasesL1 alias graphAdmin pool (home region)Person KeyRight-to-erasure shreds Person Key
DPDPAadhaar, PAN, DLL1 Secure Data bandAdmin pool (IN region pinned)Person Key + per-field envelopeField-level shred
GDPR (EU)Personal data + processing recordL1 + L2 + L4 + consentEU-region admin + app poolsPerson + Tenant keysPortability export from MDM endpoint; erasure shreds Person Key
PCI DSSCard tokens (no raw PAN)L1 Secure Data band, PCIAdmin poolPerson Key + tokenizerToken revoke via processor; field shred locally
SOC 2Audit ledgerPer-pool audit chainEvery poolPool KEK (immutable)Retention-policy driven (typically 7y)
RBI / SEBIInvestor stake, K-1sL4 Persona extension + L5 Encounter (cap call, distribution)App pool (OneEstate)Tenant Key + Encounter Key8y retention, then seal
Data residencyAll PII at-restL1–L6Region-pinned pools onlyPer-region root CMKRegion-bounded by pool placement
The compliance benefit of layering. A regulator asking "show me every place we hold this patient's data" gets answered by a single graph traversal — person_id → app_identities → memberships → personas → encounters/relationships. A regulator asking "prove you erased patient X's data" gets answered by "we shredded the Person Key on YYYY-MM-DD; here is the audit chain entry". No table scan, no shard reconciliation.
The billing benefit of layering v3.1. Every metered API call (see Architecture §10A) carries the full six-layer tuple — org_id · app_id · tenant_id · bu_id · persona_id · encounter_id — from the JWT. That lets sdk-billing split a tenant's bill per app, per business unit, per persona-kind (e.g., "your clinicians cost X, your patients cost Y"), and per encounter ("Hospital A's Q3 visits cost Z in platform fees"). AWS, GCP, Azure, and Stripe all bill against a flat customer/account identity and rely on user-defined tags for any further split. We get this for free because the six-layer stack is the identity, not a metadata afterthought.

7 · Scenario Walkthroughs · API + Tables

7.1 · Healthcare — Ravi visits Dr. Smith at Hospital A

// 1. Ravi (already a person) joins Hospital A
POST /v1/persona/membership
  { app_identity_id: 'apid_ravi_hc', tenant_id: 'ten_hospA', roles: ['patient'] }
→ { tenant_membership_id: 'tmb_ravi_hA', persona_id: 'pers_ravi_hA_patient' }

// 2. Dr. Smith is a doctor at Hospital A
// (persona already exists: pers_drsmith_hA_doctor)

// 3. PCP relationship is established
POST /v1/rebac/relationships
  {
    kind: 'primary_care',
    persona_a_id: 'pers_drsmith_hA_doctor',
    persona_b_id: 'pers_ravi_hA_patient',
    scope: ['chart.read', 'rx.write'],
    consent_ref: 'cnst_pcp_ravi'
  }
→ { relationship_id: 'rel_pcp_ravi_smith' }

// 4. Visit is scheduled
POST /v1/engagement/encounters
  {
    tenant_id: 'ten_hospA', app_id: 'healthcare', kind: 'visit',
    participants: [
      { persona_id: 'pers_ravi_hA_patient', role: 'patient' },
      { persona_id: 'pers_drsmith_hA_doctor', role: 'attending' }
    ],
    scheduled_at: '2026-04-12T10:30Z'
  }
→ { encounter_id: 'enc_visit_42', state: 'OPEN' }

// 5. Dr. Smith reads Ravi's chart (Pool Router → app-healthcare-007)
GET /v1/healthcare/chart/pers_ravi_hA_patient
  Authorization: Bearer 
→ Policy: ABAC ✓
→ ReBAC: rel_pcp_ravi_smith ACTIVE ✓
→ Encounter: enc_visit_42 IN_PROGRESS ✓
→ chart returned

// 6. Visit closes
POST /v1/engagement/encounters/enc_visit_42/close
→ state: 'CLOSED'

// 7. 6 years later, retention triggers seal
POST /v1/engagement/encounters/enc_visit_42/seal
→ state: 'SEALED', encounter key shredded

7.2 · eCommerce — Anita places an order at BookStore

// 1. Anita joins BookStore tenant (e-com app)
POST /v1/persona/membership
  { app_identity_id: 'apid_anita_ec', tenant_id: 'ten_bookstore', roles: ['buyer'] }
→ { tenant_membership_id: 'tmb_anita_bs', persona_id: 'pers_anita_bs_buyer' }

// 2. Order placed (one encounter)
POST /v1/engagement/encounters
  {
    tenant_id: 'ten_bookstore', app_id: 'ecommerce', kind: 'order',
    participants: [
      { persona_id: 'pers_anita_bs_buyer', role: 'buyer' }
    ],
    metadata: { order_no: 'BS-2026-04-998' }
  }
→ { encounter_id: 'enc_order_BS998' }

// 3. Payment + line items hang off encounter
// Payment SDK uses Anita's PCI token from L1 Secure Data
// Order body is encrypted under encounter key

// 4. Later, Anita orders from ElectroMart
POST /v1/persona/membership
  { app_identity_id: 'apid_anita_ec', tenant_id: 'ten_electromart', roles: ['buyer'] }
→ { tenant_membership_id: 'tmb_anita_em', persona_id: 'pers_anita_em_buyer' }

// 5. ElectroMart staff cannot see BookStore orders.
// Pool Router refuses cross-tenant query.
// Anita aggregating her own data needs an analytics-export consent.

7.3 · Marketplace — two-sided contractor / homeowner platform v3.1

Storm-damage repair marketplace. Sam (Homeowner persona at ten_marketplace) posts a job; Bob's Roofing (Contractor persona at the same tenant) bids; platform holds funds in escrow; dispute resolution is an encounter the platform staff joins. Exercises: two personas at one tenant with opposing interests, escrow via sdk-payment, reputation across encounters, dispute encounter with a third-party participant.

// 1. Sam and Bob's Roofing both hold personas at the marketplace tenant
//    Sam: pers_sam_mp_homeowner   Bob: pers_bob_mp_contractor
// 2. Sam posts a job — a Listing encounter
POST /v1/engagement/encounters
  { kind: 'listing', tenant_id: 'ten_marketplace',
    participants: [{ persona_id: 'pers_sam_mp_homeowner', role: 'requester' }] }
→ { encounter_id: 'enc_listing_223' }

// 3. Bob bids — bids are sub-encounters referencing the listing
POST /v1/engagement/encounters
  { kind: 'bid', parent_encounter_id: 'enc_listing_223',
    participants: [{ persona_id: 'pers_bob_mp_contractor', role: 'bidder' }] }
→ { encounter_id: 'enc_bid_BR_99' }

// 4. Sam accepts → Job encounter opened; sdk-payment escrows funds
POST /v1/engagement/encounters
  { kind: 'job', parent_encounter_id: 'enc_listing_223',
    participants: [
      { persona_id: 'pers_sam_mp_homeowner', role: 'payer' },
      { persona_id: 'pers_bob_mp_contractor', role: 'provider' }
    ],
    payment_ref: 'pay_escrow_998' }
→ { encounter_id: 'enc_job_998' }

// 5. Bob captures evidence (sdk-evidence) — each capture stamped with encounter_id
// 6. Dispute? sdk-approval routes to platform support; a 3rd persona joins enc_job_998
//    as 'arbitrator'; ReBAC grants temporary read on evidence chain
// 7. Resolution closes the job encounter; escrow releases; ratings written to
//    persona-extension on both sides; encounter key sealed at the end of the
//    dispute window for retention

7.4 · B2B2C subscription — tenant's app charges its end users v3.1

A SaaS tenant (FitStudio) uses our platform to run their fitness app; their end users subscribe to FitStudio at $20/month. Platform bills FitStudio for SDK usage; FitStudio bills its end users. End users are personas at FitStudio's tenant, not direct platform customers.

// 1. End user signs up via FitStudio's app
//    Identity SDK mints person + app identity + tenant membership + persona
//    Persona kind: 'member' at tenant_id='ten_fitstudio'
// 2. FitStudio's app calls sdk-payment for end-user's monthly $20 charge
//    actor: { kind: 'tenant_admin', tenant_id: 'ten_fitstudio' }
//    payee: tenant's own Stripe Connect account (not ours)
// 3. sdk-meter records that call as 'payment.charge.subscription' SKU
//    against ten_fitstudio (the platform-tenant)
// 4. End of month: sdk-billing invoices FitStudio for all SDK usage
//    FitStudio receives one invoice line: '847k API calls · $1,245'
//    FitStudio's revenue from end-user subscriptions is theirs, not ours
// 5. The end user appears in our system only as a persona;
//    they never see a Projexlight invoice

7.5 · Multi-currency consolidation — global tenant with regional revenue v3.1

GlobalCorp operates in US, EU, IN with revenue in USD, EUR, INR. Their fiscal year ends March 31. They want one consolidated quarterly P&L in USD. Exercises: fiscal-period entity, multi-region pool placement, geo-node + currency dimensions, warehouse aggregation with FX normalization.

// 1. GlobalCorp configures its fiscal calendar
PUT /v1/tenant/fiscal-period
  { tenant_id: 'ten_globalcorp', fiscal_year_start: '04-01',
    base_currency: 'USD',
    fx_source: 'platform_daily_close' }

// 2. Each region's encounters carry geo_node_id + transaction currency
//    US sales encounter: geo_node_id='geo_us', currency='USD'
//    EU sales encounter: geo_node_id='geo_eu', currency='EUR'
//    IN sales encounter: geo_node_id='geo_in', currency='INR'
//    Each lives in its region's app pool (residency)

// 3. Quarterly close (cron, T+1 day after fiscal quarter end)
//    sdk-analytics rollup job (warehouse only — no live cross-pool join):
//      SELECT geo_node_id, SUM(amount * fx_to_usd_at_txn_date) AS usd_revenue
//      FROM warehouse.encounters_by_tenant
//      WHERE tenant_id = 'ten_globalcorp'
//        AND fiscal_period_id = 'fp_globalcorp_FY24_Q1'
//      GROUP BY geo_node_id;

// 4. Output: consolidated P&L in USD, broken down by geo_node
//    Audit trail: which FX rates applied per transaction; reproducible
//    Showback: per-BU and per-geo splits available via sdk-billing

7.6 · Cross-tenant referral — Hospital A refers patient to Hospital B v3.1

Ravi's PCP at Hospital A refers him to a specialist at Hospital B. Without explicit cross-tenant relationship, B has no access to A's chart. Exercises: cross-tenant Relationship (L6 with cross-tenant key), Encounter Grant scoped to specific records, consent-driven hand-off, commission-tracking if the referral is paid.

// 1. Dr. Smith at Hospital A initiates the referral
POST /v1/rebac/cross-tenant-relationship
  { kind: 'referral',
    source: { persona_id: 'pers_drsmith_hA_doctor', tenant_id: 'ten_hospA' },
    target: { tenant_id: 'ten_hospB', persona_kind: 'specialist' },
    subject: { persona_id: 'pers_ravi_hA_patient' },
    scope: ['chart.read.subset', 'imaging.read'],
    consent_ref: 'cnst_ravi_refer_hB',
    expires_at: '2026-09-01' }
→ { relationship_id: 'rel_referral_ravi_hAhB', cross_tenant_key_id: 'ck_998' }

// 2. Hospital B's intake team accepts the referral
//    A specialist persona at B is matched: pers_drjones_hB_specialist
PUT /v1/rebac/cross-tenant-relationship/rel_referral_ravi_hAhB
  { accepted_by: 'pers_drjones_hB_specialist' }

// 3. Dr. Jones can now read Ravi's chart subset
//    On read: ReBAC check finds the cross-tenant relationship + active consent
//    Decryption uses cross-tenant relationship key (re-encrypted from A's tenant key)
//    Every read audited in BOTH ten_hospA and ten_hospB audit chains

// 4. Visit at Hospital B opens a new encounter under ten_hospB
//    pers_ravi_hB_patient created (separate persona at Hospital B)
//    Persona-extension link tags rel_referral_ravi_hAhB for provenance

// 5. If A and B have a commercial referral agreement
//    sdk-billing applies commission rule: % of B's revenue → A's reseller-style payout
//    Tracked via sdk-meter actor.relationship_ref

7.7 · Tenant impersonation by support v3.1

Support engineer Priya needs to debug an issue in Tenant X's account. Direct DB access is forbidden. She uses scoped, time-bounded, consent-required impersonation; every action under impersonation is loudly audited and visible to the customer.

// 1. Tenant X granted "support impersonation" consent during contract signing
//    Stored as a tenant-level standing consent: cnst_support_impersonation
//    Tenant admin can revoke at any time from their console

// 2. Priya opens a support ticket and requests impersonation
POST /v1/identity/impersonation/request
  { support_engineer: 'usr_priya',
    target_tenant_id: 'ten_X',
    reason: 'investigating ticket SUP-998',
    requested_scope: ['read:audit', 'read:billing', 'read:user.list'],
    duration_minutes: 30 }
→ { request_id: 'imp_req_223' }

// 3. Approval routing via sdk-approval
//    Manager approves → if scope includes write, also requires customer approval
//    Customer X's primary contact gets a notification + must approve in-app

// 4. Approved → sdk-identity mints an impersonation JWT
//    Claims: actor.kind='support_impersonator',
//            actor.real_user='usr_priya',
//            actor.target_tenant='ten_X',
//            expires_at=now+30min
//    JWT cannot be silently refreshed; expires hard

// 5. Every call under this JWT
//    - tagged in audit with both actor.real_user AND impersonation flag
//    - surfaced LOUDLY in customer's audit-read API (red banner)
//    - sdk-meter records usage under platform overhead, not customer's bill

// 6. End of session: certificate of impersonation generated
//    Lists every action, every record touched, every reason
//    Stored permanently in tenant's audit chain