Skip to Content
Entity ModelThe layer cake

The entity model: the layer cake

Every table, foreign key, and migration decision in the OpsDNA monorepo hangs off one canonical entity model, documented normatively in DATA-MODEL.md at the repo root. This page explains that model — the “layer cake” from tenant down to partner account — and why it is shaped the way it is. If you are about to add a table or FK, read DATA-MODEL.md itself first; this page is the orientation tour.

The model answers a deceptively hard question for venture fund accounting: who is who? A VC firm is simultaneously a SaaS customer (a tenant), a business identity (an organization), a set of raises (funds), a pile of juridical parties (legal entities — masters, feeders, SPVs, the ManCo, the GP entity, every LP), a set of books we keep (accounting entities), and a set of investors holding accounts in vehicles. Earlier iterations of the schema overloaded tables to play several of these roles at once — ledger_scopes was “the entity”, fund_structures was “the fund” — and ADR-0007 (docs/adrs/0007-accounting-entity-fund-and-identifier-policy.md) recast that into the explicit spine below before GA closed the one-way doors.

The design center is DATA-MODEL rule 2: entity.legal_entity is the substrate — every juridical party gets exactly one row — and roles are optional, symmetric 1:1 extensions, not subclasses. A legal entity can be a vehicle (entity.fund_vehicle), keep books (entity.accounting_entity), and invest (entity.investor) — all three at once. That is precisely how “Fund I’s master invests in an SPV” works with no special case: the master gains an investor extension and a partner account in the SPV’s vehicle.

The cake

Layer by layer (physical tables live in the entity PostgreSQL schema):

  • entity.tenant — thin: isolation, billing, lifecycle, and the RLS perimeter. tenant_mode is direct_customer | reseller | internal | sandbox; the business archetype lives one layer down. See Tenancy and RLS.
  • entity.organization — the business/collaboration identity (organization_kind: vc_gp_firm | startup | institutional_lp | law_firm | fund_admin). Service provisioning keeps it one-per-tenant today, but the catalog deliberately has no uniqueness on tenant_id so multi-organization tenants stay an additive option (ADR-0023). The WorkOS org link is optional and 1:N.
  • entity.fund — the fund complex / raise (name, number, vintage): the LP-facing reporting anchor. It owns nothing — no books, no PCAP, no per-LP economics. Cross-vehicle governance (investment period, key-person, recycling caps) attaches via entity.fund_policy_version.
  • entity.legal_entity — every juridical party, always owned by exactly one organization (rule 1). Identity is split into legal_entity_category (fund_vehicle, operating_company, gp_entity, investor_party, portfolio_company, external_fund, service_provider, other) and legal_form (lp, llc, corporation, trust, …) — an ADR-0007 v2 correction to the old single kind.
  • The three extensionsentity.fund_vehicle (vehicle role master/feeder/blocker/parallel/spv/co_invest/continuation, consolidation treatment, is_primary), entity.accounting_entity (the ledger anchor and hash key — the recast of the old ledger_scope; see /persistence and /fund-accounting), and entity.investor.
  • entity.partner_account — one investor’s account in one vehicle, UNIQUE (tenant_id, vehicle_legal_entity_id, account_number). This is the per-LP economic spine, and it has no role column (ADR-0011): GP-ness derives from the investor’s legal_entity_category, and economics come from entity.partner_class plus the applicable entity.vehicle_ruleset_version. lp_account is just an application view over non-GP accounts.

Why this shape (the invariants)

The nine core rules in DATA-MODEL.md all defend against the same failure mode: two places that can disagree about one fact, especially a fact that moves money.

  • One owner (rule 1) and fund owns nothing (rules 3/5): economics — fees, carry, waterfall, PCAP, partner classes — attach to the vehicle, because that is the grain at which LPAs actually differ (feeder terms vs master terms). The fund is a thin grouping; an SPV is a vehicle in its own single-vehicle fund; a ManCo is a fundless operating_company with books.
  • Extensions, not subclasses (rule 2): keeps the master-invests-in-SPV and feeder-holds-position-in-master cases as ordinary rows instead of special tables — see Fund-structure wiring.
  • No authored duplicates of derivable facts (rule 4, ADR-0018): general_partner_of and invests_in edges are projected from partner accounts at read time, never written where derivable.
  • Deterministic spine, frozen parentage (rules 6/9): the whole spine is rebuilt byte-identically by the Acceptance Lab, so ids are deterministic and parentage FKs are insert-time identity choices — see Identifier policy.

A concrete worked example from DATA-MODEL.md: CalPERS as an LP is one legal_entity(category=investor_party, legal_form=exempt_lp) + an investor extension + a partner_account in Fund I’s QP feeder + a membership in the applicable LP partner_class. The GP’s commitment lives in the same partner_account table — GP-ness is the entity category, and a 0%-carry class on the GP commit is a partner_class fact, never a role flag.

-- "Which vehicles does Fund I have, and which keep books?" SELECT le.display_name, fv.vehicle_role, fv.is_primary, ae.accounting_entity_id IS NOT NULL AS has_books FROM entity.fund_vehicle fv JOIN entity.legal_entity le ON le.tenant_id = fv.tenant_id AND le.legal_entity_id = fv.legal_entity_id LEFT JOIN entity.accounting_entity ae ON ae.tenant_id = fv.tenant_id AND ae.legal_entity_id = fv.legal_entity_id WHERE fv.tenant_id = :tenant AND fv.fund_id = :fund;

Pitfalls. (1) Do not attach economics to entity.fund — it owns nothing; if a table wants a fund_id FK for money, it almost certainly wants a vehicle legal_entity_id instead. (2) Do not add a role column to partner_account or author a general_partner_of edge where a partner account exists — both are the ADR-0011 dual-identity smell. (3) Spine parentage is immutable after insert (rule 9): corrections are superseding rows, never in-place FK rewrites. (4) Never contradict DATA-MODEL.md without an ADR — and never edit an applied migration; migrations are forward-only.

For the exhaustive table inventory use docs/schema-atlas.html in the monorepo; decision history is ADR-0006/0007 (spine + ids), ADR-0011 (no roles), ADR-0018 (routing), ADR-0020/0038 (the CRM relationship layer above the spine), ADR-0023 (decoupled tenancy).

Last updated on