Access as a graph traversal, not a column check
Organizations are rows in one entities table, typed by a MySQL enum with eleven values — regulatory bodies, health plans, MCOs, ACOs, IPAs, PHOs, MSOs, TPAs, FQHCs, individual providers and PACE organizations. They are linked by a closure table, entity_relationships, keyed on ancestor, descendant and relationship type, with a depth column where zero means self and one means direct parent.
A closure table pre-computes every ancestor and descendant pair, so the question 'which entities can this user reach' is an indexed lookup rather than a recursive walk at request time. Relationship types are first-class values — hierarchy, delegation, contract, value-based contract, contracted provider — which means a delegation is a row you can query, expire and audit, not a boolean on an organization.
Writes to the graph are all-or-nothing. Creating a relationship inserts the self-link and one row per ancestor of the new parent inside a single transaction or nested savepoint, so a partial closure state cannot be observed by a concurrent reader. The inserts use MySQL's INSERT IGNORE, which makes re-running the operation against already-seeded rows safe rather than an error.
- entity_type has exactly one source of truth — the MySQL enum. A test fails the whole suite if the Pydantic literal, the frontend list or the docs drift from it.
- A third table, entity_data_access, holds per-user grants: role, permissions, delegated scopes and reachable entity ids, unique per user, entity and role.
