Files
member-console/design/companion.md
T
cgalo5758 568383b55e Reorganize status docs and add identifier docs
Move settled investigation records into
`status/explorations/<topic>-<date>/` with README indexes,
promote durable conventions to `docs/`, and add
`docs/identifiers.md` plus upstream Doc 46 person-name
provenance documents. Update cross-references and archive
resolved issues.
2026-08-29 21:15:31 -05:00

146 KiB
Raw Blame History

Data Model Development Companion

Version 16.0 Companion to: Data Model Reference v16.0

This document tracks the development state of the data model: resolved decisions, open issues with full problem descriptions, exploratory documents, and policy documents. The Data Model Reference is the stable specification; this document records how and why it reached its current state.

Module-scoped companions available. Each module in modules/ has a companion.md that provides a module-scoped view of the decisions and issues relevant to that module. Module companions reference decision numbers from this document — they do not duplicate decision text. This document remains the authoritative, complete record of all decisions. (Note: the former resource-pools module was renamed to entitlements in v11; see Decision 109.)


Resolved Decisions

# Decision Summary Reference
1 Resource pool model Pools bridge billing and resources via provisions and assignments. Documents 5, 5.1
2 Pool provisions with exclusive FKs Three nullable FK columns with a check constraint. Document 6
3 Patronage scope Patronage attributed to patrons and products, not workspaces. Document 5.2
4 Metered usage routing Pool-level metered configuration with billing targets and spending controls. Document 5.2
5 Multi-item subscriptions Subscriptions contain subscription_items, each binding a price at a quantity. Document 6
6 Three access mechanisms Subscriptions, purchases, and grants produce pool provisions. Document 6
7 Product entitlement rules Product-to-entitlement mapping is a separate configuration concern. Document 6
8 Stripe references removed Payment processor references live in integration schemas. Document 6
9 Role inheritance removed Flat permission arrays. Inheritance deferred. Initial design review
10 Subscription semantics Subscriptions are a billing mechanism, not the sole path to product access. Document 6
11 Invitation fields removed from persons Invitation state managed by a dedicated entity (Issue 2). Initial design review
12 Actor model: four actor types Persons, PATs, service accounts, and system. Document 1
13 Service accounts locally-managed API keys stored locally with optional Keycloak linkage. Document 1
14 Exclusive FKs on role_assignments person_id OR service_account_id, never both. Document 1
15 Audit log actor model actor_type identifies identity; actor_credential_type identifies authentication method. Document 1
16 Permission format: resource:action 35 permissions across 17 resource surfaces. Document 3
17 Additive-only permission resolution No deny rules. Union semantics. Document 3
18 Org membership separate from role assignments Membership is belonging; assignments are scoped refinements. Document 3
19 Platform administration via platform organization Cooperative is an organization. Platform admins have platform_admin role. Document 3
20 Role assignment scope: exclusive arc Three exclusive FK columns for scope. Document 7
21 Grant recipient: exclusive arc Three exclusive FK columns for recipient. Document 7
22 Patronage event source: partial exclusive arc source_payment_id and source_invoice_id FKs; source_type retained for non-table-backed types. Document 7
23 Audit log polymorphism: intentional exceptions Bare polymorphic associations on entity and credential references, with documented rationale. Document 7
24 Polymorphic pattern heuristic adopted Gate 0 + exclusive arc vs. CTI decision heuristic. Documents 7, 7.1
25 Product entitlement rules: CHECK constraint Mutual exclusivity of boolean vs. numeric rule fields. Document 7.1
26 Persons user_id: partial unique index Enforces 1:1 relationship with users. Document 7.1
27 Organizations owner_person_id: conditional NOT NULL Personal organizations must have an owner. Document 7.1
28 Invoice source FK non-exclusivity subscription_id and purchase_id are intentionally non-exclusive. Document 7.1
29 Actor supertype: deferred with trigger conditions actors CTI not adopted. Triggers: actor type set expands beyond two table-backed types, or a third table requires a structural actor FK. Document 7.1
30 Primary key strategy: UUIDv7 All tables use UUIDv7. External APIs should use opaque external IDs. Document 8
31 GDPR erasure via anonymization Comprehensive anonymization protocol. persons gains anonymized terminal state. Document 9
32 Person merges tracked structurally person_merges table records merge provenance. Document 9
33 ON DELETE RESTRICT as default Universal default. Exceptions require documented justification. Document 9
34 JSONB governance policy adopted PII categorically prohibited in all JSONB columns. No exceptions. Issue 17 Policy
35 Invoice billing address: inline relational columns JSONB billing_address replaced by six relational columns. Decision Analysis 35
36 Retention hold mechanism retention_hold flag on persons; retention_holds tracking table. Anonymization gains pre-execution retention check. Document 10
37 Partial erasure capability Anonymization protocol supports field-level granularity. partially_erased added to persons status. Document 10
38 Phased retention compliance Phase 1: US structural hooks. Phase 2: pre-EU GDPR workflows. Phase 3: cooperative equity integration. Document 10
39 Single invitations table with scope-aware exclusive arcs Handles all invitation scenarios (email/person × org/workspace) in one entity. Progressive identity resolution for invitee (inclusive OR). Document 11
40 Invitation fields removed from org_members invited_by_person_id, invited_at, accepted_at, and pending status removed. Replaced by invitation_id FK for provenance. Memberships are born active. Document 11
41 Invitation token follows credential conventions mc_inv_<random> format. Hash-and-prefix pattern consistent with PATs and service account keys. Token regenerated on resend. Document 11
42 Temporal strategy: inline timestamps + selective actor attribution + audit log history Approach A as primary (26 timestamp columns across 16 tables). Approach D selectively applied to governance-sensitive entities (14 actor FK columns). Audit log serves as complete history for repeated transitions. Document 12
43 Entitlement toggle history delegated to audit log boolean_entitlements table does not gain temporal columns for is_enabled toggles. Toggle history lives exclusively in the audit log. Document 12
44 Audit log retention must satisfy temporal query requirements Formal dependency: audit log retention ≥ 7 years (US tax safe harbor). Established by Decision 78: security and compliance tiers retain for 7 years; critical retains for 20 years. Document 12
45 Status transition recording convention Status changes recorded in audit log changes JSONB as {"status": {"from": "X", "to": "Y"}}. Application-layer convention. Document 12
46 Materialization pipeline: synchronous, full re-evaluation materialize_pool_entitlements(pool_id) called within provision-change transactions. Idempotent. Full re-evaluation, not incremental. Document 13
47 Entitlement resolution: direct query No materialized view or cache for initial implementation. Hot-path indexes support the join traversal. Document 13
48 Quota stacking policy stacking_policy column added to product_entitlement_rules. Values: additive, maximum, replace. Document 13
49 Numeric entitlement definition/state separation numeric_entitlement_usage table (originally quota_usage). Mutable usage state separated from entitlement definition. Eliminates write contention between materialization and usage increments. Document 13
50 Numeric entitlement provenance via contributions numeric_entitlement_contributions join table (originally quota_contributions) records per-provision contributions. Provenance is multi-valued; resource_limit is denormalized, recomputed from contributions by materialization. Document 13
51 Entitlement override: deferred No administrative override mechanism. Entitlements are a pure function of provisions and rules. Force-enable via grants. Force-disable via provision suspension. Document 13
52 Cross-org collaboration via workspace-scoped role assignments Workspace sharing across organizations is the primary cross-org collaboration mechanism. No new tables required. Mediated through existing role assignments and invitations. Document 14
53 Cross-org billing: deferred with structural affordance preserved pool_provisions.billing_account_id remains unconstrained. Governance mechanisms (acceptance, visibility, withdrawal) deferred until demand materializes. Document 14
54 Issue 7 status: reduced Reclassified from "scenario validation needed" to "deferred with trigger conditions." The model structurally supports cross-org billing; implementation governance is deferred. Document 14
55 Invoice line items entity adopted invoice_line_items table added. Every invoice has at least one line item. Document 15
56 Invoice totals retained as denormalized summaries subtotal, discount_amount, tax_amount, total on invoices are maintained by invoice generation and verified against line items. Document 15
57 Discounts: hybrid representation on invoices Per-line discount_amount for attributed discounts; line_type = 'discount' rows for invoice-level discounts. Document 15
58 Tax at line level tax_rate and tax_amount on each line item. Document 15
59 Product attribution on line items product_id FK enables direct patronage derivation from invoices. Document 15
60 Usage events retain invoice-level attribution Superseded by Decision 68. usage_events.billed and invoice_id removed; attribution via pending_charge_usage_events join table. Document 15 → Document 17
61 Three-layer discount model coupons + promotion_codes + discounts. Rule definition, code distribution, and active application are separate entities. Document 16
62 Discount scope: exclusive arc Three-way exclusive arc on redemptions: billing account, subscription, or line item. Document 16
63 Duration semantics: once, repeating, forever Matches Stripe and Polar duration models. duration_remaining tracks repeating discount consumption. Document 16
64 coupons.applies_to_products as UUID array Product restrictions stored as a typed array. Avoids a join table for a simple restriction set. Document 16
65 Stacking semantics deferred to implementation Schema is stacking-ready. Application-level order-of-operations deferred. Default: apply in created_at order. Document 16
66 Credit note discount reversal deferred Discount duration_remaining restoration on credit notes deferred until credit note entity is designed. Document 16
67 Pending charges entity adopted pending_charges table introduced. Bridges raw inputs and finalized outputs. Document 17
68 Usage event billing attribution via join table usage_events.billed and invoice_id removed. pending_charge_usage_events join table links events to charges. Supersedes Decision 60. Document 17
69 Invoice immutability boundary: draft → open Invoice line items mutable while draft, frozen upon open. Corrections via credit notes. Document 17
70 Four pending charge sources Subscription cycles, metered usage rating, proration, and ad hoc charges produce pending charges through distinct processes. Document 17
71 Credit grant entity adopted credit_grants table introduced. Prepaid or promotional credit allocations on billing accounts. Document 18
72 Credit transaction ledger adopted credit_transactions table introduced. Immutable, append-only ledger. Document 18
73 Dual topology support Invoice-time (Topology A) and real-time (Topology B) credit application. invoices gains credit_applied and amount_due fields. Document 18
74 Credits are not discounts Discounts reduce the obligation; credits reduce the collection. Different accounting treatment, different entities. Document 18
75 Credit application order Discounts → tax → total → credits → amount_due. Preserves revenue recognition and tax computation. Document 18
76 Credit-gated billing mode (prepaid-only enforcement) billing_accounts gains platform_billing_mode and customer_billing_mode. Most restrictive setting wins. Document 18
77 Audit log partitioning: monthly RANGE on created_at Composite PK (log_id, created_at). Monthly granularity chosen for index size, retention granularity, and archival unit alignment. timestamp renamed to created_at for partition key convention alignment. Document 20
78 Audit log retention: five tiers critical (20yr), security (7yr), compliance (7yr), operational (1yr), debug (90d). Classification at write time via tier column. Tier durations defined in audit_retention_policies table. Document 20
79 Audit log legal holds audit_legal_holds table blocks automated archival/anonymization of audit data within scope. Complements existing retention_holds (which protects person PII). Same compliance family, different targets. Document 20
80 Audit log indexing budget: 6 indexes 3 composite B-tree (entity lookup, actor lookup, action filter), 1 BRIN (time range), 2 partial (failed auth, high severity). Materialized from_status/to_status columns replace JSONB extraction. GIN on JSONB explicitly rejected for write amplification. Document 20
81 Audit log write architecture: hybrid sync + async Governance-critical events written synchronously in business transaction. Operational/debug events may use buffered batch inserts. Specific buffering implementation deferred. Document 20
82 Audit log archival: partition lifecycle pipeline Partitions transition through hot → warm → cold → frozen tiers. audit_archive_manifest tracks each partition's state. Cold/frozen stored as columnar files outside PostgreSQL. Specific file format and tools deferred to build time. Document 20
83 Audit log outbox for external consumers Transactional outbox pattern for delivering audit events to external systems (SIEM, analytics). Outbox INSERT in same transaction as audit event. Specific relay implementation deferred. Document 20
84 Audit schema separation Superseded. Originally placed audit tables in a separate audit schema. Reversed in v9: all domain tables, including audit tables, live in the core schema. Schema separation is reserved for integration schemas (per-provider), not for domain subsystems. Document 20, superseded by Decision 85
85 Core schema namespace Superseded by Decision 113. Originally placed all domain tables in a single core schema. Replaced by schema-per-module: each module gets its own PostgreSQL schema. Document 21, superseded by Decision 113 (Document 30)
86 Integration namespace: schema-per-provider Each external service gets its own PostgreSQL schema (stripe, polar, etc.). Integration schemas reference domain tables via schema-qualified FKs (e.g., REFERENCES billing.products(...)) but do not reference each other. Clean decommissioning via DROP SCHEMA CASCADE. Individual integration schemas are designed when each integration is built, not in the data model reference. (Updated: FK targets changed from core.* to module-qualified names per Decision 113.) Document 21
87 Payment methods, refunds, disputes as core entities Self-sufficiency principle: business questions ("does this account have a payment method?", "was this payment refunded?", "is this payment disputed?") must be answerable from core alone. Provider-specific details live in integration schemas. Document 21, Reference: Domain/Integration Boundary Methodology
88 Integration infrastructure: webhook_events and integration_outbox Shared webhook idempotency table (partitioned monthly, matching audit pattern) and transactional outbox for outbound integration triggers. Both live in the integration schema. (Updated: moved from core to integration per Decision 113.) Document 21
89 External ID conventions External provider IDs are stored as TEXT (not VARCHAR), never used as primary keys, and carry UNIQUE constraints scoped per provider configuration. Document 21
90 Billing execution engine: Stripe Billing (Phase 1) Stripe handles subscription lifecycle, invoice generation, payment processing, dunning, and tax for Phase 1. Core schema tables modeling these concepts become projections synced via webhooks. The integration boundary is explicit: Stripe owns generic billing execution; the core schema owns cooperative semantics. Strategic decision; no schema changes. Document 23
91 Temporal as cooperative-layer orchestration substrate Temporal orchestrates cooperative-specific billing workflows — patronage attribution, resource pool provision lifecycle, credit grant governance, usage aggregation — that bridge Stripe events and the core schema. Temporal is not an alternative to Stripe Billing; it executes what Stripe cannot. Temporal workflows absorb more billing execution responsibility progressively as the platform matures. Document 23
92 Billing table role classification under provider delegation Core schema billing tables classified as: Category A (authoritative, cooperative-specific: resource_pools, pool_provisions, pool_assignments, pool_ondemand_config, grants, patrons, patronage_events, credit_grants, credit_transactions, billing_accounts), Category B (projections synced from Stripe: subscriptions, subscription_items, subscription_changes, invoices, invoice_line_items, payments, payment_methods, refunds, disputes, products, prices, coupons, promotion_codes, discounts), Category C (reduced role: pending_charges, pending_charge_usage_events, purchases). Classification guides integration schema design and the eventual scope of Phase 3 custom execution. Note: grants subsequently moved from billing to entitlements (Decision 106); its Category A status is unchanged — it remains authoritative and not synced from any external system. Document 23
93 Cooperative billing as governance layer on generic mechanics The core architectural insight: cooperative-specific billing requirements (patronage attribution, grants as access mechanisms, promotional credit governance, credit-gated billing mode, dual billing topology) are governance and attribution concerns layered on top of generic billing mechanics — not alternative billing mechanics. This defines the integration boundary and determines which concerns are worth building vs. delegating. Document 23
94 Module boundaries align with domain concerns, with two cross-cutting modules Seven modules: identity, organization, entitlements, billing, cooperative (domain chain), audit, integration (cross-cutting). Module structure is a documentation/development concern, not a schema separation. Schema-separation clause superseded by Decision 113: each module now receives its own PostgreSQL schema. Module boundary definitions remain valid. Document 27, schema clause superseded by Decision 113 (Document 30)
95 Cooperative concerns remain within the billing module Patronage and credit governance retained as tagged sub-domain within billing. Grants subsequently moved to entitlements (Decision 106). Remaining scope flagged for revisiting (Issue 20). Document 27
96 Audit is a standalone module, not merged with integration Different purposes (compliance observation vs. external connectivity) despite shared patterns. Merging would conflate regulatory and operational concerns. Document 27
97 product_entitlement_rules remains in the billing module Superseded by Decision 107. Configuration-ownership principle overridden by definiteness test. Document 27
98 Entitlement rule type discriminators: boolean and numeric rule_type values renamed from 'entitlement'/'quota' to 'boolean'/'numeric'. Resolves genus/species ambiguity where "entitlement" served as both the umbrella concept (the entitlement system) and a species within it (binary capabilities). Grounded in terminological survey of 30+ platforms. Quotas and numeric allocations remain in a single table; reset_period distinguishes the behavioral variant. Terminology research
99 Limit and quota as distinct rule types rule_type values 'limit' (static allocation, no reset) and 'quota' (renewable consumption, with reset) replace 'numeric'. Both are children of the numeric genus. Both materialize into numeric_entitlements with entitlement_type discriminator. Terminology research, Issue 21 resolution
100 Credit inclusion as declarative product rule rule_type = 'credit' added to product_entitlement_rules. Materialization creates product-scoped credit_grants with source_provision_id provenance. Cross-module write from entitlements pipeline into billing entity documented. Issue 21 resolution
101 Resource access implied by entitlement or credit Access to a metered resource is implied by the existence of a quota entitlement or a credit balance for that resource. No separate "metered access" entitlement is needed. The definiteness test governs the boundary: entitlements are definite grants; pricing agreements are not entitlements. Issue 21 resolution
102 Provision lifecycle coupling for metered config and credits pool_ondemand_config gains provision_id FK; credit_grants gains source_provision_id FK. Both reference pool_provisions for lifecycle coupling and provenance. NULL for manually configured or manually created entries. Issue 21 resolution
103 Resource key namespace enforcement resource_keys reference table added. All resource-identifying columns (product_entitlement_rules.resource_key, boolean_entitlements.resource_key, numeric_entitlements.resource_key, pool_ondemand_config.resource_key, usage_events.resource_key) FK into resource_keys.resource_key. Prevents silent namespace mismatches between entitlement and on-demand systems. Issue 21 resolution
104 Dual-path access gating Entitlement system gates definite grants (boolean features, numeric limits, quotas, credits) — checked first. Billing metered config gates post-paid resources where no entitlement covers the resource — checked second. Every enforcement point must know which path to check. Issue 21 resolution
105 Naming cascade from entitlement taxonomy entitlementsboolean_entitlements, quotasnumeric_entitlements, quota_contributionsnumeric_entitlement_contributions, quota_usagenumeric_entitlement_usage. Field renames: feature_key/quota_keyresource_key, quota_valueresource_value, quota_per_unitresource_per_unit, quota_limitresource_limit, quota_identitlement_id. Aligns schema names with the established taxonomy. Terminology research, Issue 21 resolution
106 Grants moved from billing to entitlements grants table reassigned from billing to entitlements. Grants answer "what does this entity have?" (entitlements), not "who pays?" (billing). The definiteness test, structural dependency analysis, external platform survey, and module coherence all favor the move. pool_provisions.grant_id becomes intra-module. grants.product_id and grants.granted_to_billing_account_id become consumed interfaces from billing (existing pattern). No schema changes — module assignment is a documentation/development concern (Decision 94). Billing: 24 → 23 tables. Resource-pools: 10 → 11 tables. Partially addresses Issue 20 by removing grants from the cooperative extraction candidate set. Document 28
107 product_entitlement_rules moved from billing to entitlements Supersedes Decision 97. The definiteness test and governing-question test establish that product_entitlement_rules answers "what does a product definitively grant?" — an entitlement question — not "who pays?" Sole operational consumer is the entitlements materialization pipeline; billing never reads this table for financial purposes. product_entitlement_rules.product_id becomes a consumed interface from billing (same pattern as grants.product_id). product_entitlement_rules.resource_key becomes intra-module (resource_keys already in entitlements). Billing's asymmetric ownership anomaly resolved. No schema changes. Billing: 23 → 22 tables. Entitlements: 11 → 12 tables. Document 29
108 Cooperative module created patrons and patronage_events extracted from billing to a new cooperative module. Cooperative governance observes billing outcomes rather than participating in billing execution. The cooperative module answers "what value has this member contributed?" — a distinct governing question from billing's "who pays?" patrons.billing_account_id and patronage_events.source_payment_id/source_invoice_id/product_id become consumed interfaces from billing. No schema changes. Billing: 22 → 20 tables (combined effect of Decisions 106108). Cooperative module: 0 → 2 tables. Module count: 6 → 7. Status: early development; full cooperative accounting depth deferred to Issue 16. Document 29
109 Module renamed from resource-pools to entitlements The resource-pools module's intellectual center shifted through successive migrations: the v10 entitlement taxonomy (Decisions 98105), grants (Decision 106), and product_entitlement_rules (Decision 107). Eight of twelve tables are now explicitly entitlement-concerned. Industry convention (Stripe Entitlements, Lago, Kill Bill) uses "entitlements" for this domain layer. The rename aligns the module name with its governing question and its table majority. No schema changes. v11 session
110 pool_metered_config renamed to pool_ondemand_config; metered_key renamed to resource_key "Metered" in industry convention means "usage is measured" — quotas with numeric_entitlement_usage are literally metered. The prior naming falsely claimed a monopoly on metering for the post-paid path. "On-demand" captures uncommitted, post-paid consumption without implying exclusivity over measurement. metered_keyresource_key aligns with entitlement tables that already use resource_key. metered_key is retained in billing-native tables (pending_charges, invoice_line_items) where it legitimately describes billing-side charges. v11 session
111 Universal usage events with resolution_path discriminator All consumption paths — quota decrements, credit deductions, and on-demand charges — generate usage events. Previously, usage_events served only the post-paid path. The resolution_path field (VARCHAR(20): quota, credit, on_demand) discriminates which path resolved each event. billing_account_id becomes nullable (quota-resolved events have no direct payer). numeric_entitlement_usage.current_usage remains as the real-time enforcement counter (materialized projection of quota-path events). v11 session
112 Entitlement set abstraction (partially amended by Decision 137 — the grants direct-set arm is dropped and provisions additionally carry product_id; the set/product decoupling itself stands) product_entitlement_rules re-parented as entitlement_set_rules under new entitlement_sets table. Entitlement sets decouple capability specification from commercial packaging: a set defines "what is conferred," a product defines "how it is sold." Products reference a set via products.entitlement_set_id (dependency direction reversed: billing now consumes from entitlements). Grants may reference a set directly via grants.entitlement_set_id for ad hoc capability conferral without a commercial product intermediary, or retain product_id for audit clarity. pool_provisions.product_id replaced with pool_provisions.entitlement_set_id — always resolved at provision-creation time, keeping the materialization pipeline singular regardless of source type. No set composition (sets referencing sets); create new sets with combined rules instead. Entitlements: 12 → 13 tables. Total: 54 → 55 tables. v12 session
113 Schema-per-domain-module Each of the seven modules receives a PostgreSQL schema matching the module name (identity, organization, entitlements, billing, cooperative, audit, integration). The core schema is retired; public retains shared extensions only. Integration per-provider schemas (Decision 86) unchanged — provider FK targets updated from core.* to module-qualified names. Supersedes Decision 85; partially supersedes Decision 94 (schema-separation clause only; module boundary definitions remain valid). Document 30
114 Cross-module FKs retained with schema qualification All ~33 cross-module foreign keys preserved using schema-qualified REFERENCES syntax. Documentation-first project context: no running services, no separate connection pools. FKs enforce governance-critical invariants (actor attribution, financial provenance, org scoping). Dependency direction strictly acyclic (identity → organization → entitlements → billing → cooperative). Removal straightforward via existing interfaces.md catalogs if future service extraction demands it. Revisitation trigger: adoption of per-module connection pool isolation. Document 30
115 Per-schema PostgreSQL roles as recommended practice Three-role model per schema: owner ({schema}_owner), writer ({schema}_writer), reader ({schema}_reader). Cross-module access via reader role inheritance (e.g., entitlements_writer inherits organization_reader). Documented recommendation; becomes binding when a runtime database is provisioned. Document 30
116 Plan ladders introduced in billing plan_ladders (named set of mutually-exclusive plan products) and plan_ladder_tiers (ordered membership junction) added to billing. products.plan_ladder_id nullable FK added. The ladder is a catalog-shape construct declaring "these products are alternatives"; capability composition remains in entitlement_sets (Decision 112). Many-to-many via junction enables bundles (one product on multiple ladders). Billing: 20 → 22 tables. Document 31 (Issue 23 resolution)
117 Bidirectional product_type / plan_ladder_id CHECK (superseded by Amendment #3 — Decisions 120122) CHECK ((product_type = 'plan') = (plan_ladder_id IS NOT NULL)) on billing.products, introduced by Amendment #1 to enforce that the label and the structural fact cannot disagree. The deferred bundle-predicate refinement was closed by dissolution rather than refinement: Amendment #3 retires the plan_ladder_id column and the 'plan' enum value, eliminating both sides of the biconditional and the CHECK with them. Retained here for historical context. Document 31 Amendment #1; superseded by Amendment #3
118 pool_provision_ladders junction with GiST exclusion constraint (amended by Decision 138 — surrogate junction identity replaces the natural composite PK; the "sole procedural element" claim is superseded by the Decision 137 function-family enclosure) Junction table added to entitlements carrying (provision_id, plan_ladder_id, pool_id, status, activated_at, ended_at). Partial GiST exclusion constraint enforces at most one active provision per (pool, ladder) — a database-level invariant departing from every commercial billing platform surveyed, consistent with the project's preference for declarative enforcement. Denormalized columns kept in sync with parent pool_provisions by AFTER UPDATE trigger (sole procedural element of the design). Separates the commercial fact (provision) from the catalog-shape fact (ladder position) per Doc 31 §5. Supersedes the original Option A (multiple provision rows per bundle). Entitlements: 13 → 14 tables. Document 31 Amendment #1
119 Trial handling retained at price/subscription layer (partially amended by Decisions 138139, Doc 41 §10.3 — the surface list loses grant_reason = 'trial_extension'; trial surfaces are exclusively price- and subscription-owned; the junction-status "binary" description is separately corrected to the ternary the sync trigger has always propagated) No structural addition. prices.trial_period_days, subscriptions.trial_start/trial_end, status 'trialing', change types 'trial_started'/'trial_ended', and grants.grant_reason = 'trial_extension' are the authoritative trial surfaces. pool_provision_ladders.status remains binary (active/ended) — a trialing tier is an occupied tier from the ladder's perspective; the exclusion constraint applies uniformly. Kill Bill's catalog-phase model considered and rejected: imports a bi-temporal model that fights with subscription_changes, couples trial to plan rather than to price, and presents an unfavorable expressiveness-to-complexity ratio. Document 34 (Issue 25 resolution)
120 products.plan_ladder_id retired; junction universal The "single-ladder convenience path" from Amendment #1 is dropped. billing.plan_ladder_tiers becomes the sole and universal representation of product-ladder membership: single-ladder plans and multi-ladder bundles alike express cardinality through rows in the junction. A single-ladder plan is a plan whose tier-row set has cardinality one; no structural distinction separates it from a bundle beyond that cardinality. Removes a topology-privileging denormalization inconsistent with the platform's general-purpose stance toward unknown future cooperatives. Document 31 Amendment #3
121 Structural kind derivation via product_kinds view The 'plan' value is removed from the products.product_type domain (narrowed to `'addon' 'usage'
122 lifecycle_status added to products VARCHAR(20) column with CHECK-narrowed domain (`'draft' 'published'
123 Scheduled changes as a first-class intent entity New billing.subscription_scheduled_changes table (intent) distinct from the append-only subscription_changes and pool_provision_transitions (records). Carries change_kind, target price/quantity, effective_at/effective_trigger, status (scheduled/applied/superseded/canceled), and a backlink to the record produced on firing. Partial unique index enforces at most one pending change per (subscription, item, kind) — the intent-layer analogue of the pool_provision_ladders GiST exclusion. Immediate switches bypass it (inert when unused); deferral (125) and commitment-gating (126) compose onto it. Placement: billing, charter member of the subscription seam (Doc 38, Decision 127). Document 36 (Issue 26)
124 Proration-credit disposition: ledger default, cash exception billing_accounts.default_credit_disposition (ledger/cash, default ledger) with a per-event override on the scheduled change. A net downgrade credit routes to a credit_grant (category='paid', source_provision_id set) by default, or to a refund (Decision 87) when cash; credit_transactions.source_type gains 'proration'. No new money entities; cash egress gated behind explicit action per the uniform industry posture. Document 36 (Issue 26)
125 Tier-reduction reconciliation policy entitlements.entitlement_set_rules.tier_reduction_policy (block/defer/clamp/force_reduce, default defer), enforced at the locus that knows allocation: block as a switch-request precondition, defer via a boundary-triggered scheduled change (123), clamp/force_reduce at materialization. Term reserved distinct from the §6.1 on-demand "overage" pattern. Document 36 (Issue 26)
126 Commitment as subscription attributes gating downgrade timing billing.subscriptions gains commitment_end, commitment_renewal (auto_renew/expire), early_termination_policy (block/fee/allow) with a coherence CHECK. A future-dated commitment routes a downgrade by policy: block defers to commitment_end via (123); fee emits an early_termination_fee line item (new line_type value); allow is advisory. Makes "committed terms are the exception" data-driven. First-class Contract object and value-commitments deferred to Issue 24. Document 36 (Issue 26)
127 Subscription lifecycle kept within billing as a named internal seam Issue 24 resolved. The tables answering the agreement-in-force governing question (subscriptions, subscription_items, subscription_changes, subscription_scheduled_changes) and the commitment attributes of subscriptions are recognized as the subscription seam — a documented sub-domain reasoned about in isolation — and are not extracted into a subscriptions schema. The governing-question test finds a real but adjacent-and-generative distinction (subscriptions are the engine that produces billing facts, not a concern foreign to billing); the structural-dependency analysis is decisive against a physical split, since four billing financial-core tables hold inbound FKs to the seam (invoices, invoice_line_items, discounts, credit_transactions), so Decision 107's "constituency entirely outside the holding module" criterion is unmet; convention concurs. A seam is a conceptual module boundary honored in documentation without a separate schema — it grants isolation of reasoning at no structural cost. Reversible per the sharpened triggers (Doc 38 §9). Document 38 (Issue 24)
128 Document 36 placements confirmed final (confirming amendment) The placements Document 36 made provisionally pending Issue 24 — billing.subscription_scheduled_changes (123) and the commitment attributes on billing.subscriptions (126) — are final in billing as charter members of the subscription seam (127), designated to relocate as a unit only if a future investigation reverses Decision 127. No schema change; removes a provisional qualifier and adds a relocation-cohesion note. First-class Contract remains deferred (Doc 38 §9). Document 38 (Issue 24)
129 Provider as the contract noun; canonical registry billing.providers "Provider" is the incumbent term (genus spanning payment, provisioning, notification, tax — provider_kind discriminates the species). The soft strings are promoted to a lookup table: slug PK (VARCHAR(50), matching existing string values — zero-migration promotion), display_name, provider_kind CHECK, status (active/suspended/retired) with CHECK and suspended_at/retired_at carrying the soft-delete discipline. Homed in billing (conceptual address per Decision 140; physically core as built): not integration (Decision 87 self-sufficiency), not entitlements (would cycle against Decision 114), not a new schema (seam doctrine, Decision 127). As-built drift — missing status CHECK, dropped timestamps — deliberately not absorbed (Doc 44 §2); card-tracked. Documents 3940; ratified as amended by Document 44 (2026-08-21)
130 Resource-key ownership by column; bare keys; slug-prefix namespacing Nullable provider FK on entitlements.resource_keys; key strings stay bare and globally unique (no composite key, no stored dotted form — the dotted <provider>.<resource> form is retired, colliding with Decision 114 notation). Per the 9a refinement (§6.2): provider encodes fact-source cardinality (set ⟺ facts from exactly one provider; NULL ⟺ pooled or platform-native), metering is an independent third axis (NULL-and-metered stays expressible), and the namespacing rule is platform-owns-unprefixed / providers own <slug>_*, boot registration the backstop — ratified including the as-built slug-nesting strengthening. Three roles kept apart: bare key (machine), provider column (grouping), display_name (UI). Documents 3940; ratified as amended by Document 44 (2026-08-21)
131 Per-instance state provider-owned; per-provider transport; uniform webhook report-back Per-provisioned-instance identity and canonical state live in per-provider schemas (four of five boundary heuristics); the domain owns the count/limit and, only where a hot path demands it, a consumer-side read-only projection — a cache, never canonical. Transport as ratified is a per-provider capability decision (amending the draft's uniform outbox mandate, blessing the as-built contract): the outbox remains default and required where its transactional guarantee is load-bearing (payment-grade; Stripe); a provisioning provider whose workflows own their writes end-to-end may dispatch directly via Temporal (FedWiki, Discourse). Report-back stays uniform via integration.webhook_events; cross-time webhook dedup is a handler-level contract (the partitioned table cannot carry a global unique constraint — Doc 44 §3). The §6.1 domain-event trigger path is inexpressible in the ratified contract → Issue 32. Documents 3940; ratified as amended by Document 44 (2026-08-21)
132 Manifest-as-rows; provider_operations; registry-driven operator enumeration The manifest is rows contributed to existing structures: owned resource_keys rows and provider_operations rows (closed verb set, FK-anchored, enumerable), split by source of truth per §6.3 — capability rows boot-registered from a typed manifest (ProviderSource pattern; re-registration must not clobber operator-canonical operational rows), operational/commercial rows database-canonical. The operator panel enumerates the registry (WHERE provider_kind = 'provisioning'), with operator_surface_path absorbed as registry data (the route path is manifest-declared; rendering and chrome remain member-console code). Re-scoped out: the plan_ladders.provider catalog-position annotation — never implemented, not exercised, and incomplete for boolean-capability providers whose catalog position is set-rule-shaped; deferred with a named trigger (a ladder-shaped catalog contribution requiring provider attribution). Soft-string FK promotions remain designed-deferred (partition-maintenance sequencing). The §6.3 boot/lint declaration↔execution check remains normative and is as-built debt. Documents 3940; ratified as amended by Document 44 (2026-08-21)
133 Read-operation class; desired/observed projection; provider-declared state vocabulary The contract gains a read class — list/describe — by which the platform enumerates a provider's instances and states; the desired/observed projection (console intent, written immediately / provider-confirmed, lagging; gap rendered "pending") is required from site-selection read-only onward, consumer-side, fed by webhook push and read-op pull; billing-grade facts never source from it (Decision 87). As ratified, the mutating verb set is create / set_status / delete with per-provider lifecycle vocabularies declared in the manifest and persisted to billing.provider_states (state-set agreement validated at registration) — absorbing the as-built evolution: a closed verb set over an open, provider-declared state vocabulary. Set-shaped (converged) delivery is inexpressible in this projection and verb set → Issue 33. Documents 3940; ratified as amended by Document 44 (2026-08-21)
134 Product kind is a projection, dissolved rather than completed billing.product_kinds dropped. Each consumer reads its own surface: conferral reads billing.product_conferral_shapes (set, lifecycle, ladder ids and ranks); diagnostics read billing.product_shape (per-dimension columns — billing_shape, consumption_shape — with 'mixed' first-class and no cross-dimension precedence); the invoicing pipeline reads per-price columns (recurring_interval, usage_type); presentation reads display_category. One authoritative read path per consumer. Supersedes the Document 35 development invariant (Doc 41 §10.1). Document 41 (Issue 29; ratified 2026-08-21)
135 one_time and usage dissolve at the price grain No product-level derivation — the question is malformed at product granularity: one-time-ness is prices.recurring_interval IS NULL and usage-ness is prices.usage_type, each read per price by its only consumers. usage_pricing_requires_recurrence CHECK on billing.prices makes the one genuinely incoherent combination (a one-time metered/licensed price) unrepresentable. Document 41 (Issue 29; ratified 2026-08-21)
136 addon dissolves into presentation; product_type renamed and demoted Dependence ("addon-of") is not a catalog fact: no operation consults it, and the addon-revoke incident was caused by target resolution, not missing dependence modeling. products.product_type renamed to display_category, domain CHECK dropped, demoted to non-behavioral presentation metadata fenced by column comment; NonPlanTiers becomes unrepresentable, since no label remains whose value purports to describe structure. Document 41 (Issue 29; ratified 2026-08-21)
137 The conferral primitive Grants name products: grants.entitlement_set_id dropped, product_id NOT NULL (partially amending Decision 112's grants clause); ad hoc sets confer via internal wrap products (is_public = FALSE, published, priceless per the amended publication gate). Provisions name their product: pool_provisions.product_id denormalized at conferral (partially amending Decision 112's provisions clause). One enclosed five-member SECURITY DEFINER family — confer / end_conferral / sync_source_status / align_conferral_shape / update_conferral_bounds — with direct DML on the position machinery revoked from entitlements_writer, consumed identically by operator grants, subscription reconcile (a diff routing quantity deltas to the bounds member), purchases, and system defaults. Ladder occupancy, set, supersession, and transitions derived from conferral shape, never caller-selected. Decree-first entry-point ordering (the grant row is written even when conferral no-ops; revoking a never-delivering decree succeeds with zero provision effect); extend-as-replace (extends_grant_id lineage) enacted primitively. Document 41 (Issue 29; ratified 2026-08-21)
138 Kind-conditional machinery adjudicated surface by surface pool_provision_ladders gains a surrogate identity (provision_ladder_id, replacing the natural composite PK; a partial unique index restates live-uniqueness) so re-occupancy is expressible. pool_provision_transitions narrowed to position audit: plan_ladder_id/provision_ladder_id NOT NULL going forward, five-branch transition_rank_shape CHECK (strict rank inequality on upgrade/downgrade; new transfer type for the rank-equal cross-source case), occupancy FK re-targeted to the surrogate key — all NOT VALID over append-only history. Revocation resolves by source arc through three per-source live partial unique indexes (grant; purchase; subscription × product) — the durable finding-#25 fix and the retry-idempotence backstop for all three arcs. Sync trigger gains the monotonic-end guard; align_conferral_shape scans non-ended incumbency and raises conferral_shape_collision. Doc 35 §8 co-occurrence legal (lifetime-license tier); stacking_policy = 'replace' stands. Decision 119 partially amended (Doc 41 §10.3); Doc 31 §11 Scenario 3 amended to the logical-end rule; Decision 118's "sole procedural element" claim superseded by the enclosure. Document 41 (Issue 29; ratified 2026-08-21)
139 Conferral metadata and bounds grant_reason NOT NULL with CHECK domain {manual, default, evaluation, promotional, complimentary, sponsored, board_decision, legacy} — no 'other' (the audit note is description), no 'trial'/'trial_extension' (vehicle-owned vocabulary; the extension phenomena receive homes in lineage and the general domain). The NOT NULL is load-bearing: both CHECKs are NULL-permeable under three-valued logic without it. 'default' structurally defined by the default_iff_system_authored biconditional (granted_by_person_id nullable, NULL exactly for system-authored defaults). Bounds uniform across shapes: expiry universal; quantity universal (NOT NULL DEFAULT 1, CHECK >= 1) with the position machinery quantity-blind by structural absence — seat-based positioned plans expressible, item_quantity_changed enacted through update_conferral_bounds. extends_grant_id reconciled into the design model. Document 41 (Issue 29; ratified 2026-08-21)
140 Physical schema topology: core + domains + integration + per-provider Supersedes the physical claim of Decision 113 (schema-per-domain-module). The platform's physical layout is one core schema plus domains, integration, and per-provider schemas (e.g., stripe, fedwiki, discourse). The seven-module conceptual map survives intact: each module is henceforth a seam (GLOSSARY.md §5.7) — a conceptual boundary honored in documentation without a dedicated schema. Module-qualified names in the designed reference are read as conceptual addresses, not physical schema addresses. Decision 114's schema qualification applies where module and physical boundaries coincide; between co-resident modules the same FKs are intra-schema. Decision 115's roles attach to physical schemas. Decisions 8687 and 94 unchanged. Exact core/domains table membership is an as-built fact documented on the member-console side (migrations ground truth), not asserted by the design. Document 43 (Issue 30; arbitrated 2026-08-20)
141 Model-documentation division of labor across repositories Descriptive home for as-built models: member-console docs/models/ cards (migrations as ground truth), citing decisions by number rather than restating schema. Normative home: membcons-db's four surfaces — modules/data-model.md (the designed, aspirational schema; member-console's "compass"), modules/companion.md, documents/ + MANIFEST.md, modules/GLOSSARY.md. The designed/as-built split is realized across the two repositories, deliberately; data-model.md does not fork into designed/as-built variants. The per-module model.md projections are retired (frozen at v14 with retirement banners). The design/ sync into member-console shrinks to the four surfaces plus a contract design/README.md (Doc 43 Appendix B); all other per-module files drop out of the sync. Open questions travel by the Document 35 pattern; card-observed structural divergence returns here as a Stage-1 issue. modules/ restructuring beyond the projection freeze is deferred. Document 43 (Issue 30; arbitrated 2026-08-20)

Open Issues

Issues advance through the five-stage architectural-investigation pipeline documented in doc-33-architectural-investigation-process.md: issue surfacing, external research, integration brief, recommendation, and framing memo. Each entry below is a Stage-1 artifact — it states a governing question and its trigger conditions, not a resolution. Issue 23 (plan enrollment) is the canonical worked instance across all five phases; the reusable brief template lives at prompts/template-architectural-investigation.md.

Resource & Billing Detail

Issue 4: Entitlement & Quota Architecture

Status: Reduced (core architecture resolved; implementation refinement remains) Severity: Functional Affected tables: boolean_entitlements, numeric_entitlements, numeric_entitlement_contributions, numeric_entitlement_usage, product_entitlement_rules, pool_provisions Resolved by: Document 13 (Decisions 4651)

The materialization pipeline architecture, quota stacking policy, and definition/state separation are resolved. Remaining areas for implementation:

Effective entitlements materialized view. Direct-query approach adopted for initial implementation. If hot-path latency becomes unacceptable, a materialized view or cache layer may be needed.

Sharded counters. numeric_entitlement_usage uses single-row-per-entitlement for initial implementation. Path to sharded counters (Approach Q3 from Document 13) is structurally preserved.


Issue 7: Cross-Organization Billing Scenarios

Status: Deferred with trigger conditions Severity: Functional Affected tables: pool_provisions, resource_pools, billing_accounts Resolved by: Document 14 (Decisions 5254)

Cross-org collaboration is supported via workspace-scoped role assignments and invitations (no new tables). Cross-org billing is structurally supported (pool_provisions.billing_account_id is unconstrained) but governance mechanisms are deferred. Trigger conditions for reopening: a concrete cross-org billing request from a platform user, or regulatory requirements for cross-org financial governance.


Issue 16: Cooperative Accounting Depth

Status: Open Severity: Functional (domain completeness) Affected tables: patrons, patronage_events Dependency note: Critical path for automated retention hold management (Decision 38, Phase 3). Until equity positions are modeled, retention holds for departing members with unredeemed equity must be managed manually.

The patronage tracking model records raw value flow but does not model several operations required for 1099-PATR filing and audited financial statements: patronage allocation, retained vs. distributed patronage, member equity accounts, fiscal year close, and equity redemption.

Requires a dedicated exploratory document. Possible new entities: patronage_allocations, equity_accounts, equity_transactions, fiscal_year_closes.


Issue 20: Cooperative Submodule Extraction

Status: Partially resolved (Decisions 106, 107, 108) Severity: Architectural (module boundary) Affected tables (remaining): credit_grants, credit_transactions; future cooperative-specific tables (equity accounts, member standing, capital contributions) Related decisions: 93, 95, 106, 107, 108 Depends on: Issue 16 (Cooperative Accounting Depth)

Decision 95 originally placed cooperative concerns within billing. Subsequent boundary analysis progressively resolved this:

  • Decision 106: grants moved to entitlements. Grants are not cooperative-specific — they answer "what does this entity have?" rather than "who pays?"
  • Decision 107: product_entitlement_rules moved to entitlements. The definiteness test and governing-question test both favor entitlements.
  • Decision 108: patrons and patronage_events moved to the new cooperative module. These tables answer "what value has this member contributed?" — a cooperative governance question — and observe billing outcomes rather than participating in them.

Remaining candidates for extraction: credit_grants and credit_transactions require deeper analysis. credit_grants is written by the entitlements materialization pipeline (pool_provisions.credit_grant.source_provision_id), creating a cross-module write from entitlements into billing. credit_transactions is tightly coupled to invoice and payment records. Their extraction is deferred until Issue 16 produces the cooperative accounting depth exploratory document, which will clarify whether a clean extraction boundary exists or whether their cooperative governance hooks are better documented as behavioral contracts on billing entities.

Trigger condition: When Issue 16 produces the cooperative accounting depth exploratory document.


Issue 21: Metered Resources Have No Declarative Product Rules

Status: Resolved Severity: Functional (expressiveness gap) Affected tables: product_entitlement_rules, pool_ondemand_config, usage_events, credit_grants, credit_transactions, boolean_entitlements, numeric_entitlements, numeric_entitlement_contributions, numeric_entitlement_usage, resource_keys Resolved by: Decisions 99105

Resolution applied the definiteness test from the entitlement taxonomy to decompose the three gaps:

  1. Gap 1 (metered-access rule) dissolved. Access to a metered resource is implied by the existence of a quota entitlement or credit balance for that resource (Decision 101). No separate "metered access" entitlement type is needed.
  2. Gap 2 (quota-to-metered overage) resolved as billing-module configuration. Overage emerges from the co-presence of a quota and on-demand config for the same resource key. The resource_keys reference table (Decision 103) enforces the shared namespace. pool_ondemand_config gains provision_id for lifecycle coupling (Decision 102). Dual-path access gating (Decision 104) defines the enforcement sequence.
  3. Gap 3 (declarative credit inclusion) resolved by credit rule type. rule_type = 'credit' added to product_entitlement_rules (Decision 100). Materialization creates credit_grants with source_provision_id provenance (Decision 102).

Additionally, rule_type = 'numeric' was split into 'limit' (static, no reset) and 'quota' (renewable, with reset period) (Decision 99). The full naming cascade (Decision 105) aligned all table and field names with the taxonomy.


Issue 22: Consumption Tracking Module Extraction

Status: Deferred with trigger conditions Severity: Architectural (module boundary) Affected tables: usage_events, pool_ondemand_config Related decisions: 104, 109, 110, 111

The v11 module rename from resource-pools to entitlements (Decision 109) was justified by the intellectual center: eight of twelve tables answer entitlement questions governed by the definiteness test. But the rename itself surfaced a latent tension. Two tables within the entitlements module do not answer entitlement questions at all:

  • pool_ondemand_config describes pricing agreements for uncommitted, post-paid consumption. These agreements explicitly fail the definiteness test (Decision 101: "you can use as much as you want" is not a definite grant). The table answers "who pays if someone consumes this resource without a pre-committed balance?" — a billing-adjacent question about financial routing, not an entitlement question about what an entity has.

  • usage_events records consumption facts across all resolution paths (Decision 111). It is an observational table — "what happened?" — not a declarative one. Quota-resolved events and credit-resolved events do relate to entitlements, but on-demand-resolved events bypass the entitlement system entirely. The table's universality is a virtue (one consumption record regardless of resolution path), but that same universality means it serves concerns beyond entitlements.

These tables currently reside in the entitlements module for pragmatic reasons: they are tightly coupled to the pool lifecycle (pool_ondemand_config.pool_id, usage_events.pool_id), the on-demand configuration participates in the dual-path access gating sequence (Decision 104), and extracting two tables into a separate module would create more organizational overhead than it resolves. The current placement is defensible as "consumption tracking is the operational complement of entitlement enforcement."

However, if the consumption-tracking concern grows in complexity — a dedicated rating engine, usage aggregation infrastructure, metering pipeline tables, or real-time streaming consumers — the case for a separate module strengthens. A "consumption" or "metering" module would answer a distinct governing question: "what was consumed, how was it resolved, and what are the financial consequences?" This question is orthogonal to the entitlements question ("what does this entity have?") and orthogonal to the billing question ("who owes what?"). It occupies the observational middle ground between the two.

Trigger conditions for reopening:

  1. A third consumption-tracking table is proposed (e.g., usage_aggregations, rating_rules, metering_pipeline_state).
  2. The usage event rating pipeline (Known Gap 7 in Document 22) requires its own architectural documentation or infrastructure tables.
  3. The usage_events table's cross-module read surface expands to the point where its placement in entitlements creates import-direction confusion (billing and entitlements both reading from a table whose governing question belongs to neither).

Issue 24: Subscription Management Module Extraction

Status: RESOLVED (Document 38, Decisions 127128) — kept within billing as a named internal seam; not extracted Severity: Architectural (module boundary) Affected tables: subscriptions, subscription_items, subscription_changes, subscription_scheduled_changes (the subscription seam); ambiguous-but-retained-in-billing: purchases, prices, plan_ladders, plan_ladder_tiers Related decisions: 93, 106, 107, 108, 112, 113, 114; 123 and 126 (Issue 26 placements, now confirmed by 128); 127, 128 (resolution) Surfaced by: Document 31 (plan enrollment recommendation); advanced by Document 36 (Issue 26); resolved by Document 38 Holding document: documents/issue-24-subscription-module-extraction.md (collected context; superseded for decision purposes by Doc 38) Stage-3 brief: prompts/issue-24-subscription-module-extraction.md Resolution: documents/doc-38-subscription-module-extraction.md

The billing module currently conflates two adjacent-but-distinct governing questions. billing_accounts, invoices, payments, credit_grants, and credit_transactions answer "what is owed, by whom, for what, and has it been paid?" — the billing question proper. subscriptions, subscription_items, and subscription_changes answer a different question: "what is the state of this customer's ongoing commercial agreement, and how has it changed?" The subscription is not itself a billing fact — it is the agreement that generates billing facts over time. Invoices answer "who owes?"; subscriptions answer "on what recurring basis?"

Four signals suggest the current collapse is load-bearing rather than incidental:

  1. The rate-card dissolution trend (Document 31 §6 of its input reference). Metronome and Stripe v2 explicitly decompose "plan" into Rate Card (pricing), License Fee (recurring commitment), and Contract (the customer-to-rate-card binding). That last role is structurally what a subscription module would own.

  2. The definiteness test applied to subscriptions themselves. A subscription is neither a capability ("what does this entity have?" — entitlements) nor, strictly, a billing fact ("who owes what?" — billing). It is the temporal agreement that both sits upstream of billing fact generation and produces the provisioning events consumed by entitlements materialization.

  3. The change-log weight. subscription_changes is a substantial audit surface with its own lifecycle vocabulary (item_added, trial_started, paused, etc.), distinct from invoice or payment audit trails. A module boundary would make this independence explicit rather than incidental.

  4. Cross-module write direction is coherent. A subscription module would consume billing.products and billing.prices (catalog), produce entitlements.pool_provisions (via materialization), and feed billing.invoices (generated on billing-cycle close). It would sit cleanly downstream of the catalog and upstream of both entitlement materialization and invoice generation — a well-defined dependency direction.

Pragmatic reasons for the current placement include operational proximity to invoices and prices and historical convention. These are real costs to revisit but not dispositive against extraction.

Candidate set boundaries (preliminary):

  • Clearly in the new module: subscriptions, subscription_items, subscription_changes.
  • Ambiguous: purchases (a completed one-time agreement — subscription-module by governing-question alignment, or billing-module by lifecycle simplicity?); prices (catalog pricing — likely stays in billing despite being subscription-consumed).
  • Clearly out: invoices, payments, billing_accounts, credit_grants, credit_transactions.

Relationship to Issue 23 (Plan Enrollment). Document 31's recommendation is orthogonal to this extraction: plan_ladders stays in billing under either arrangement, the exclusion constraint stays on entitlements.pool_provisions, and the canonical enrollment query does not reference subscriptions directly. The only induced changes are clerical FK relocations. Issue 24 therefore does not block Issue 23 and can be pursued on an independent timeline.

Trigger conditions for advancing:

  1. A concrete subscription-lifecycle concern arises that is awkward to express with billing-module placement (e.g., a rating engine, a contract-versioning apparatus, or a subscription-state projection required by a non-billing consumer).
  2. A second module begins consuming subscription_changes as an audit source, making its billing placement misleading.
  3. Billing module table count or internal heterogeneity reaches the threshold that motivated prior extractions (Decisions 106, 107, 108).

Assessment when opened should apply the same apparatus as prior boundary work: definiteness test, governing-question test, structural dependency analysis, and a module-coherence accounting of what each module would and would not own.


Issue 25: Trial Handling Under Plan Ladders

Status: Resolved (no structural change) Severity: Architectural (verification) Affected tables: billing.prices, billing.subscriptions, billing.subscription_changes, entitlements.pool_provision_ladders, entitlements.grants Related decisions: 87, 106, 107, 112, 113; Document 31 (Amendments #1, #2) Resolved by: Document 34 (compressed-pipeline run per Document 33 methodology)

The Document 31 introduction of plan_ladders raised the question of whether trials carry a catalog-shape aspect — whether a ladder or a tier should carry trial semantics — or whether the existing price-and-subscription-level representation (prices.trial_period_days, subscriptions.trial_start/trial_end, status 'trialing', change types 'trial_started'/'trial_ended', and grants.grant_reason = 'trial_extension') suffices.

Document 34 resolves: the existing arrangement is correct and requires no structural addition. Trials are a property of the pricing concession attached to a commercial vehicle, not of the catalog's shape. The pool_provision_ladders junction retains its binary 'active'/'ended' status; the exclusion constraint applies uniformly to trialing and paid provisions because, from the ladder's perspective, a trialing tier is an occupied tier. Kill Bill's catalog-driven phase model was considered and rejected on three grounds: it imports a bi-temporal model that fights with subscription_changes; it couples trial to plan rather than to price, losing the monthly-with-trial vs. monthly-without-trial expressiveness; and its expressiveness-to-complexity ratio does not favor the common case.

Documentation clarifications were added to modules/billing/model.md, modules/entitlements/model.md, and modules/GLOSSARY.md §2 recording the architectural rationale. No schema changes.

This issue also served as the first compressed-pipeline instance of the Document 33 methodology: Stage 2 (external research) was satisfied by the existing synthesis reference-plan-enrollment-modeling.md rather than commissioned anew, and Stage 5 (framing memo) was absorbed into the recommendation when the answer was "no structural change."


Issue 26: Plan-Switch Timing, Commitment Terms, and Tier-Reduction Reconciliation

Status: Resolved (Documents 3637, Decisions 123126) Severity: Architectural Affected tables: billing.subscriptions, billing.subscription_items, billing.subscription_changes, billing.plan_ladders, billing.plan_ladder_tiers, billing.credit_grants, billing.credit_transactions, entitlements.pool_provisions, entitlements.pool_provision_transitions, entitlements.pool_provision_ladders, entitlements.numeric_entitlements, entitlements.pool_ondemand_config Related decisions: 55, 56 (invoice immutability); 71, 72, 73 (prepaid credits and ledger); 87 (self-sufficiency, refunds); 103, 104 (resource-key namespace, dual-path gating); 116, 117, 118 (plan ladders); 106, 107, 108, 112, 113 (module boundaries) Surfaced by: Implementation-team inquiry into paid↔paid plan-switch timing, and the antecedent paid→free downgrade question. Resolved by: Document 36 (recommendation, Decisions 123126); Document 37 (framing memo). Stage-2 synthesis: documents/reference-subscription-change-economics.md. Stage-3 brief: prompts/issue-26-subscription-change-economics.md.

An implementation question — when does a plan switch take effect, and how is it billed — exposed a cluster of related gaps the current model cannot fully express. The questions are surfaced together because they share a common root and interlock; whether they resolve as one investigation or several is a Stage-3 determination.

Unifying diagnosis. The model conflates intent (the forward-looking terms of a commercial agreement) with record (an audit of what has occurred). subscription_changes and entitlements.pool_provision_transitions are append-only logs of mutations, yet subscription_changes.effective_at is documented as the moment a change "took or takes effect." A log asked to also carry not-yet-effective intent cannot cleanly represent a change that is scheduled, may be superseded before it fires, or must be voided — the precondition for any deferred-switch semantics.

Governing questions.

  1. Switch-timing configurability. Can the model express a deferred or scheduled plan switch — effective at period end, or at an arbitrary future instant — rather than only the immediate supersession the ladder-transition machinery performs today? Cancellation already carries scheduling affordances (subscriptions.cancel_at_period_end, cancel_at); plan switches carry none. The platform should be able to express both immediate-prorated and deferred switches, so that future operators may adopt either policy without the model privileging a direction. (Stage-2 reference candidate: Stripe Subscription Schedules / phases.)

  2. Proration-credit disposition. When a mid-cycle switch yields a credit, where does that value go — the prepaid-credit ledger (the better default) or a cash refund (an exception some operators will want)? Both destinations already exist as entities (credits, Decisions 7173; refunds, Decision 87); the gap is a routing policy with a default and an override, not a new mechanism.

  3. Tier-reduction reconciliation. When a downgrade sets a numeric entitlement or quota below the pool's current allocation or consumption — an eight-seat allocation dropping to a five-seat tier — what happens? This is distinct from, and must not be conflated with, the "overage pattern" of entitlements/architecture.md §6.1, which concerns forward, post-paid consumption beyond an included quota; reusing that term here would violate the semantic-coherence bar. The case warrants its own name (e.g. tier-reduction reconciliation) and a materialization-layer policy — candidate dispositions: block the transition, grandfather the surplus to period end, clamp the limit while tolerating existing surplus, or force reduction. It is a policy, not a structural invariant: the GiST exclusion constraint already prevents two active tiers, but quota-below-usage is a runtime condition the constraint neither does nor should adjudicate.

  4. Commitment terms. Minimum-term and recurring-commitment obligations are not modeled at all; subscriptions carries no term boundary. Modeling commitment would let a committed-plan downgrade defer to the commitment boundary as a data-driven consequence rather than a hardcoded exception, and aligns with the "License Fee / recurring commitment" role identified in the rate-card-dissolution trend already cited under Issue 24.

Cross-issue linkage. Questions 1 and 4 are precisely the "contract-versioning apparatus" named as Issue 24's first advancing trigger. The subscription-module boundary question (Issue 24) should therefore be assessed before any new scheduling or commitment tables are placed, so that they land in the correct module (Decision 113, schema-per-module). Questions 2 and 3 are comparatively local — billing-side routing and entitlements-side materialization policy, respectively.

Resolution. Run as a full Document 33 pipeline (Stages 25) on 2026-05-25, resolved as a single investigation carrying four decisions. The spine is a first-class scheduled-change intent entity (Decision 123) that keeps the append-only logs honest; tier-reduction deferral (125) and commitment boundary-gating (126) compose onto it rather than inventing parallel machinery, and credit disposition (124) is a routing policy over the existing ledger/refunds. Module placement honors Issue 24: scheduling and commitment structures sit with subscriptions and move coherently if a subscription module is later extracted; a first-class Contract object and value-commitments (minimum-spend, ramps) remain deferred to Issue 24.


Issue 29: Product Kind Taxonomy — Single Axis or Projection of Orthogonal Dimensions?

Status: RESOLVED (Decisions 134139 ratified 2026-08-21 by direct arbitration, relayed via the member-console session — the as-built console has implemented the recommendation since 2026-07-11; integrated into the decision log and the v15 model reference, with the Stage-5 absorption explicitly ratified alongside. Surfaced Issue 31 per Doc 41 §12's own instruction. Recommendation: documents/doc-41-product-kind-taxonomy-recommendation.md, drafted 2026-07-07; Stage 5 absorbed into the recommendation's §11 per the Issue 25 precedent, a disposition the recommendation itself flags for explicit ratification since the conferral half of the resolution is not dissolutive. Stage 2 waived per the Issue 25 compressed-pipeline precedent, satisfied by existing syntheses: reference-plan-enrollment-modeling.md, Docs 24, 3132, 35. The draft was produced through a multi-agent design competition — four candidate postures, three-judge panel, conferral-first posture selected — followed by three rounds of adversarial review under six lenses; all must-fix findings from every round are resolved in the draft as it stands.) Severity: Architectural (classification model; conferral-interface uniformity) Affected tables: billing.products (product_type, lifecycle_status), billing.product_kinds (view), billing.plan_ladder_tiers, billing.prices (recurring_interval, usage_type), billing.credit_grants (expires_at), entitlements.grants (product/set source duality), entitlements.pool_provisions, entitlements.pool_provision_ladders, entitlements.pool_provision_transitions (plan_ladder_id nullability); prospectively, whichever structure comes to carry the addon dependence relation (Doc 35 §5.3) Related decisions: 106 (grants placement); 112 (entitlement sets); 116118 (plan ladders); 119 (trial surfaces); 122 (lifecycle_status); Doc 31 Amendments #1 and #3 (label-vs-structure dissolution); Doc 24 §4.2 (product_type as catalog-semantics discriminator) Surfaced by: Conceptual review of Document 35's taxonomy discipline (CLI session, 2026-07-06), observing that the four kinds' planned structural derivations read from disjoint structural surfaces. Scope expanded same session with an implementation-team symptom: two separate operator interfaces for issuing grants (one ladder-aware for plans, one for other products), forced by the kind asymmetry. Stage-3 brief: prompts/issue-29-product-kind-taxonomy.md Implementation findings: documents/issue-29-implementation-findings.md (dev-team answers to prompts/issue-29-dev-team-questions.md, verified against application commit b7486c6; integrated into the brief 2026-07-07)

Governing question. Is product kind a single classificatory axis of the catalog, or a projection of several orthogonal structural dimensions onto one label — and, consequently, should the per-kind structural derivations planned by Document 35 §5 be reconceived as per-dimension structural facts, with kind (if retained at all) derived as a presentation-layer projection over them?

Diagnosis. The taxonomy's development invariant (Doc 35 §4) treats the four kinds as one partition awaiting per-kind structural derivation. But the planned derivations read from disjoint surfaces, each of which is more naturally a dimension than a kind-predicate:

  1. Ladder position (plan-ness) — plan_ladder_tiers membership; structural today.
  2. Billing recurrenceprices.recurring_interval; a price property, and a product may carry mixed shapes.
  3. Consumption model (metered/drained vs. durable) — prices.usage_type, boolean- vs. numeric-flavored entitlement-set rules.
  4. Dependence (supplementary-to) — no structural home; precisely the missing signal Doc 35 §5.3 concedes for addon.
  5. Value temporality (expiring vs. perpetual) — credit_grants.expires_at, downstream of the catalog entirely.

Three observations suggest the single-axis framing is under strain. First, the dimensions co-occur: the exclusivity question Doc 35 §8 flags (tier rows and a non-recurring price) is resolved today by CASE precedence in the view — a forced choice, not a modeled fact. Second, both preconditions blocking the one_time and usage derivations (the mixed-price-shape ambiguity of §§5.15.2) are instances of the same root: a product legitimately occupying more than one position across dimensions has no honest home in a partition. Third, the worked counterexample already latent in the schema — a prepaid expiring credit pack — is one_time in billing shape, usage-like in consumption, and temporal at the grant layer; the kind label can carry only one of these facts.

What resolution would decide. Whether the Doc 35 §5 program proceeds per-kind (kind remains a partition; cross-dimension products adjudicated by precedence rules) or per-dimension (each dimension earns its own structural fact; kind becomes a documented projection for pricing pages and reporting, consistent with Doc 24's "catalog semantics, not technical behavior" posture). This issue thereby supplies the governing question for the successor-document choice Doc 35 §6 deliberately left open.

Operational symptom (grant-issuance bifurcation). The kind asymmetry surfaces at the conferral boundary: granting a non-plan product is capability conferral (resolve entitlement set → create pool_provision, off-ladder lifecycle), while granting a plan is position acquisition (the same, plus pool_provision_ladders junction rows, the GiST cardinality constraint, possible supersession, rank-semantic transitions). A bare-set grant cannot occupy a ladder position at all (Doc 31 §7), so plan-equivalent capability conferred by set silently escapes plan cardinality. The implementation team has consequently built two operator interfaces for grants — and the verified inventory (documents/issue-29-implementation-findings.md) sharpens the diagnosis: the UI routes on the product_type label while the guards enforce ladder membership, and the divergence between the two predicates is a named invariant-violation class (NonPlanTiers); the fork is independently re-implemented per entry point (operator handlers, subscription reconcile — the latter silently skipping >1-ladder products); the two flows write differently-shaped grants rows; the set-direct escape is live in the demo seed; and a publish-blocking incident (addon revocation ending the org's plan, finding #25) is interim-guarded pending this investigation. The investigation therefore carries a uniformity mandate: one conferral primitive for any product and any entry point, with kind-conditional behavior derived from the catalog's shape rather than selected by the caller.

Trigger conditions for advancing:

  1. Drafting the first non-plan structural derivation (one_time or usage) — each must first resolve the mixed-price-shape ambiguity, which is this issue in miniature; adjudicating it per-kind without settling the axis question would decide the question silently, by precedent rather than by design.
  2. Choosing the addon structural commitment (an addon_of FK, entitlement-set composition, or a product_relationship junction — Doc 35 §5.3): each candidate is a choice of home for the dependence dimension.
  3. Catalog demand for a product that legitimately spans dimensions — the credit-pack shape, or a product carrying both tier rows and a non-recurring price (Doc 35 §8).
  4. Chartering the successor document contemplated by Doc 35 §6.

Issue 31: Cross-Source Supersession vs. Continued Billing

Status: Open (Stage 1 — surfaced by Document 41 §11.2, logged per its §12 instruction upon ratification of Decisions 134139) Severity: Architectural (billing/entitlements coupling; commercial-layer question) Affected tables: billing.subscriptions, billing.subscription_items, billing.subscription_scheduled_changes, entitlements.pool_provisions, entitlements.pool_provision_transitions Related decisions: 90 (billing execution posture), 123126 (scheduled-change machinery), 137138 (the primitive that makes the residue visible) Surfaced by: Document 41 §11.2 — the residue it names "the scariest detail of the original incident, still open"

When an operator grant supersedes a subscription-sourced position, the position ends but the vehicle keeps billing: the subscription continues invoicing for a tier its pool no longer holds. Decision 137's machinery makes the supersession visible — the end transition names the superseded provision, whose source arc names the subscription, and a later resumption returns the empty set from sync_source_status — but deliberately does not couple conferral to billing mutation. What should happen to the subscription (auto-cancellation, a scheduled change, an operator prompt, or nothing) is a commercial-layer question in Decision 90 territory, adjacent to Document 36's scheduled-change machinery; solving it inside the position machinery would exceed that machinery's governing question.

Governing question: when a non-subscription source supersedes a subscription-delivered position, what commercial enactment — if any — should the system derive, and where does the authority over bill-or-don't-bill live?

Trigger to advance: operator-era incidence of the residue (a real organization paying for a superseded tier), or the first design pass over Decision 90's billing-execution posture — whichever comes first.


Cross-Cutting Concerns

Issue 27: Latent Seam Audit Across Modules

Status: Open (Stage 1 — surfaced, not assessed) Severity: Architectural (module organization / methodology) Affected tables: All modules (cross-cutting) Related decisions: 113 (schema-per-module), 114 (cross-module FKs); 106, 107, 108 (placement precedents); 127 (subscription seam — the construct this generalizes) Surfaced by: Issue 24's resolution (Document 38) and the module/seam disambiguation it prompted (GLOSSARY.md §5.7)

Issue 24 established that the repo's schema-per-module policy (Decision 113) silently fuses two separable questions — is this a conceptually distinct domain? and should it occupy its own schema? — and introduced the seam as the construct that holds them apart (a distinct sub-domain named and reasoned about in isolation, without a separate schema; see GLOSSARY.md §5.7). The subscription seam (Decision 127) is the first named instance. Having acquired the lens, the question is whether it should be applied systematically rather than only when a boundary dispute forces it.

Governing questions (no answers at Stage 1):

  1. Unnamed seams within modules. Do other modules — billing beyond subscriptions, entitlements, organization, identity — harbor conceptually distinct sub-domains that currently go unnamed, such that naming them would improve legibility (e.g. a catalog seam vs. a money seam within billing; an authentication seam vs. an actor-identity seam within identity)?
  2. Seams ripe to become modules. Does any existing seam meet the Decision 107 criterion (referential constituency entirely outside its host) and therefore warrant promotion to a module — the inverse of Issue 24's verdict?
  3. Cross-module seams. Are there coherent sub-domains that span two modules (a concern whose tables are split across schemas), which a seam vocabulary would help name even though no single-module extraction applies?

Why now. Surfacing a global seam map before the next boundary dispute would let placement decisions reference a coherent whole rather than being re-derived per issue (the same defect Document 33 was created to remedy for investigations generally). This is exploratory cataloguing, not a commitment to restructure.

Trigger to advance. Capacity permitting, or ahead of any future module-extraction issue (so the map informs the specific decision). Candidate first step: a discovery pass (potentially multi-agent) cataloguing candidate seams per module, feeding a Stage-3 brief only if the catalogue warrants it.


Issue 28: External-Service Provider / Extension Contract

Status: RESOLVED (Decisions 129133 ratified as amended, 2026-08-21 — Document 44 — upon satisfaction of the conformance gate by member-console's clause-level check, status/explorations/doc39-conformance-2026-08/doc39-conformance-2026-08-21.md: 19 clauses — 13 conforms, 2 deviates, 2 not-exercised, 2 contract-cannot-express. Amendments absorbed: per-provider transport (131); as-built verb/state vocabulary and operator-surface path (132133); slug-nesting strengthening (130). plan_ladders.provider re-scoped out of 132 with a named trigger; registry status-CHECK/timestamp drift deliberately not absorbed, card-tracked. The two 9e findings are opened as Issues 3233; model reference integrated at v16, +3 tables. Recommendation and framing: Docs 3940, refined by the member-console 9a design review. Evidence state as of 2026-08-21: Milestone 9 closed 2026-07-23 with phases 9a9g complete; Discourse shipped as the proving second integration through the registered contract — registry-driven routes, workers, config, UI, and migrations, with a live commerce loop (checkout → entitlement → forum group). The contract is thereby validated in practice; a clause-level conformance check of the Discourse integration against Doc 39 as written has not been run, so the 9d gate below is satisfiable but not yet certified. Arbitrated 2026-08-21: no waiver — the gate is satisfied properly. The clause-level conformance check runs as member-console's provider-integration model card (task 2.5 of the in-flight model-cards change), which reads Doc 39 and the as-built Discourse integration side by side and records per-clause conformance findings; ratification proceeds when that report arrives. Two contract findings from the 9e review are ledgered as upstream design feedback and are to be dispositioned at ratification time — as amendments to Doc 39 or as fresh Stage-1 issues: (a) core domain events have no fan-out to interested integrations; (b) the manifest cannot express set-shaped (converged) delivery.) Severity: Architectural (cross-module contract; new lookup table plus a column on a shared-namespace primary key) Affected tables: entitlements.resource_keys (proposed provider column); integration.webhook_events, integration.integration_outbox (currently soft provider / target_provider strings); entitlements.pool_provision_ladders / billing.plan_ladders (provider-tier rungs); proposed new billing.providers (and optional billing.provider_operations) Related decisions: 86 (schema-per-provider), 87 (self-sufficiency), 103 (resource-key shared namespace), 113 (schema-per-module), 114 (cross-module FKs), 127 (subscription seam — the construct this reuses); proposed 129133 Surfaced by: Member-console Milestone 9 (Integration / Extension Architecture) scoping; relates to open Issue 27 (the provider/extension boundary is a candidate cross-module seam) Stage-3 brief: prompts/issue-28-provider-extension-contract.md Recommendation: documents/doc-39-provider-extension-contract-recommendation.md Framing memo: documents/doc-40-provider-extension-contract-framing.md

The member-console treats FedWiki as the sole external integration; its operator UI, template structure, and resource-key conventions all bake that assumption in. Milestone 9 proposes a shared contract so that the next integrations (NextCloud, Discourse) conform rather than each being bespoke. The contract's code surface — handler interfaces, Temporal activities, operator information architecture — belongs to the member-console repository; its data surface — what a provider is in the schema, which resources it owns, where per-provisioned-instance state lives — is a data-model question and is the subject of this issue.

The term integration is already overloaded. The integration schema (Decisions 8687) is external-API plumbing (webhooks, outbox) within which Stripe lives; Milestone 9's "integration," by contrast, denotes a provisionable, metered, lifecycle-managed service. The repo's pre-existing word for the latter is provider (webhook_events.provider, integration_outbox.target_provider, the per-provider schemas) — the genus that spans Stripe (payment) and FedWiki/NextCloud (provisioning). Milestone 9's Integrations section is the provisioning subset of that genus.

Governing questions (the four threads surfaced in scoping):

  1. Naming. Is the contract's noun provider (consistent with existing schema vocabulary) or a freshly minted term (extension, service)? If provider, how is the provisioning subset distinguished from payment and notification providers without introducing a second word for one thing (semantic-coherence test)?
  2. Resource-key namespacing. Does <provider>.<resource> become the stored key — a destructive rename of a VARCHAR primary key from which several FKs fan out (Decision 103) — or is provider ownership expressed as a column, with the dotted form derived only for display? The dotted literal collides visually with the schema-qualified FK notation of Decision 114 (module.table).
  3. State boundary. Does the member-console become the canonical owner of per-site rows (identity plus an active / read-only / suspended status), or does per-provisioned-instance state remain provider-owned, with the domain retaining only the aggregate count and — where a core question demands it — a read-only projection? This gates M8c (provider read-only) and M10 (metering the namespaced keys).
  4. Contract shape. What is the data shape of the manifest — which resource keys a provider owns, which lifecycle operations it supports, which catalog rungs it contributes — and how does that shape drive operator-panel enumeration in place of today's hardcoded route map ("sites" → "/operator/fedwiki-sites")?

Relation to Issue 27. The provider/extension boundary is a cross-module seam candidate (Issue 27, questions 1 and 3): a coherent sub-domain whose tables would span billing (catalog), entitlements (resource ownership), and integration (lifecycle delivery). Following Decision 127's precedent, the recommendation names the seam rather than extracting a new schema, deferring extraction to the structural-dependency test (Decision 107).

Trigger to advance. (Original trigger, now overtaken: Milestone 9 Phase 9a was unblocked to proceed in parallel with Milestone 7; Milestone 9 closed complete on 2026-07-23.) Ratification of Decisions 129132 (and optional 133 with them) remains gated on validating the contract against a second conforming integration (Phase 9d), per the milestone's own checkpoint. The gate is now satisfiable on the Discourse evidence but not certified — see the evidence state in the Status line. Arbitration (2026-08-21) chose the check over a waiver: member-console's provider-integration model card doubles as the clause-level conformance check of the Discourse integration against Doc 39 as written (cards are being written in order; provider-integration is sixth). Upon receipt of its per-clause findings, Decisions 129133 ratify, with the two ledgered 9e findings dispositioned in the same pass. Done: the report arrived 2026-08-21; the ratification pass is complete (Document 44), the 9e findings are Issues 3233, and the conformance check's six new findings are dispositioned in Doc 44 §3.


Issue 30: Decision 113 and the As-Built Schema Topology

Status: RESOLVED (Document 43, Decisions 140141) — entered and resolved same day by arbitration Severity: Architectural (module organization; physical/conceptual divergence) Affected: all domain-module schemas; Decisions 94, 113, 114, 115; GLOSSARY.md §5.7 Surfaced by: member-console M10 phase 10i (model cards) — the as-built console collapsed the domain schemas into core plus domains, while the integration and per-provider schemas (Decisions 8687) conform to design. Stage-1 framing: documents/doc-42-model-documentation-division.md Appendix A. Resolution: documents/doc-43-schema-topology-and-documentation-division.md

The schema-per-module policy (Decision 113) asserted module = PostgreSQL schema. The as-built console honored the conceptual module boundaries but not the physical ones, effectively rendering the domain modules as seams (§5.7) within a coarser layout. The governing question — when designed and as-built physical topology diverge, which yields? — was framed with three horns: (a) supersede, blessing the as-built layout as the design's position; (b) reaffirm, recording a convergence obligation on the console; (c) generalize into a two-level construct separating conceptual modules from physical schema groupings. Arbitration (Christian, 2026-08-20) chose (a), scoped precisely: only the physical claim falls; the seven-module conceptual map survives as seams (Decision 140). The companion disposition (Decision 141) records the model-documentation division of labor with member-console — descriptive as-built home there, normative designed home here, the design remaining aspirational rather than historical ("the compass"), the per-module model.md projections retired, and the design/ sync shrunk to the four normative surfaces. In practice the resolution accomplishes most of horn (c) without new apparatus: the model now is a two-level construct, conceptual modules over a coarser physical layout, and Issue 27's seam audit operates on a uniform footing — the modules themselves are seams within the physical topology.


Issue 32: Core-Domain Event Fan-Out to Interested Providers

Status: Open (Stage 1 — surfaced by the Doc-39 conformance check; the first of Issue 28's two contract-cannot-express findings) Severity: Architectural (contract completeness; integration trigger path) Affected tables: integration.integration_outbox (caller-targeted emission), billing.provider_operations (no subscription vocabulary), core domain tables whose transitions interest providers (org_members, entitlements.grants, entitlements.boolean_entitlements, entitlements.pool_provisions) Related decisions: 131132 (as ratified, Document 44); 8687 Surfaced by: member-console status/explorations/doc39-conformance-2026-08/doc39-conformance-2026-08-21.md (contract-cannot-express against D131/§6.1's trigger path); ledgered at 9e as design feedback

The outbox is caller-targeted (Enqueue(providerSlug, …)), so a core handler must know which providers care about a domain transition — and core handlers writing membership, grant, and entitlement changes do not know Discourse exists. The manifest has no vocabulary by which a provider subscribes to domain events. Discourse substitutes a level-triggered sweep (reading boolean_entitlements live) plus provider-side webhooks for latency — correct but bespoke, and the substitution re-derives per provider exactly what a fan-out seam would own once. Candidate evolution: manifest-declared core-event subscriptions — the manifest gains a declared-interests surface, and a domain-side emission seam fans out to subscribed providers. Trigger to advance: a second provider needing the same sweep workaround, or the dispatch-layer work (the same layer owing the §6.3 conformance check) making the seam buildable.


Issue 33: Set-Shaped (Converged) Delivery in the Provider Contract

Status: Open (Stage 1 — surfaced by the Doc-39 conformance check; the second of Issue 28's two contract-cannot-express findings) Severity: Architectural (contract expressiveness; operation-class vocabulary) Affected tables: billing.provider_operations (mutating verb set), billing.provider_states; the Decision 133 desired/observed projection shape Related decisions: 131, 133 (as ratified, Document 44) Surfaced by: member-console status/explorations/doc39-conformance-2026-08/doc39-conformance-2026-08-21.md (contract-cannot-express against D133's per-instance projection and mutating verbs); ledgered at 9e as design feedback

Decision 133's projection and verb set are per-instance-shaped: create/set_status/delete act on an instance the caller holds, and the desired/observed pair describes one instance's lifecycle. A provider whose delivery is set-shaped — Discourse converges a member-set per managed forum group — has no per-instance analogue for any mutating verb and no home for stored desired-state: its observed side materializes provider-side (observed_group_members) while the desired side is computed live from boolean_entitlements, with list/describe the only declarable operations. The contract cannot say "this provider reconciles a set." Candidate evolution: an operation class for converged delivery — a declared reconcile-set operation with a set-shaped desired/observed projection. Trigger to advance: a second set-shaped provider, or Issue 32's fan-out design (the two share the manifest's declaration surface and are natural co-travelers).


Issue 34: Person Name Provenance and the Identity-Provider Shadow Profile

Status: Open (Stage 1 — surfaced 2026-08-29 by maintainer arbitration out of the identifier-policy work; Stage 2 input identified but not yet in hand, see below) Severity: Architectural (identity module; column provenance, PII duplication, and an erasure invariant) Affected tables: identity.users (the whole IdP profile cluster: username, email, email_verified, display_name, avatar_url, locale, timezone), identity.persons (gains presentation fields), identity.retention_holds and the GDPR Anonymization Protocol (Structural Policies), which the sync rule must not defeat Related decisions: the identity/person separation of Documents 1 and 26; the GDPR Anonymization Protocol and Document 10 (retention vs. erasure); Document 45's role taxonomy, under which an IdP-supplied name is an external reference and a person's display name is a name Surfaced by: member-console status/explorations/identifiers-2026-08/person-name-exploration.md (five observed designs across eight primary sources), arbitrated by the maintainer on 2026-08-29 with alternative 3 (the shadow model) approved in substance and routed to this pipeline

The governing question: where does a person's name live when the identity provider also carries one, and who owns it? Today the reference places display_name on identity.users and gives persons no name at all, while member-console as built places display_name and primary_email on persons and overwrites them from OIDC claims on every login. Both arrangements collapse two jobs into one column, differing only in which row hosts the collapse — the overloading Document 45 exists to prevent.

The maintainer's framing, which governs the recommendation: three names with three jobs. (1) The identity provider's name — an external reference, stored as received, never assigned locally. (2) The name the person goes by here — a name in Document 45's sense: presentation, owned by the cooperative, editable by the person. (3) The designed legal_first_name / legal_last_name — owned, never sourced from an identity provider, and what a tax form and the membership register require. The current schema expresses the first and third and has nowhere to put the second.

Approved shape, to be specified by the recommendation: identity.users' profile cluster is renamed with explicit provenance (usernameidp_preferred_username, emailidp_email, email_verifiedidp_email_verified, display_nameidp_name, plus idp_synced_at; avatar_url under the same shadow rule). identity.persons gains display_name, primary_email, and display_name_source ∈ {idp, local} defaulting to idp. locale and timezone are person-owned preferences an identity provider never overwrites, on the ground that a preference the provider does not know about is not a cache of anything. The sync rule: idp_* always rewritten on login; persons.display_name follows the identity provider only while display_name_source = 'idp', with a preferred_username fallback on every path; clearing a local name reverts to idp; email stays provider-owned for now, with email_source named as the anticipated sibling.

The erasure invariant is the part that must be normative text rather than implementation care. The shadow model duplicates PII across two rows. If a person is erased while display_name_source = 'idp' and the user row survives, the next login rewrites the erased name from the provider — erasure silently undone on a schedule. The rule: erasure flips the source to local and blanks the local fields, and the sync refuses to write any person under erasure.

Trigger to advance: already triggered — the maintainer has approved the shape and routed it here. Stage 2 complete: documents/reference-identity-profile-provenance.md, synthesized 2026-08-29 from member-console status/explorations/identifiers-2026-08/identifiers-exploration/research-person-names.md (26 URLs across eleven systems and three standards, evidence grades preserved). It catalogues five designs, names the four axes they combine — policy granularity, when the provider writes, whether the design stores one field or two, and what happens to a local edit — and records that OIDC Core and SCIM are both silent on the policy question, so the choice is a product decision rather than a conformance one. Two findings bear directly on the approved shape: a one-field design is lossy in whichever direction it resolves, and no surveyed system reverts to the provider's value without having kept it (Google Workspace can revert precisely because it never discarded the admin-set name); and no surveyed documentation addresses erasure, retention, or a re-sync restoring an erased value, which is the interaction this issue must specify rather than inherit. Stage 3 complete: prompts/issue-34-person-name-provenance.md (2026-08-29, after the maintainer reviewed Stage 2). The brief poses eight decisions, of which three were not visible before the schema was read closely: (6) persons.user_id is nullable by design, so an invited-but-unregistered person has no provider profile and display_name_source = 'idp' is incoherent as their default; (5) the erasure invariant must bind to the existing persons status lifecycle (partially_erased, anonymized, merged) and retention_hold rather than to a new notion of "under erasure," and the JSONB Governance Policy forecloses storing the provider profile as a blob because it is PII; (7) person merge means two user rows and two provider profiles, and may be deferred with a named trigger since person_merges is designed but unbuilt. Stage 4 complete: documents/doc-46-person-name-provenance-recommendation.md (2026-08-29), proposing six decisions for ratification and targeting data model v16.4. All eight brief decisions rendered; two deferred with named triggers (person merge; email_source). Load-bearing choices: the users cluster renamed idp_* after the claims it mirrors; locale/timezone moved to persons as preferences a provider never writes; persons gains display_name (nullable), primary_email, and display_name_source with no column default, since the correct initial value differs between the invitation and first-login paths. The erasure invariant is enforced in three layers, of which the middle is the load-bearing one: a CHECK (status NOT IN ('partially_erased','anonymized') OR display_name_source = 'local') makes the state synchronisation would need in order to undo an erasure unrepresentable, since a CHECK cannot constrain authorship but can make the dangerous row illegal. A CHECK (user_id IS NOT NULL OR display_name_source = 'local') makes an invited person's typed name survive their first login without a special case. Stage 5 complete: documents/doc-46-person-name-provenance-framing.md (2026-08-29). Establishes the vocabulary the recommendation deployed without defining — most importantly that provenance (which system authored a value) and ownership (which system may change it) are independent properties, and one column can carry only one, which is the whole argument for two columns restated as a definition. Separates the four schema-enforced invariants from the five policy choices, by the test "could a maintainer change this without creating a state the model calls impossible?" Surfaces five gaps, of which one becomes Issue 35 and one questions a column the recommendation itself proposes (idp_synced_at has no named consumer and may fail Document 45 §2.3's own prohibition — to be settled at ratification rather than left). The pipeline is complete; Issue 34 awaits ratification. Nothing visible changes downstream before the schema lands; member-console implements when its first person-owned surface exists, and can adopt the whole recommendation before it builds the nullable user link, merges, or the erasure path — all three constraints are vacuous until then.


Issue 35: Personal Organization Naming and Derived-Name Provenance

Status: Open (Stage 1 — surfaced 2026-08-29 by documents/doc-46-person-name-provenance-framing.md §5.1) Severity: Architectural (the Issue 34 defect, unrepaired one level up, and made worse by Issue 34's remedy) Affected tables: organization.organizations (name, org_type = 'personal', owner_person_id), reading from identity.persons Related decisions: Issue 34's proposed decisions (Document 46); Document 45 §2's role taxonomy; Decision 19 (the cooperative as an organization) Surfaced by: Document 46's framing memo, which found the defect while scoping to persons and deliberately did not fold it in

modules/data-model.md states that "every person has a personal organization created automatically" and says nothing about how that organization is named. Member-console fills the silence by deriving the name from the person's display name at signup — personalOrgName(displayName), internal/provisioning/provisioning.go:78, written once, never resynchronised, with no source recorded (confirmed by the console side, 2026-08-29). The resulting value is provider-derived in provenance, locally owned in principle, permanently stale in practice, and — the decisive point — indistinguishable from a name an owner chose. That is precisely the conflation Issue 34 removes from persons, sitting untouched on organizations.

Issue 34's remedy makes it worse rather than better. Once a person can adopt a display name of their own, "derive the organization name from the person's name" becomes ambiguous — from the presentation name, which may now be adopted, or from the provider's claim? And a personal organization named after a name its owner has since abandoned is a new failure mode that Issue 34 introduces and does not address.

Candidate resolutions, none selected: name the personal organization from the person at creation and leave it owned thereafter, stating that in the reference so the derivation stops being an undocumented downstream invention; or make the personal organization's name a rendered projection of its owner rather than a stored value, so it cannot go stale; or give organizations the same shadow treatment persons receives, which is heavier and probably disproportionate for a name only its owner sees. Trigger to advance: ratification of Issue 34, whose outcome changes what the derivation would even mean.


Issue 36: Irrevocable Terminal States Are Asserted in Prose and Enforced Nowhere

Status: Open (Stage 1 — surfaced 2026-08-29 by documents/doc-46-person-name-provenance-framing.md §5.5, during the Issue 34 investigation) Severity: Architectural, and the most consequential open issue in the identity area — a re-identification path through a protocol whose entire purpose is irreversibility Affected tables: every table with an irrevocable terminal state. identity.persons (anonymized, partially_erased, merged) is the acute case because the GDPR Anonymization Protocol depends on it; the registry at documents/policy-soft-delete-terminal-state.md §4.1 enumerates the rest Related decisions: the Soft-Delete and Terminal State Policy and the GDPR Anonymization Protocol (both Structural Policies); Document 9; Document 10 (retention vs. erasure) Surfaced by: Document 46's framing memo. Independently identified by the member-console session while assessing Document 46's erasure trigger, which stands on this assumption

documents/policy-soft-delete-terminal-state.md §4.1 is titled Irrevocable Terminal States and says of records in them: "Records in these states shall never transition to any other state." The persons lifecycle repeats it — anonymized is an "Irrevocable terminal state," partially_erased is "Not reversible," merged is "Irrevocable."

Nothing enforces any of it. The persons Constraints block carries the lifecycle as prose bullets; there is no CHECK, no trigger, no transition table. The policy document describes itself as "a constraint specification — referenceable in code reviews, migration approvals," which is a review instrument rather than a database constraint. UPDATE identity.persons SET status = 'active' WHERE status = 'anonymized' succeeds, and the row is thereafter an ordinary person whose PII may be written back.

This predates Issue 34 and is independent of it, but Issue 34 is what made it visible: Document 46 §5's erasure invariant is load-bearing on terminal statuses actually being terminal, and its strongest guard is a lock beside an open window without this. The scope is wider than identity — the finding reaches every entity in the §4.1 registry.

Candidate resolutions, none selected: a per-table CHECK forbidding the illegal transitions, which requires the old row value and therefore a trigger rather than a CHECK in PostgreSQL; a single generic transition-guard trigger driven by a transition table, which centralises the vocabulary at the cost of a lookup on every status write; or REVOKE UPDATE (status) from writer roles with transitions performed only through SECURITY DEFINER functions, which is the shape Document 41's conferral family already established for the position machinery. The third has precedent in this model and is the one to evaluate first.

Current exposure: the hole is dormant as built (confirmed by the console side, 2026-08-29): only 'active' is ever written to persons.status, and no erasure path exists, so no row can currently be resurrected from a terminal state it cannot reach. The defect is in the designed model and in any implementation that builds erasure without the guard — which is precisely why the sequencing below matters.

Trigger to advance: already triggered. It should be sequenced ahead of, or with, Issue 34's implementation rather than after it, since Issue 34's erasure guarantee is incomplete until this closes, and the erasure path is the thing that arms the hole.


Issue 18: Billing Strategic Assessment

Status: Resolved Severity: Strategic (build-vs-buy) Affected tables: All billing module tables Resolved by: Document 23 (Decisions 9093)

The build-vs-buy assessment was completed. The recommended strategy adopts Stripe Billing as the Phase 1 execution engine, retains cooperative-specific tables as the authoritative system of record, and delegates cooperative-module orchestration to Temporal. The key finding is that most cooperative-specific billing requirements are governance and attribution concerns layered on top of generic billing mechanics, not alternative mechanics (Decision 93). This defines a clean integration boundary: Stripe owns generic billing execution; the core schema owns cooperative semantics; Temporal bridges them.

No schema changes are required for the strategic decision itself. Phase 1 implementation will add a stripe provider schema (per existing Decision 86 pattern) and populate projection tables via webhook sync. No core tables are added, removed, or restructured by this decision.


Issue 19: Resource Pool Architecture Documentation (now Entitlements module)

Status: Resolved Severity: Documentation (prerequisite for future changes) Affected tables: resource_pools, pool_provisions, pool_assignments, pool_ondemand_config, boolean_entitlements, numeric_entitlements, numeric_entitlement_contributions, numeric_entitlement_usage, usage_events, product_entitlement_rules, resource_keys Resolved by: Document 22

Document 22 (Resource Pool Architecture Deep-Dive) provides: end-to-end walkthrough from subscription through materialization to workspace access; semantic descriptions of all entitlements-module entities plus product_entitlement_rules; cross-module boundary documentation; materialization pipeline operational detail; quota stacking policies with examples; infrastructure provisioning state placement; and 11 known gaps documented with rationale for deferral.


Issue Dependencies

Issue 16 (Cooperative Accounting) depends on findings from Document 15 (Invoice Line Items — patronage attribution from invoices), which in turn blocks Decision 38, Phase 3 (automated retention hold management).

Issue 18 (Billing Strategic Assessment) — RESOLVED (Document 23): its resolution reframed all billing tables as Category A (authoritative), B (projections), or C (reduced role).

Issue 19 (Resource Pool Architecture Documentation) — RESOLVED (Document 22): its resolution unblocked any changes to entitlements-module tables.

Issue 22 (Consumption Tracking Module Extraction) — DEFERRED with trigger conditions. The v11 entitlements rename surfaced a tension: usage_events and pool_ondemand_config do not answer entitlement questions. Extraction deferred until consumption-tracking complexity warrants a separate module.

Issue 23 (Plan Enrollment) — RESOLVED (Document 31, Decisions 116118): plan_ladders, plan_ladder_tiers, and pool_provision_ladders introduced. Bidirectional CHECK on products (Decision 117); GiST exclusion constraint on junction (Decision 118).

Issue 24 (Subscription Management Module Extraction) — RESOLVED (Document 38, Decisions 127128). Surfaced by Document 31; advanced by Issue 26 (Document 36), whose subscription_scheduled_changes and commitment attributes (Decisions 123, 126) constitute the "contract-versioning apparatus" of trigger #1. The assessment kept the subscription lifecycle within billing as a named internal seam rather than extracting a subscriptions schema: the structural-dependency analysis was decisive (four billing financial-core tables hold inbound FKs to the seam, so Decision 107's "constituency entirely outside the holding module" criterion is unmet). Decision 128 confirms Doc 36's provisional placements as final. The verdict is reversible per sharpened triggers (Doc 38 §9 — a first-class Contract, or a second non-billing peer consumer). Independent of Issue 23 (orthogonal).

Issue 25 (Trial Handling Under Plan Ladders) — RESOLVED (Document 34, Decision 119): no structural change. Trial surfaces retained at price/subscription layer; pool_provision_ladders.status remains binary.

Issue 26 (Plan-Switch Timing, Commitment Terms, and Tier-Reduction Reconciliation) — RESOLVED (Documents 3637, Decisions 123126): a scheduled-change intent entity, ledger-default credit disposition, a tier-reduction reconciliation policy, and commitment-as-subscription-attributes. Scheduling and commitment structures were placed on/with subscriptions so they survive a future Issue 24 subscription-module extraction; a first-class Contract object and value-commitments remain deferred to Issue 24, which Issue 26 therefore informs but does not resolve.

Issue 27 (Latent Seam Audit Across Modules) — OPEN (Stage 1). Generalizes the module/seam construct established by Issue 24 (Decision 127, GLOSSARY.md §5.7) into a systematic cross-module question: which modules harbor unnamed seams, which seams are ripe to become modules, and which seams span module boundaries. Informs any future extraction issue (e.g. Issue 22) and should ideally precede it, so placement decisions reference a global seam map rather than re-deriving one per dispute.

Issue 29 (Product Kind Taxonomy: Single Axis or Projection) — OPEN (Stage 4: doc-41 drafted, candidate Decisions 134139, awaiting ratification). Sits upstream of every non-plan structural derivation contemplated by Document 35 §5: whether kind is a partition or a projection of orthogonal dimensions determines whether those derivations are drafted per-kind or per-dimension. Descends from Doc 31 Amendment #3 (which made plan structural and left the other three labeled); supplies the governing question for the successor-document choice Doc 35 §6 left open. Orthogonal to the module-boundary issues (22, 27), though a per-dimension resolution would touch prices and the entitlement-set layer.


Architectural Notes

Hold Mechanisms: Two Complementary Systems

The model has two structurally parallel hold mechanisms that serve different compliance needs:

  • retention_holds (Decisions 3638) → protects person PII from premature anonymization. Operates at the record level: a specific person's data cannot be anonymized while a hold is active. The persons.retention_hold flag provides a hot-path check. Legal authorities include tax retention mandates, cooperative equity obligations.

  • audit_legal_holds (Decision 79) → protects audit event data from premature archival or deletion. Operates at the partition level: audit partitions within the hold's scope cannot be archived, anonymized, or dropped while a hold is active. Legal authorities include SEC investigations, subpoenas, internal audits, member disputes.

Both are "hold" mechanisms from the same compliance family. They share structural patterns (placed_by/released_by actor attribution, legal_authority classification, active/released/expired status lifecycle) but have different scope granularity and different targets. Neither subsumes the other.

PII Tension in Audit Logs (Known Tension Point — Issue 9 Resolution)

The JSONB Governance Policy (Decision 34) categorically prohibits PII in JSONB columns, and the audit_logs description states that changes and metadata must not contain PII. However, several tension points exist:

  • actor_ip and actor_user_agent are arguably PII under GDPR (Recital 30: "online identifiers... may be used to create profiles of the natural persons and identify them"). These are first-class relational columns, not JSONB, so Decision 34 does not directly apply — but the broader spirit of PII minimization suggests they warrant attention.

  • Legal hold investigations may require correlating audit events with identifiable actors. If the actor has been anonymized (via the GDPR anonymization protocol on persons), the audit trail loses investigative value. The security and critical retention tiers are designed for exactly these scenarios — events in those tiers survive longer than the anonymization window.

  • Future compliance scenarios (SOC 2 evidence requests, cooperative patronage audits) may require retaining actor-identifying information in audit records beyond the person's anonymization.

This is a known tension, not a resolved decision. A possible future refinement: a tiered PII policy where certain PII-adjacent fields (IP, user agent) are permitted in audit records but subject to the retention tier's anonymization schedule (e.g., the security tier anonymizes IP/user_agent at warm→cold transition, while critical preserves them through the full retention period). This is flagged for future work. Decision 34 remains active as written.


Resolved Issues

Issue Resolution Decisions
Issue 2: Invitation State Machine invitations table introduced. org_members cleaned of invitation state. 39, 40, 41
Issue 4: Entitlement & Quota Architecture Materialization pipeline, stacking policy, quota definition/state separation, quota_contributions provenance. 46, 47, 48, 49, 50, 51
Issue 7: Cross-Organization Billing Deferred with structural affordances preserved. Cross-org collaboration via role assignments. 52, 53, 54
Issue 8: Temporal Modeling Strategy Inline timestamps + selective actor attribution. Audit log as complete history. 42, 43, 44, 45
Issue 9: Audit Log Architecture Monthly RANGE partitioning, five retention tiers, legal holds, 6-index budget, hybrid sync/async writes, partition lifecycle pipeline, transactional outbox. Architecture decisions captured; implementation-specific technology choices deferred to build time. (Decision 84, audit schema separation, was superseded by Decision 85.) 77, 78, 79, 80, 81, 82, 83
Issue 10: Integration Schema Architecture Schema-per-provider namespace strategy adopted. Core schema renamed from public to core. Three core entities (payment_methods, refunds, disputes) added per self-sufficiency principle. Integration infrastructure tables (webhook_events, integration_outbox) added. External ID conventions established. Individual integration schema designs deferred to implementation. 85, 86, 87, 88, 89
Issue 18: Billing Strategic Assessment Stripe Billing as Phase 1 execution engine. Temporal for cooperative-module orchestration. Billing tables classified by role (authoritative / projection / reduced). No schema changes. 90, 91, 92, 93
Issue 19: Resource Pool Architecture Documentation Deep-dive document produced. End-to-end walkthrough, entity semantic descriptions, cross-module boundaries, materialization pipeline operational detail, quota stacking, infrastructure provisioning placement, and 11 known gaps documented. — (synthesis document, no new decisions)
Issue 21: Metered Resources / Declarative Product Rules Gap 1 dissolved (access implied by entitlement/credit), Gap 2 resolved as billing-module overage config with shared namespace enforcement, Gap 3 resolved by rule_type = 'credit'. Naming cascade aligned all schema names with entitlement taxonomy. 99, 100, 101, 102, 103, 104, 105
Issue 23: Plan Enrollment plan_ladders and plan_ladder_tiers added to billing; pool_provision_ladders junction (with GiST exclusion constraint) added to entitlements. Nullable products.plan_ladder_id with bidirectional CHECK (Decision 117). Enrollment modeled as derived relation over provision + ladder position; no dedicated enrollment table required. Denormalized junction columns synced by AFTER UPDATE trigger on pool_provisions. 116, 117, 118
Issue 25: Trial Handling Under Plan Ladders No structural change. Trial specification retained at price level (prices.trial_period_days); trial-instance state on subscription (trial_start/trial_end, status 'trialing'). pool_provision_ladders.status remains binary; exclusion constraint applies uniformly to trialing provisions. Kill Bill catalog-phase model rejected. 119
Issue 11: Polymorphic Association Inconsistency Exclusive FK arcs with CHECK constraints. Audit log retained as documented exception. 20, 21, 22, 23, 24, 25
Issue 12: Primary Key Strategy UUIDv7 adopted for all tables. 30
Issue 13: Invoice Line Items invoice_line_items table. Sum invariant. Discount hybrid representation. Tax at line level. Product attribution. 55, 56, 57, 58, 59
Issue 14: Soft-Delete and Terminal State Policy Policy document produced. ON DELETE RESTRICT universal. 31, 32, 33
Issue 15: Discount and Promotion Model Three-layer model: coupons, promotion_codes, discounts. Scope exclusive arc. Duration semantics. 61, 62, 63, 64, 65, 66
Issue 17: JSONB Governance Policy Policy document produced. PII categorically prohibited. 34, 35

Exploratory Documents

Document Title Status
1 Service Accounts & Actor Model Complete; decisions integrated
3 Roles & Permissions Design Complete; decisions integrated
5 Workspace-Centric vs. Billing-Centric Resource Models Complete; decisions integrated
5.1 Multi-Billing-Account Entitlement Resolution Complete; decisions integrated
5.2 Metered Usage and Patronage Attribution Scope Complete; decisions integrated
6 Subscription, Purchase & Product Access Semantics Complete; decisions integrated
6.1 Document 6 Revisions: Semantic Precision and Structural Corrections Complete; decisions integrated
7 Polymorphic Relationship Audit and Pattern Decision Complete; decisions integrated
7.1 Polymorphic Audit: Reconciliation of Two Independent Analyses Complete; decisions integrated
8 Primary Key Strategy: UUIDv7 Migration Complete; decisions integrated
9 Issue 14 Exploration: Soft-Delete and Terminal State Policy Complete; decisions integrated
10 The Obligation to Remember: Retention Requirements Analysis Complete; decisions integrated
11 Issue 2 Exploration: Invitation State Machine Complete; decisions integrated
12 Issue 8 Exploration: Temporal Modeling Strategy Complete; decisions integrated
13 Entitlement & Quota Architecture Complete; decisions integrated
14 Cross-Organization Resource Sharing Complete; decisions integrated
15 Invoice Line Items Complete; decisions integrated
16 Discount and Promotion Model Complete; decisions integrated
17 Pending Charges and the Billing Lifecycle Pipeline Complete; decisions integrated
18 Prepaid Credit Balances and the Credit Grant Model Complete; decisions integrated
19 Payment Arrangements and Billing Topology Reference Complete; reference document (no new decisions)
20 Audit Log Architecture Integration Complete; decisions integrated
21 Integration Schema Architecture Complete; decisions integrated
22 Resource Pool Architecture Deep-Dive Complete; synthesis document (no new decisions)
23 Billing Strategic Assessment Complete; decisions integrated
24 Billing & Value Domain Deep-Dive Complete; synthesis document (no new decisions)
25 Organization & Access Domain Deep-Dive Complete; synthesis document (no new decisions)
26 Identity & Credentials Domain Deep-Dive Complete; synthesis document (no new decisions)
27 Module Boundary Proposal Complete; decisions integrated (9497)
28 Grants Module Placement — Are Grants a Billing Concern? Complete; decisions integrated (106)
29 Product Entitlement Rules Module Placement Complete; decisions integrated (107; supersedes 97). Cooperative module creation (108).
30 Schema-per-Module and Per-Schema Role Conventions Complete; decisions integrated (113115)
31 Plan Enrollment: Architectural Recommendation Complete; decisions integrated (116118). Resolved Issue 23. Surfaced Issue 24.
32 Plan Management Framing Complete; framing memo (no new decisions; vocabulary adopted in Glossary)
33 Compressed Pipeline Methodology Complete; process document (no new decisions)
34 Trial Handling Under Plan Ladders: Architectural Recommendation Complete; decisions integrated (119). Resolved Issue 25.

Reference Documents

Document Title Status
Ref 1 Domain/Integration Boundary Methodology Active; five heuristics and seven-step methodology for domain-vs-integration decisions
Ref 2 PostgreSQL Schemas: A Practical Production Guide Active; evidence base for schema-per-concern viability
Ref 3 Integration Patterns from Production Billing Systems Active; survey of Kill Bill, Lago, Saleor, Medusa, Odoo

Policy Documents

Policy Governing Scope Status
Soft-Delete and Terminal State Policy (v1.0) All status-bearing tables, FK behavior, GDPR anonymization protocol, person merge protocol Active
JSONB Governance Policy (v1.0) All JSONB columns Active
Retention Justification Register (US Federal) US retention obligations mapped to data model tables Active (initial draft)

Version History

Version Summary of Changes
4.1 Polymorphic audit reconciliation, UUIDv7 migration, CHECK constraints formalized. 31 tables.
5.0 Added retention_holds, person_merges. Expanded persons status vocabulary. Replaced invoices.billing_address JSONB with relational columns. Resolved Issues 14, 17. Split into reference + companion. 33 tables, 38 decisions.
6.0 Added invitations. Cleaned org_members (removed invitation fields, added invitation_id FK, removed pending status). Added temporal timestamps and actor attribution across 16 tables. Resolved Issues 2, 8. Merged full open issue descriptions from consolidated reference. 34 tables, 45 decisions.
7.0 Added quota_contributions, quota_usage, invoice_line_items, coupons, promotion_codes, discounts, pending_charges, pending_charge_usage_events, credit_grants, credit_transactions. Modified quotas (definition/state separation), usage_events (billing attribution via join table), billing_accounts (billing mode + auto-recharge), invoices (credit application fields), product_entitlement_rules (stacking policy). Resolved Issues 4, 7, 13, 15. Added prepaid credits model and payment topology reference. 44 tables, 76 decisions.
8.0 Resolved Issue 9 (Audit Log Architecture). Modified audit_logs: added tier, severity, from_status, to_status; renamed timestampcreated_at; changed PK to composite (log_id, created_at) for partitioning. Added audit_retention_policies, audit_legal_holds, audit_archive_manifest, audit_outbox. Added Architectural Notes section documenting hold mechanism relationship and PII tension point. 48 tables, 84 decisions.
9.0 Resolved Issue 10 (Integration Schema Architecture). Domain tables placed in core schema. Added payment_methods, refunds, disputes to billing module (self-sufficiency principle). Added webhook_events (partitioned) and integration_outbox to new Integration Infrastructure section. Established schema-per-provider namespace strategy, external ID conventions, and domain/integration boundary methodology reference. Opened Issue 18 (Billing Strategic Assessment) and Issue 19 (Resource Pool Architecture Documentation). Added Reference Documents section. 53 tables, 89 decisions.
10.0 Resolved Issue 21 (Metered Resources / Declarative Product Rules). Applied entitlement taxonomy naming cascade: entitlementsboolean_entitlements, quotasnumeric_entitlements, quota_contributionsnumeric_entitlement_contributions, quota_usagenumeric_entitlement_usage. Added resource_keys reference table. Split rule_type = 'numeric' into 'limit' and 'quota'. Added rule_type = 'credit' with materialization into credit_grants. Added pool_metered_config.provision_id and credit_grants.source_provision_id for lifecycle coupling. Established dual-path access gating and shared resource key namespace enforcement. 54 tables, 105 decisions.
11.0 Module renamed: resource-pools → entitlements (Decision 109). Table renamed: pool_metered_configpool_ondemand_config; field metered_keyresource_key in entitlements-module tables (Decision 110). usage_events made universal: resolution_path discriminator added, billing_account_id made nullable (Decision 111). Permission strings updated: pool.metered:*pool.ondemand:*. Issue 19 title updated. 54 tables (unchanged count), 111 decisions.
12.0 Entitlement set abstraction (Decision 112). Added entitlement_sets table; renamed product_entitlement_rulesentitlement_set_rules re-parented to sets. Products reference sets via entitlement_set_id (dependency direction reversed: billing consumes from entitlements). Grants may reference sets directly for ad hoc capability conferral. pool_provisions.product_id replaced with pool_provisions.entitlement_set_id. Materialization pipeline remains singular. 55 tables, 112 decisions.
13.0 Resolved Issue 23 (Plan Enrollment). Added billing.plan_ladders and billing.plan_ladder_tiers (catalog-shape constructs); added nullable products.plan_ladder_id FK with bidirectional CHECK enforcing logical equivalence of product_type = 'plan' and plan_ladder_id IS NOT NULL (Decision 117). Added entitlements.pool_provision_ladders junction with partial GiST exclusion constraint enforcing at most one active provision per (pool, ladder); denormalized columns synced by AFTER UPDATE trigger on pool_provisions (Decision 118). Resolved Issue 25 (Trial Handling): no structural change — trial specification retained at price/subscription layer, pool_provision_ladders.status remains binary (Decision 119). Schema-per-document policy (Decision 113) and cross-module FK convention (Decision 114) unchanged. Added Document 30 entries to Exploratory Documents. Opened Issue 24 (Subscription Management Module Extraction) — deferred with holding document. 58 tables, 119 decisions.
14.0 Integrated Issue 26 (subscription-change economics, Document 36) into the module schema reference. Added billing.subscription_scheduled_changes — a first-class scheduled-change intent entity distinct from the append-only subscription_changes/pool_provision_transitions records, with a partial-unique invariant of one pending change per (subscription, item, kind) (Decision 123). Added billing_accounts.default_credit_disposition and credit_transactions.source_type += 'proration' for ledger-default proration-credit routing, cash as recorded exception (Decision 124). Added entitlements.entitlement_set_rules.tier_reduction_policy (block/defer/clamp/force_reduce, default defer), enforced at the locus that knows allocation (Decision 125). Added billing.subscriptions commitment attributes (commitment_end, commitment_renewal, early_termination_policy) with the commitment_fields_coherent CHECK, plus invoice_line_items.line_type += 'early_termination_fee' (Decision 126). All changes additive — current behavior is the zero-configuration default. Resolved Issue 24 (Document 38): the subscription lifecycle is kept within billing as a named internal seam rather than a separate module (Decisions 127128); introduced the module-vs-seam distinction (GLOSSARY.md §5.7) and surfaced Issue 27 (latent seam audit). 59 tables, 128 decisions.
15.0 Ratified and integrated Issue 29 (Document 41, Decisions 134139) by direct arbitration, 2026-08-21. Catalog: products.product_type renamed to display_category (presentation-owned, no domain CHECK); product_kinds view dropped, replaced by product_conferral_shapes (conferral read path) and product_shape (per-dimension diagnostics with 'mixed' first-class); usage_pricing_requires_recurrence CHECK on prices. Grants: entitlement_set_id dropped, product_id NOT NULL, extends_grant_id added, grant_reason NOT NULL with CHECK domain and the default_iff_system_authored biconditional, granted_by_person_id nullable. Provisions: product_id denormalized at conferral; quantity NOT NULL DEFAULT 1 with quantity_positive; three per-source live partial unique indexes. Junction: surrogate PK provision_ladder_id with live-uniqueness partial index; sync trigger monotonic-end guard. Transitions: narrowed to position audit with fifth type transfer, rank-shape CHECK, and occupancy FK (NOT VALID over history) — and the table itself enters the master reference, closing a drift in which it was documented only in the retired module projection: 59 → 60 tables. Five-member SECURITY DEFINER conferral family; writer DML revoked on the position machinery. Decisions 112, 118, 119 annotated as partially amended. Also recorded this cycle: Decisions 140141 (Document 43 — physical topology core + domains + integration + per-provider superseding Decision 113's physical claim; cross-repo documentation division of labor; per-module model.md projections retired). Resolved Issues 29 and 30; surfaced Issue 31 (cross-source supersession vs. continued billing). 60 tables; decisions numbered through 141 (129133 remain proposed pending Issue 28 Phase 9d).
16.0 Ratified and integrated Issue 28 (Documents 3940, Decisions 129133 as amended by Document 44) upon satisfaction of the conformance gate (member-console status/explorations/doc39-conformance-2026-08/doc39-conformance-2026-08-21.md; no waiver, per arbitration). Added billing.providers (canonical provider registry: slug PK, provider_kind, status with soft-delete discipline), billing.provider_operations (boot-registered capability verbs: create/set_status/delete/list/describe), and billing.provider_states (provider-declared lifecycle vocabularies) — the provider/extension seam's registry, spanning billing/entitlements/integration without extraction (Doc 39 §5). Added entitlements.resource_keys.provider (nullable FK — fact-source cardinality; platform-owns-unprefixed / <slug>_* namespacing). Corrected integration.webhook_events dedup contract: cross-time idempotency is handler-level (a global unique constraint cannot exist on the partitioned table); in-table constraint backstops same-instant redelivery only. Soft-string FK promotions documented as designed-deferred. plan_ladders.provider re-scoped out with a named trigger. Decision numbering gap closed: all decisions through 141 are ratified. Resolved Issue 28; surfaced Issues 3233 (core-event fan-out; converged-set delivery). 63 tables, 141 decisions.
16.1 Entity slug retirement (maintainer decision from the member-console side, 2026-08-28; applied directly, without a numbered-decision process). Dropped slug from organization.organizations, organization.workspaces, and entitlements.resource_pools. Rationale, from member-console's identifier survey: no route, lookup, or rename ever consumed these columns — URLs are UUID-keyed and presentation is name-keyed — and the organization value was in practice being filled from the OIDC preferred_username, persisting a login name no one could see. The product's only genuinely public handle is the FedWiki site hostname, which already carries DNS-label rules of its own. The resulting division is clean: identity is the UUID, presentation is the name. Disambiguation, previously an incidental effect of slug uniqueness, is now carried explicitly — workspace name is unique per organization, case-insensitively, over live rows (WHERE status <> 'deleted' — the live-partial pattern of v15.0, so a soft-deleted workspace frees its name). The System-tenant index takes no such predicate: it admits exactly one row, ever. The platform-administration convention is correspondingly restated: the cooperative is no longer located by the magic string slug = 'platform' but by org_type = 'system', a System tenant held to a single row by a partial unique index over org_type where org_type = 'system' — a structural predicate an operator cannot rename out from under the platform. plan_ladders.ladder_key (a stable machine identifier) and the billing.providers slug primary key (Decisions 129133) are unaffected and remain. Full exploration with primary-source research: member-console status/explorations/identifiers-2026-08/identifiers-exploration.md (appendices in status/explorations/identifiers-2026-08/identifiers-exploration/). Two unrelated as-built drifts noted in passing, not absorbed: billing.plan_ladders.ladder_key is VARCHAR(64) in the designed model and VARCHAR(100) as built; the personal-organization owner CHECK (CHECK (org_type != 'personal' OR owner_person_id IS NOT NULL)) is absent as built. Both are recorded here for disposition rather than silently reconciled in either direction. 63 tables, 141 decisions.
16.2 invoices.invoice_number specified (maintainer arbitration, 2026-08-29, following the parallel identifier drafts and their cross-critiques — Document 45, documents/critique-of-member-console-identifiers.md, and member-console status/explorations/identifiers-2026-08/identifiers-comparison.md). The arbitration is that the model keeps platform-side invoice issuance: pending_charges are swept into a draft invoice before any provider is involved, and the model owns the invoice arithmetic and the credit ledger, so core must be able to number a document no provider numbered. invoice_number is therefore a reference number — a citation, not an address and not a provider identifier — assigned by the platform at issuance, unique per billing account ((billing_account_id, invoice_number) over non-null values, not the global uniqueness previously implied), NULL only while status = 'draft' (CHECK (status = 'draft' OR invoice_number IS NOT NULL)), immutable once assigned, and never reused — a voided invoice keeps its number. Added billing.billing_accounts.next_invoice_number (INTEGER NOT NULL DEFAULT 1), incremented and read inside the issuing transaction: a PostgreSQL sequence was rejected because nextval() is non-transactional and burns values on rollback, and gapless numbering is a jurisdictional requirement for issued invoices in several jurisdictions. A payment provider's own invoice number is an external reference living in that provider's integration schema (Decision 86), with the platform's number pushed to the provider where the provider accepts one (Stripe's create-invoice endpoint takes a number parameter) so a member sees one number everywhere. Full specification, rationale, and the four required implementation changes: Document 45 §13. No decision number assigned — a specification of an existing under-specified column, arbitrated directly. 63 tables, 141 decisions.
16.3 Entity keys introduced; v16.1's slug drops become renames (maintainer decision, 2026-08-29; approved design: member-console status/explorations/identifiers-2026-08/entity-keys-design.md). This amends v16.1 and reverses the seed-identity approach recorded at v16.2-era in Document 45 §8. The reversal's ground is Decision 30: all tables use database-generated UUIDv7, so an external caller cannot compute or supply an ID. Document 45 had excluded seeds from its consumer test (criterion C5) on the strength of label-derived UUIDv5 identifiers — a technique that silently contravenes Decision 30 for every seeded row. The two sessions treated that as a documented exception between themselves instead of putting the conflict to the maintainer; it is not theirs to except. C5 is withdrawn and folds into C1: a seed is code naming a row literally, and is a legitimate consumer. Added the Entity Key Contract to Structural Policies: key TEXT NULL, grammar ^[a-z][a-z0-9_]*$ (≤64 chars, one CHECK per table), NULLs distinct so no partial index is needed, unique within the smallest inhabited namespace (parent where one exists, table where none does), mutable with the old value freed — a key is a declarative address, not a public handle — and never derived from a person's data. Keys are assigned by class, never per column: tenant/actor and catalog entities carry one; vocabularies already are keys and keep their ratified column names as the one spelling exception; ledgers, relationships, and documents never carry one. Where an external reference already uniquely names every row it is the key and no second column is added (users.oidc_subject, hostnames, per-provider mapping identifiers). Schema: organizations.slugkey (nullable, UNIQUE (key)), workspaces.slugkey (UNIQUE (org_id, key)), resource_pools.slugkey (UNIQUE (org_id, key)), plan_ladders.ladder_keykey (nullable, closing the VARCHAR(64)/(100) drift); entitlement_sets, products, and prices gain key (prices scoped (product_id, key) — the analogue of Stripe's lookup_key). The v16.1 System-tenant singleton index and live-rows workspace name guard stand unchanged, as does v16.2 invoice numbering. AIP-133 client-supplied IDs are withdrawn from the policy as incompatible with Decision 30, and the seed-identity cross-repository contract is withdrawn entirely. Two rules added to the contract before review, closing hazards raised against the design: identity versus address (where a row is located by a structural predicate — org_type = 'system', pool_type = 'default' — the predicate is the identity and the constant key is only a declarative address; platform code resolves by predicate and never by key), and seeds ensure existence, never update (ON CONFLICT … DO NOTHING, not DO UPDATE: since a key may be reassigned, a seed naming a key it no longer owns lands on whichever row now holds it, and updating would silently overwrite an operator's edits on a row the seed was never meant to touch; a declarative loader whose file is the source of truth may update and must declare it). 63 tables, 141 decisions.