# Data Model Reference **Version 11.0** > **Modular navigation available.** This document is the consolidated master reference for all 54 tables. For module-scoped navigation, see [`modules/README.md`](README.md), which decomposes the model into six independently navigable modules (identity, organization, entitlements, billing, audit, integration). The master reference remains authoritative; module files are projections providing navigability and maintainability. --- ## Foundational Concepts This data model separates four concerns that are often conflated: 1. **Identity & Credentials**: Who is this actor, and how do they authenticate? 2. **Organization & Access**: What containers exist, and who can access them with what permissions? 3. **Entitlements**: What capabilities are available, and who provisioned them? 4. **Billing & Value**: Who pays, what do they pay for, and what value have they contributed? These concerns have different lifecycles, different data requirements, and different stakeholders. A person's legal name for tax forms is not the same as their login email. An organization's access control is not the same as its billing arrangement. A resource pool's entitlements are not the same as the subscription that funded them. Separating these concerns allows each to evolve independently. Four architectural principles govern the relationships between these modules: **Billing and governance are orthogonal.** Who pays for a resource and who controls a resource are different questions with different answers. The data model does not structurally fuse them. A billing account in one organization can provision a resource pool in another organization. This principle is drawn from analysis of GCP's resource hierarchy. **Access mechanisms are plural.** Subscriptions are one way to obtain access. One-time purchases and administrative grants are others. All access mechanisms produce pool provisions through a uniform interface. The entitlement system does not need to know how access was obtained. **Actors are plural.** Humans are not the only entities that act in the system. Personal access tokens, service accounts, and the system itself all perform actions. Each actor type authenticates differently, receives permissions differently, and appears in audit trails distinctly. **Payment topologies are plural.** Postpaid invoicing (consume first, pay later) is one payment topology. Prepaid credits (pay first, consume later) is another. Hybrid models blend both. The billing system supports all three without requiring structural changes to the entities that track consumption, entitlements, or access control. The payment topology is a billing account configuration, not an architectural assumption. --- ## Primary Key Strategy All tables use **UUIDv7** as their primary key type. UUIDv7 (RFC 9562) encodes a millisecond-precision Unix timestamp in the high bits followed by random data, producing identifiers that are globally unique, time-ordered, and non-sequential. This choice addresses three requirements: - **Non-predictability.** UUIDv7 values cannot be enumerated or used to infer entity counts. Sequential BIGSERIAL keys leak creation order and cardinality. - **Merge safety.** If two instances of this system need to merge (e.g., cooperative federation, environment reconciliation), UUIDv7 keys will not collide. Auto-incrementing keys require remapping. - **Partition friendliness.** The time-ordered prefix supports range-based partitioning on high-volume tables (`audit_logs`, `usage_events`) without a separate partition key. The time-ordered prefix preserves B-tree index locality — inserts append near the end of the index, and range scans on creation time are efficient. The cost relative to BIGSERIAL is 16 bytes per key instead of 8, which increases index size and JOIN overhead. This is acceptable for the entity cardinalities this system expects. **Timestamp leakage.** UUIDv7 encodes the creation timestamp in the high 48 bits. This is acceptable for internal identifiers. If the system develops an external-facing API, tables exposed through that API should add an opaque, random external identifier column (e.g., `external_id UUID DEFAULT gen_random_uuid()`) that is used in API responses and URL paths. Internal UUIDv7 keys should not appear in external interfaces. Throughout this document, `UUID` as a column type denotes UUIDv7 with a `DEFAULT uuidv7()` generation strategy. --- ## Schema Organization Each domain module occupies its own PostgreSQL schema, matching the module name (Decision 113): | Schema | Module | Tables | |--------|--------|--------| | `identity` | Identity & Credentials | 5 | | `organization` | Organization & Access | 8 | | `entitlements` | Entitlements | 13 | | `billing` | Billing & Value | 20 | | `cooperative` | Cooperative | 2 | | `audit` | Audit | 5 | | `integration` | Integration | 2 | The `public` schema holds shared extensions and functions only. Cross-module foreign keys are retained with schema-qualified references (Decision 114). Integration data for external services (payment processors, infrastructure provisioning, tax computation, notification delivery) lives in dedicated per-provider schemas (e.g., `stripe`, `polar`, `nextcloud`). Integration schemas are designed when each integration is built and are not part of this reference document. They reference domain tables via schema-qualified foreign keys (e.g., `REFERENCES billing.products(product_id)`) but do not reference each other (Decision 86). --- ## Structural Policies Four cross-cutting policies govern the behavior of this schema. Their full specifications are maintained as separate policy documents; this section summarizes the constraints that table definitions depend upon. **Soft-Delete and Terminal State Policy.** No record that has ever been referenced by another table's foreign key is physically deleted. All lifecycle terminations are status transitions to terminal states. Foreign key references to terminal-state records are valid and expected. All FK constraints use `ON DELETE RESTRICT`; exceptions require documented justification in the migration. **GDPR Anonymization Protocol.** Erasure requests are satisfied by comprehensive anonymization — irreversible destruction of all PII on the `persons` and linked `users` records — not physical deletion. Before executing, the protocol checks for active retention holds. If holds exist, partial erasure is performed: non-retained PII is scrubbed while legally required fields are preserved. **JSONB Governance Policy.** No JSONB column stores personally identifiable information or sensitive personal data. This is a categorical prohibition with no exceptions. JSONB columns serve as structured extensibility surfaces for configuration, supplementary attributes, and non-PII metadata. Attributes that become query dependencies should be promoted to relational columns. **Temporal Modeling Convention.** Each status-bearing entity records explicit `{state}_at` timestamps for each non-initial status value. Governance-sensitive entities additionally record `{state}_by` actor attribution. These columns capture the most recent entry into each state. The audit log serves as the complete history for repeated transition cycles. Status transitions must be recorded in the audit log `changes` JSONB as `{"status": {"from": "X", "to": "Y"}}`. --- ## Identity & Credentials — `identity` schema Identity & Credentials answers: "Who is this actor, and how do they authenticate?" This module covers both human and non-human actors. Human actors authenticate through the identity provider (Keycloak) as users, or programmatically through personal access tokens. Non-human actors authenticate through service account API keys or (optionally) Keycloak client credentials. ### users A **user** is an authentication identity. It represents a login credential managed by the identity provider (Keycloak). This table is a local cache of authentication data, synchronized from the identity provider. A user is not a person. A user is a way for a person to authenticate. This distinction matters because: - A person exists in the business domain even before they create a login (e.g., when invited) - Authentication data (email, username) may differ from legal/business data - If the identity provider changes, user records change but person records persist | Field | Type | Purpose | |-------|------|---------| | `user_id` | UUID | Primary key | | `oidc_subject` | VARCHAR(255) | Unique identifier from identity provider. Immutable reference that survives email changes. | | `oidc_issuer` | VARCHAR(255) | Which identity provider issued this identity. Supports multiple IdPs. | | `email` | VARCHAR(255) | Email used for authentication. May differ from billing email. | | `email_verified` | BOOLEAN | Whether the identity provider has verified this email. | | `username` | VARCHAR(100) | Display username from identity provider. | | `display_name` | VARCHAR(255) | Human-readable name for UI display. | | `avatar_url` | VARCHAR(500) | Profile image URL. | | `locale` | VARCHAR(10) | Preferred language/locale. | | `timezone` | VARCHAR(50) | Preferred timezone for date display. | | `last_login_at` | TIMESTAMPTZ | Most recent authentication timestamp. | | `last_login_ip` | INET | IP address of most recent login. For security auditing. | | `status` | VARCHAR(20) | Account state: `active`, `suspended`, `deleted`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `deleted_at` | TIMESTAMPTZ | When deleted. | | `created_at` | TIMESTAMPTZ | When this auth identity was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Relationships:** - `users` (1) → (0..1) `persons`: A user may be linked to a person. The relationship is optional on the persons side because a person can exist before they sign up (invited but not yet registered). --- ### persons A **person** is a human being as a business and legal entity. It holds information needed for invoices, contracts, tax forms, and cooperative membership. A person is not a user. A person is a human who participates in business relationships. This distinction matters because: - Business data (legal name, tax ID, mailing address) doesn't belong in the authentication system - A person may be invited to an organization before they create a login - Cooperative membership, equity ownership, and patronage belong to the person, not to their login credentials - Legal and tax requirements (like 1099-PATR forms) require data the auth system doesn't have | Field | Type | Purpose | |-------|------|---------| | `person_id` | UUID | Primary key | | `user_id` | UUID | FK → users. Link to authentication identity. NULL if invited but not yet signed up. | | `legal_first_name` | VARCHAR(100) | Legal first name as it appears on official documents. | | `legal_last_name` | VARCHAR(100) | Legal last name as it appears on official documents. | | `phone` | VARCHAR(50) | Contact phone number. | | `address_line1` | VARCHAR(255) | Street address line 1. Required for invoices and tax forms. | | `address_line2` | VARCHAR(255) | Street address line 2. | | `city` | VARCHAR(100) | City. | | `state_province` | VARCHAR(100) | State or province. | | `postal_code` | VARCHAR(20) | Postal/ZIP code. | | `country_code` | VARCHAR(2) | ISO 3166-1 alpha-2 country code. | | `tax_id_type` | VARCHAR(20) | Type of tax identifier: `ssn`, `ein`, `itin`, `vat`, `gst`, `other`. | | `tax_id_last4` | VARCHAR(4) | Last 4 digits of tax ID. Full ID stored in secure vault. | | `tax_id_verified` | BOOLEAN | Whether tax ID has been verified. | | `tax_id_verified_at` | TIMESTAMPTZ | When tax ID was verified. | | `retention_hold` | BOOLEAN DEFAULT FALSE | Whether this person's PII is subject to an active retention obligation. When `true`, the anonymization protocol must not execute without resolving the hold. | | `status` | VARCHAR(20) | State: `pending`, `active`, `inactive`, `partially_erased`, `anonymized`, `merged`. | | `activated_at` | TIMESTAMPTZ | When pending → active. | | `deactivated_at` | TIMESTAMPTZ | When active → inactive. | | `deactivated_by` | UUID | FK → persons. Who deactivated. | | `partially_erased_at` | TIMESTAMPTZ | When partial erasure was performed. | | `anonymized_at` | TIMESTAMPTZ | When full anonymization was performed. | | `created_at` | TIMESTAMPTZ | When this person record was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Constraints:** - `user_id` has a partial unique index: `UNIQUE (user_id) WHERE user_id IS NOT NULL`. A user maps to at most one person. **Status lifecycle:** - `pending` → `active`: Invitation accepted, account established. - `active` → `inactive`: Administrative deactivation. Reversible. - `active` → `partially_erased`: Erasure requested but retention holds prevent full anonymization. Non-retained PII scrubbed. Not reversible. - `active` or `inactive` → `anonymized`: Full PII destruction. Irrevocable terminal state. - `partially_erased` → `anonymized`: All retention holds released. Remaining PII scrubbed. Irrevocable. - `active` → `merged`: Duplicate resolved. All FK references repointed to surviving person. Irrevocable. **Relationships:** - `persons` (0..1) → (1) `users`: A person may be linked to a user for authentication. The relationship is 1:1: each user maps to at most one person (enforced by partial unique index on `user_id`), and each person has at most one user (enforced by the single FK column). - `persons` (1) → (0..1) `organization.organizations`: A person may own a personal organization. - `persons` (1) → (0..*) `organization.org_members`: A person may be a member of multiple organizations. - `persons` (1) → (0..*) `organization.workspaces`: A person may have created workspaces. - `persons` (1) → (0..*) `organization.role_assignments`: A person may have scoped role assignments. - `persons` (1) → (0..*) `personal_access_tokens`: A person may have programmatic access tokens. - `persons` (1) → (0..*) `retention_holds`: A person may have active retention obligations. --- ### personal_access_tokens A **personal_access_token** is a credential that allows a person to access the API programmatically. The token acts as the person — it inherits the person's permissions (potentially narrowed by scopes). It is the most common form of programmatic access for human users. Personal access tokens are distinct from service accounts. A PAT acts *as the person* (same identity, same permissions). A service account acts *as itself* (independent identity, independently granted permissions). This distinction matters for audit trails, revocation semantics, and the "what happens when a person leaves the organization" question — their PATs are revoked with their account, but service accounts they created continue operating. | Field | Type | Purpose | |-------|------|---------| | `token_id` | UUID | Primary key | | `person_id` | UUID | FK → persons. The person this token acts as. | | `name` | VARCHAR(255) | Human-readable name (e.g., "CI deploy token", "Local development"). | | `description` | TEXT | What this token is used for. | | `token_hash` | VARCHAR(255) | Hashed token value. The plaintext is shown once at creation and never stored. | | `token_prefix` | VARCHAR(10) | First few characters of the token, for identification in logs and UI (e.g., "mc_pat_a3f..."). | | `scopes` | TEXT[] | Permission scopes this token is limited to. NULL means full permissions of the person. When set, effective permissions are the intersection of the person's permissions and the token's scopes. | | `expires_at` | TIMESTAMPTZ | When this token expires. NULL for no expiration (discouraged). | | `last_used_at` | TIMESTAMPTZ | When this token was last used for authentication. | | `last_used_ip` | INET | IP address of last use. | | `revoked_at` | TIMESTAMPTZ | When this token was revoked. | | `revoked_by_person_id` | UUID | FK → persons. Who revoked this token (may differ from the owner). | | `status` | VARCHAR(20) | `active`, `expired`, `revoked`. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Token format convention:** `mc_pat_` — the prefix `mc` identifies the Member Console, `pat` identifies the token type. Leaked tokens can be identified and traced by prefix. **Security properties:** - The plaintext token is displayed exactly once at creation. Only the hash is stored. - The `token_prefix` allows identification without exposing the full token. - Expired and revoked tokens cannot authenticate. Status checks are part of the authentication path. - Tokens inherit the person's *current* permissions, not a snapshot. If the person loses access, their tokens lose access immediately. **Relationships:** - `personal_access_tokens` (0..*) → (1) `persons`: Tokens belong to a person. --- ### retention_holds A **retention_hold** records an active legal obligation requiring retention of a person's PII. Retention holds prevent the anonymization protocol from executing full erasure and instead trigger partial erasure — scrubbing non-retained data while preserving fields required by the cited legal authority. | Field | Type | Purpose | |-------|------|---------| | `hold_id` | UUID | Primary key | | `person_id` | UUID | FK → persons. The person whose PII is held. | | `legal_authority` | VARCHAR(100) | The legal basis for retention (e.g., `irc_6001`, `irc_1381_1388`, `26_cfr_31_6001`). | | `description` | TEXT | Human-readable explanation of the retention obligation. | | `data_categories` | TEXT[] | Which data categories are held (e.g., `['legal_name', 'tax_id', 'billing_address']`). | | `hold_placed_at` | TIMESTAMPTZ | When the hold was placed. | | `hold_placed_by` | UUID | FK → persons. Who placed the hold (NULL if system-automated). | | `hold_expires_at` | TIMESTAMPTZ | When the retention obligation expires. NULL if the expiration depends on a future event (e.g., equity redemption). | | `hold_released_at` | TIMESTAMPTZ | When the hold was released. NULL if still active. | | `hold_released_by` | UUID | FK → persons. Who released the hold. | | `release_reason` | TEXT | Why the hold was released. | | `status` | VARCHAR(20) | `active`, `released`, `expired`. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Behavioral contract:** When any `retention_holds` record with `status = 'active'` exists for a person, the `persons.retention_hold` flag must be `true`. When the last active hold is released or expires, the flag is set to `false`. This denormalization is acceptable because the flag is a hot-path check (consulted on every anonymization request) while the holds table is a cold-path audit record (consulted when evaluating *why* a hold exists and *when* it expires). **Relationships:** - `retention_holds` (0..*) → (1) `persons`: Holds apply to a person. --- ### person_merges A **person_merge** records that one person record was merged into another. This provides structured provenance for duplicate resolution, supporting potential reversal and audit compliance. | Field | Type | Purpose | |-------|------|---------| | `merge_id` | UUID | Primary key | | `source_person_id` | UUID | FK → persons. The person being merged away. | | `target_person_id` | UUID | FK → persons. The surviving person. | | `merged_by_person_id` | UUID | FK → persons. Who authorized the merge. | | `merged_at` | TIMESTAMPTZ | When the merge was performed. | | `reason` | TEXT | Why the merge was necessary. | | `affected_references` | JSONB | Snapshot of tables and row counts updated during the merge. | | `created_at` | TIMESTAMPTZ | | **Constraints:** - `source_person_id` ≠ `target_person_id`. **Merge operation:** Executes within a single transaction: (1) validate source is not already `merged` or `anonymized`; (2) repoint all FK references from source to target across all person-referencing tables; (3) transfer active retention holds from source to target; (4) set source status to `merged`; (5) insert `person_merges` record; (6) insert `audit_logs` entry. See the Soft-Delete and Terminal State Policy for the full affected-tables checklist and uniqueness conflict resolution. **Relationships:** - `person_merges` (0..*) → (1) `persons`: Merges reference source and target persons. --- ## Organization & Access — `organization` schema Organization & Access answers: "What containers exist, and who can access them with what permissions?" ### organizations An **organization** is the top-level container for all resources, members, and billing relationships. Every person operates within the context of at least one organization. There are no solo users in this model. Every person has a personal organization created automatically. This design choice means: - The data model is uniform whether someone is solo or on a team - Solo users can invite collaborators without migration - All resources, billing, and access control work the same way regardless of organization size - Organizations can be personal (one owner), team (multiple members), or enterprise (formal business) **Platform administration convention:** The cooperative itself is represented as an organization in the model (e.g., slug = `platform`). Platform administrators are members of this organization with the `platform_admin` role. This reuses the existing org → members → roles mechanism for platform-level governance without requiring new schema entities. | Field | Type | Purpose | |-------|------|---------| | `org_id` | UUID | Primary key | | `name` | VARCHAR(255) | Display name of the organization. | | `slug` | VARCHAR(100) | URL-safe identifier. Globally unique. Used in URLs and API references. | | `org_type` | VARCHAR(20) | Classification: `personal` (auto-created for individuals), `team` (collaborative), `enterprise` (formal business). | | `owner_person_id` | UUID | FK → identity.persons. For personal organizations, the person who owns it. Required when `org_type` = `personal`. | | `legal_name` | VARCHAR(255) | Registered legal name of the business entity. | | `entity_type` | VARCHAR(50) | Legal structure: `llc`, `corporation`, `nonprofit`, `cooperative`, `sole_prop`. | | `tax_id` | VARCHAR(100) | Business tax identifier (EIN for US companies). | | `website` | VARCHAR(255) | Organization's website. | | `settings` | JSONB | Organization-level configuration and preferences. | | `status` | VARCHAR(20) | State: `active`, `suspended`, `deleted`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `suspended_by` | UUID | FK → identity.persons. Who suspended. | | `deleted_at` | TIMESTAMPTZ | When deleted. | | `deleted_by` | UUID | FK → identity.persons. Who deleted. | | `created_at` | TIMESTAMPTZ | When this organization was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Constraints:** - Personal organizations must have an `owner_person_id`. Enforced by: `CHECK (org_type != 'personal' OR owner_person_id IS NOT NULL)`. - `slug` is globally unique to enable clean URLs. **Relationships:** - `organizations` (0..1) → (1) `identity.persons`: Personal organizations have an owner person. - `organizations` (1) → (0..*) `org_members`: Organizations have members. - `organizations` (1) → (0..*) `workspaces`: Organizations contain workspaces. - `organizations` (1) → (0..*) `billing.billing_accounts`: Organizations have billing accounts. - `organizations` (1) → (0..*) `entitlements.resource_pools`: Organizations have resource pools. - `organizations` (1) → (0..*) `roles`: Organizations may define custom roles. - `organizations` (1) → (0..*) `service_accounts`: Organizations have service accounts. - `organizations` (1) → (0..*) `invitations`: Organizations may have pending invitations. --- ### service_accounts A **service_account** is a non-human actor with its own identity and permissions. It represents an automated system or integration that interacts with the platform's API independently of any person. Service accounts belong to organizations, not persons. They are created and managed by organization administrators. They have their own role assignments, independent of any person's permissions. Service accounts are distinct from personal access tokens: a PAT acts *as a person* (inheriting their permissions); a service account acts *as itself* (with independently granted permissions). When a person leaves an organization, their PATs are revoked, but service accounts they created continue operating. Service accounts are locally-managed by default, with optional Keycloak linkage. The `oidc_subject` and `oidc_issuer` fields allow linking to a Keycloak client's service account identity without requiring it. Creating a Keycloak client for every service account is unnecessarily heavy; local API keys are the primary authentication mechanism. | Field | Type | Purpose | |-------|------|---------| | `service_account_id` | UUID | Primary key | | `org_id` | UUID | FK → organizations. The organization this service account belongs to. | | `name` | VARCHAR(255) | Display name (e.g., "CI/CD Pipeline", "Nextcloud Connector", "Backup Service"). | | `description` | TEXT | What this service account does. | | `oidc_subject` | VARCHAR(255) | If linked to a Keycloak service account: the OIDC subject identifier. NULL for locally-managed accounts. | | `oidc_issuer` | VARCHAR(255) | If linked to Keycloak: the issuer. NULL for locally-managed accounts. | | `created_by_person_id` | UUID | FK → identity.persons. Who created this service account. | | `status` | VARCHAR(20) | `active`, `suspended`, `deleted`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `suspended_by` | UUID | FK → identity.persons. Who suspended. | | `deleted_at` | TIMESTAMPTZ | When deleted. | | `deleted_by` | UUID | FK → identity.persons. Who deleted. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `service_accounts` (0..*) → (1) `organizations`: Service accounts belong to an organization. - `service_accounts` (0..*) → (0..1) `identity.persons`: Service accounts track who created them. - `service_accounts` (1) → (0..*) `service_account_keys`: Service accounts have API keys. - `service_accounts` (1) → (0..*) `role_assignments`: Service accounts receive scoped role assignments. --- ### service_account_keys A **service_account_key** is a credential for a service account. Service accounts may have multiple keys to support rotation: a new key is created before the old one is retired, ensuring uninterrupted access during the transition. | Field | Type | Purpose | |-------|------|---------| | `key_id` | UUID | Primary key | | `service_account_id` | UUID | FK → service_accounts. | | `name` | VARCHAR(255) | Human-readable name (e.g., "Production key", "Rotation 2026-02"). | | `key_hash` | VARCHAR(255) | Hashed key value. Plaintext shown once at creation. | | `key_prefix` | VARCHAR(10) | First few characters for identification (e.g., "mc_sak_b7d..."). | | `expires_at` | TIMESTAMPTZ | When this key expires. NULL for no expiration. | | `last_used_at` | TIMESTAMPTZ | When this key was last used. | | `last_used_ip` | INET | IP address of last use. | | `revoked_at` | TIMESTAMPTZ | When revoked. | | `revoked_by_person_id` | UUID | FK → identity.persons. Who revoked it. | | `status` | VARCHAR(20) | `active`, `expired`, `revoked`. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Token format convention:** `mc_sak_` (Member Console, Service Account Key). Distinct from PATs for immediate identification. **Relationships:** - `service_account_keys` (0..*) → (1) `service_accounts`: Keys belong to a service account. --- ### roles A **role** defines a named set of permissions. Roles determine what actions an actor (person or service account) can perform within an organization, workspace, pool, or other scope. Roles are either system-defined (built-in, applicable to all organizations) or organization-defined (custom roles for a specific organization). System roles provide consistent semantics across the platform. Custom roles allow organizations to tailor access control to their needs. Permissions are flat and explicit. Each role states exactly what it grants as an array of permission strings. There is no implicit derivation, no inheritance. Given an actor's roles, you can enumerate their exact permissions without traversal. | Field | Type | Purpose | |-------|------|---------| | `role_id` | UUID | Primary key | | `org_id` | UUID | FK → organizations. If set, this is a custom role for this organization. If NULL, this is a system role. | | `role_name` | VARCHAR(100) | Machine-readable identifier. Unique within scope (system or per-org). | | `display_name` | VARCHAR(255) | Human-readable name for UI. | | `description` | TEXT | Explanation of what this role is for. | | `is_system` | BOOLEAN | Whether this is a built-in system role. System roles cannot be deleted or modified. | | `permissions` | TEXT[] | Array of permission strings. Format: `resource:action` (e.g., `billing:manage`, `workspace:delete`). | | `created_at` | TIMESTAMPTZ | When this role was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Constraints:** - Custom roles may only contain permissions from the defined vocabulary. Application-level validation prevents invalid or stale permission strings. **System roles:** | Role | Purpose | Key Differentiators | |------|---------|-------------------| | `owner` | Full authority over the organization. | Includes `org:delete`, `org:transfer`. Only role that can destroy or transfer the organization. | | `admin` | Full access except irreversible org-level actions. | Everything except `org:delete`, `org:transfer`. | | `member` | Can work within workspaces and manage resources. | No billing, pool, or org-structure access. | | `billing` | Financial access only. | Can manage billing accounts, subscriptions, purchases, invoices. Cannot access workspaces or resources. | | `viewer` | Read-only access to all visible entities. | Can see everything but modify nothing. | | `platform_admin` | Platform-level administrative access. | Includes `entitlement_rules:manage`. Assigned within the platform organization only. | **Relationships:** - `roles` (0..*) → (0..1) `organizations`: Custom roles belong to an organization. System roles have no organization. - `roles` (1) → (0..*) `org_members`: Roles are assigned to organization members. - `roles` (1) → (0..*) `role_assignments`: Roles are assigned at scoped level. - `roles` (1) → (0..*) `invitations`: Roles are referenced by pending invitations. --- ### org_members An **org_member** represents the membership of a person in an organization with a specific role. This is the primary mechanism for organization-level access control. Membership is distinct from ownership and distinct from scoped role assignments. Membership represents *belonging* — a person is part of this organization. Role assignments represent *access* — an actor can do specific things in a specific scope. A person might have a workspace-scoped role assignment in an organization they are not a member of (e.g., a contractor with access to one workspace). Memberships are created upon invitation acceptance or direct administrative action — they are born `active`, never `pending`. Invitation state is tracked on the `invitations` entity. | Field | Type | Purpose | |-------|------|---------| | `org_member_id` | UUID | Primary key | | `org_id` | UUID | FK → organizations. The organization. | | `person_id` | UUID | FK → identity.persons. The person who is a member. | | `role_id` | UUID | FK → roles. The role this person has in this organization. | | `invitation_id` | UUID | FK → invitations. The invitation that created this membership. NULL if created without an invitation (e.g., auto-created for personal org owner, or platform-level administrative action). | | `status` | VARCHAR(20) | Membership state: `active`, `suspended`, `removed`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `suspended_by` | UUID | FK → identity.persons. Who suspended. | | `removed_at` | TIMESTAMPTZ | When removed. | | `removed_by` | UUID | FK → identity.persons. Who removed. | | `created_at` | TIMESTAMPTZ | When this membership was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Constraints:** - A person can only be a member of an organization once. The combination of `org_id` and `person_id` is unique. **Relationships:** - `org_members` (0..*) → (1) `organizations`: Memberships belong to an organization. - `org_members` (0..*) → (1) `identity.persons`: Memberships belong to a person. - `org_members` (0..*) → (1) `roles`: Memberships have a role. - `org_members` (0..*) → (0..1) `invitations`: Memberships may trace their provenance to an invitation. --- ### invitations An **invitation** is a request for someone to join an organizational scope (organization or workspace) with a specific role. Invitations exist independently of both `persons` and `org_members` — the invitee may not yet have an account. Invitee identity is progressive: an invitation begins addressed to an email and acquires richer identity references as its lifecycle progresses. This is a deliberate departure from the exclusive arc pattern used elsewhere in the model — the invitee identity evolves over the invitation's lifecycle rather than being fixed at creation. | Field | Type | Purpose | |-------|------|---------| | `invitation_id` | UUID | Primary key | | `invitee_email` | VARCHAR(255) | Email address of the invitee. NULL if inviting an existing person directly. | | `invitee_person_id` | UUID | FK → identity.persons. NULL if email-only invitation. May be populated when an existing person is discovered for the email. | | `org_id` | UUID | FK → organizations. If this is an organization-scoped invitation. NULL if workspace-scoped. | | `workspace_id` | UUID | FK → workspaces. If this is a workspace-scoped invitation. NULL if org-scoped. | | `role_id` | UUID | FK → roles. The role to be granted upon acceptance. | | `invited_by_person_id` | UUID | FK → identity.persons. Who created the invitation. | | `token_hash` | VARCHAR(255) | Hashed invitation token. Plaintext sent via email/notification. | | `token_prefix` | VARCHAR(10) | First characters for identification (e.g., `mc_inv_d8f...`). | | `message` | TEXT | Optional personal message from the inviter. | | `sent_at` | TIMESTAMPTZ | When the invitation was first sent. | | `last_sent_at` | TIMESTAMPTZ | When most recently sent (initial or resend). | | `send_count` | INTEGER DEFAULT 1 | Total times sent (initial + resends). | | `expires_at` | TIMESTAMPTZ | When the invitation expires. Reset on resend. | | `accepted_at` | TIMESTAMPTZ | When accepted. | | `resolved_person_id` | UUID | FK → identity.persons. The person who accepted. May be newly created (new signup) or pre-existing (existing user). Set upon acceptance. | | `resulting_member_id` | UUID | FK → org_members. If acceptance created a membership. | | `resulting_assignment_id` | UUID | FK → role_assignments. If acceptance created a role assignment. | | `declined_at` | TIMESTAMPTZ | When declined. | | `revoked_at` | TIMESTAMPTZ | When revoked. | | `revoked_by_person_id` | UUID | FK → identity.persons. Who revoked. | | `revocation_reason` | TEXT | Why revoked. | | `status` | VARCHAR(20) | `pending`, `sent`, `accepted`, `declined`, `expired`, `revoked`. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints (invitee — inclusive OR):** ```sql CHECK (invitee_email IS NOT NULL OR invitee_person_id IS NOT NULL) ``` At least one of `invitee_email` or `invitee_person_id` must be set. Both may coexist — this is progressive identity, not an exclusive arc. An invitation starts with the information available at creation time and acquires richer references as the lifecycle progresses. **Constraints (scope — exclusive arc):** ```sql CHECK ( (org_id IS NOT NULL AND workspace_id IS NULL) OR (org_id IS NULL AND workspace_id IS NOT NULL) ) ``` Exactly one of `org_id` or `workspace_id` must be set. An invitation targets one organizational scope. **Uniqueness:** Partial unique indexes enforce at most one active (non-terminal) invitation per (invitee, scope) combination. **Token format convention:** `mc_inv_`. Token is regenerated on resend (old token becomes invalid, new hash stored). **Status lifecycle:** - `pending` → `sent`: Notification delivered. - `sent` → `sent`: Resend (resets `expires_at`, increments `send_count`). - `sent` → `accepted`, `declined`, `expired`, `revoked`: Terminal states, all irrevocable. - `pending` → `expired`, `revoked`: Terminal without delivery. **Relationships:** - `invitations` (0..*) → (0..1) `identity.persons` (via `invitee_person_id`): Invitations may be addressed to a known person. - `invitations` (0..*) → (0..1) `identity.persons` (via `resolved_person_id`): Accepted invitations record who accepted. - `invitations` (0..*) → (1) `identity.persons` (via `invited_by_person_id`): Invitations are created by a person. - Exclusive arc — scope (`org_id` / `workspace_id`): - `invitations` (0..*) → (0..1) `organizations`: Org-scoped invitations target an organization. - `invitations` (0..*) → (0..1) `workspaces`: Workspace-scoped invitations target a workspace. - `invitations` (0..*) → (1) `roles`: Invitations specify a role to grant. - `invitations` (0..*) → (0..1) `org_members` (via `resulting_member_id`): Accepted org-scoped invitations create a membership. - `invitations` (0..*) → (0..1) `role_assignments` (via `resulting_assignment_id`): Accepted workspace-scoped invitations create an assignment. --- ### workspaces A **workspace** is a container for resources within an organization. Workspaces provide isolation, organization, and optionally different access controls within a single organization. | Field | Type | Purpose | |-------|------|---------| | `workspace_id` | UUID | Primary key | | `org_id` | UUID | FK → organizations. The organization this workspace belongs to. | | `name` | VARCHAR(255) | Display name of the workspace. | | `slug` | VARCHAR(100) | URL-safe identifier. Unique within the organization. | | `description` | TEXT | Explanation of what this workspace is for. | | `environment` | VARCHAR(20) | Classification: `development`, `staging`, `production`. Informational. | | `settings` | JSONB | Workspace-level configuration. | | `created_by_person_id` | UUID | FK → identity.persons. Who created this workspace. | | `status` | VARCHAR(20) | State: `active`, `archived`, `deleted`. | | `archived_at` | TIMESTAMPTZ | When archived. | | `archived_by` | UUID | FK → identity.persons. Who archived. | | `deleted_at` | TIMESTAMPTZ | When deleted. | | `deleted_by` | UUID | FK → identity.persons. Who deleted. | | `created_at` | TIMESTAMPTZ | When this workspace was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Constraints:** - `slug` is unique within an organization. The combination of `org_id` and `slug` is unique. **Relationships:** - `workspaces` (0..*) → (1) `organizations`: Workspaces belong to an organization. - `workspaces` (0..*) → (0..1) `identity.persons`: Workspaces track who created them. - `workspaces` (1) → (0..*) `role_assignments`: Workspaces may have scoped role assignments. - `workspaces` (1) → (1..*) `entitlements.pool_assignments`: Workspaces are assigned to one or more resource pools. - `workspaces` (1) → (0..*) `entitlements.usage_events`: Workspaces record resource consumption. - `workspaces` (1) → (0..*) `invitations`: Workspaces may have pending workspace-scoped invitations. --- ### role_assignments A **role_assignment** grants a role to an actor (person or service account) for a specific scope. This enables finer-grained access control than organization-level membership. Organization membership (via `org_members`) grants a baseline role to persons across the organization. Role assignments allow refinements and exceptions: - A person might be a viewer at org level but an admin for a specific workspace. - A contractor might have access to only one workspace, not the whole organization. - A service account might have access to exactly one workspace for CI/CD deployment. - Temporary access can be granted with an expiration date. Service accounts have no org membership — their permissions come entirely from role assignments. This enforces minimum privilege by design. | Field | Type | Purpose | |-------|------|---------| | `assignment_id` | UUID | Primary key | | `person_id` | UUID | FK → identity.persons. If the assignee is a person. NULL if service account. | | `service_account_id` | UUID | FK → service_accounts. If the assignee is a service account. NULL if person. | | `role_id` | UUID | FK → roles. The role being granted. | | `scope_org_id` | UUID | FK → organizations. If this assignment is organization-scoped. NULL otherwise. | | `scope_workspace_id` | UUID | FK → workspaces. If this assignment is workspace-scoped. NULL otherwise. | | `scope_pool_id` | UUID | FK → entitlements.resource_pools. If this assignment is pool-scoped. NULL otherwise. | | `granted_by_person_id` | UUID | FK → identity.persons. Who granted this role. For audit trail. | | `granted_at` | TIMESTAMPTZ | When this role was granted. | | `expires_at` | TIMESTAMPTZ | When this role expires. NULL means no expiration. | | `revoked_at` | TIMESTAMPTZ | When this role was revoked. | | `revoked_by_person_id` | UUID | FK → identity.persons. Who revoked. | | `status` | VARCHAR(20) | State: `active`, `revoked`, `expired`. | | `created_at` | TIMESTAMPTZ | When this assignment was created. | **Constraints (exclusive arc — actor):** ```sql CHECK ( (person_id IS NOT NULL AND service_account_id IS NULL) OR (person_id IS NULL AND service_account_id IS NOT NULL) ) ``` Exactly one of `person_id` or `service_account_id` must be set. **Constraints (exclusive arc — scope):** ```sql CHECK ( (scope_org_id IS NOT NULL AND scope_workspace_id IS NULL AND scope_pool_id IS NULL) OR (scope_org_id IS NULL AND scope_workspace_id IS NOT NULL AND scope_pool_id IS NULL) OR (scope_org_id IS NULL AND scope_workspace_id IS NULL AND scope_pool_id IS NOT NULL) ) ``` Exactly one of `scope_org_id`, `scope_workspace_id`, or `scope_pool_id` must be set. **Uniqueness:** The combination of actor (`person_id` or `service_account_id`), `role_id`, and scope (`scope_org_id` or `scope_workspace_id` or `scope_pool_id`) is unique. An actor cannot be assigned the same role to the same scope twice. **Relationships:** - Exclusive arc — actor (`person_id` / `service_account_id`): - `role_assignments` (0..*) → (0..1) `identity.persons`: Assignments may belong to a person. - `role_assignments` (0..*) → (0..1) `service_accounts`: Assignments may belong to a service account. - `role_assignments` (0..*) → (1) `roles`: Assignments grant a role. - Exclusive arc — scope (`scope_org_id` / `scope_workspace_id` / `scope_pool_id`): - `role_assignments` (0..*) → (0..1) `organizations`: Assignments may be scoped to an organization. - `role_assignments` (0..*) → (0..1) `workspaces`: Assignments may be scoped to a workspace. - `role_assignments` (0..*) → (0..1) `entitlements.resource_pools`: Assignments may be scoped to a pool. --- ## Permission System ### Permission String Format Permissions follow the format `resource:action`. Resources correspond to entity types and management surfaces. Actions are a small, consistent set. **Actions:** | Action | Meaning | |--------|---------| | `view` | Read/list the resource | | `create` | Create new instances | | `edit` | Modify existing instances | | `delete` | Remove instances | | `manage` | Full CRUD plus resource-specific administrative actions | ### Permission Vocabulary **Organization:** `org:view`, `org:edit`, `org:delete`, `org:transfer`, `org.members:view`, `org.members:manage`, `org.service_accounts:view`, `org.service_accounts:manage` **Workspaces:** `workspace:view`, `workspace:create`, `workspace:edit`, `workspace:delete`, `workspace.resources:view`, `workspace.resources:manage` **Entitlements:** `pool:view`, `pool:create`, `pool:edit`, `pool:delete`, `pool.assignments:view`, `pool.assignments:manage`, `pool.ondemand:view`, `pool.ondemand:manage` **Billing:** `billing:view`, `billing:manage`, `billing.subscriptions:view`, `billing.subscriptions:manage`, `billing.purchases:view`, `billing.purchases:create`, `billing.invoices:view` **Grants & Entitlement Rules:** `grants:view`, `grants:manage`, `entitlement_rules:view`, `entitlement_rules:manage` **Administration:** `roles:view`, `roles:manage`, `audit:view`, `tokens:manage` ### System Role Permission Sets **owner:** `org:view`, `org:edit`, `org:delete`, `org:transfer`, `org.members:view`, `org.members:manage`, `org.service_accounts:view`, `org.service_accounts:manage`, `workspace:view`, `workspace:create`, `workspace:edit`, `workspace:delete`, `workspace.resources:view`, `workspace.resources:manage`, `pool:view`, `pool:create`, `pool:edit`, `pool:delete`, `pool.assignments:view`, `pool.assignments:manage`, `pool.ondemand:view`, `pool.ondemand:manage`, `billing:view`, `billing:manage`, `billing.subscriptions:view`, `billing.subscriptions:manage`, `billing.purchases:view`, `billing.purchases:create`, `billing.invoices:view`, `grants:view`, `grants:manage`, `entitlement_rules:view`, `roles:view`, `roles:manage`, `audit:view` **admin:** Same as owner minus `org:delete`, `org:transfer`. **member:** `org:view`, `org.members:view`, `workspace:view`, `workspace.resources:view`, `workspace.resources:manage`, `pool:view`, `pool.assignments:view`, `billing.invoices:view` **billing:** `org:view`, `billing:view`, `billing:manage`, `billing.subscriptions:view`, `billing.subscriptions:manage`, `billing.purchases:view`, `billing.purchases:create`, `billing.invoices:view`, `pool:view`, `pool.ondemand:view` **viewer:** `org:view`, `org.members:view`, `workspace:view`, `workspace.resources:view`, `pool:view`, `pool.assignments:view`, `pool.ondemand:view`, `billing:view`, `billing.subscriptions:view`, `billing.purchases:view`, `billing.invoices:view`, `audit:view` **platform_admin:** All admin permissions plus `entitlement_rules:view`, `entitlement_rules:manage`. Assigned within the platform organization only. ### Permission Resolution Algorithm Permissions are **additive** (union) and **deny-by-default**. An actor can do nothing unless a permission explicitly allows it. There are no deny rules. **For persons:** ``` effective_permissions(person, context) = permissions_from_org_membership(person, context.org_id) ∪ permissions_from_scoped_assignments(person, context.scope) ``` If a person has `workspace.resources:view` from their org membership and `workspace.resources:manage` from a workspace-scoped assignment, they can manage resources in that workspace. **For service accounts:** ``` effective_permissions(service_account, context) = permissions_from_scoped_assignments(service_account, context.scope) ``` Service accounts have no org membership baseline. Their permissions come entirely from role assignments. **For personal access tokens:** ``` effective_permissions(token, context) = effective_permissions(token.person, context) ∩ token.scopes ``` When `scopes` is NULL, the PAT has the person's full permissions. When set, the intersection narrows the token's capabilities. --- ## Entitlements — `entitlements` schema Entitlements answers: "What capabilities are available, who provisioned them, and how are they distributed?" The resource pool is the bridge between the financial world (billing accounts, subscriptions, purchases) and the resource world (workspaces, entitlements, limits, quotas). It decouples "who pays" from "who uses" while maintaining a clear audit trail between them. ### resource_pools A **resource pool** aggregates entitlements from one or more provision sources. Workspaces draw capabilities from pools via pool assignments. Pools are the unit of resource sharing: multiple workspaces can share a pool (shared mode), or a pool can be dedicated to a single workspace. Pools are auto-created for simple cases and explicitly managed for complex ones: - When a billing account is created, a **default pool** is auto-created. - When a workspace is created, it is auto-assigned to its organization's default billing account's default pool. - Administrators can create additional pools for departmental isolation, client-funded projects, or cross-organization sponsorship. | Field | Type | Purpose | |-------|------|---------| | `pool_id` | UUID | Primary key | | `org_id` | UUID | FK → organization.organizations. The organization this pool belongs to (governance scope). | | `name` | VARCHAR(255) | Display name. Auto-generated for default pools. | | `slug` | VARCHAR(100) | URL-safe identifier. Unique within the organization. | | `pool_type` | VARCHAR(20) | `default` (auto-created with billing account), `shared` (explicit multi-workspace), `dedicated` (explicit single-workspace). | | `is_auto_managed` | BOOLEAN | If true, system manages this pool automatically. Default pools are auto-managed. | | `description` | TEXT | What this pool is for. | | `status` | VARCHAR(20) | `active`, `suspended`, `archived`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `archived_at` | TIMESTAMPTZ | When archived. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - `slug` is unique within an organization. **Relationships:** - `resource_pools` (0..*) → (1) `organization.organizations`: Pools belong to an organization (governance scope). - `resource_pools` (1) → (0..*) `pool_provisions`: Pools receive provisions from billing sources. - `resource_pools` (1) → (0..*) `pool_assignments`: Pools are assigned to workspaces. - `resource_pools` (1) → (0..*) `boolean_entitlements`: Pools hold binary capabilities. - `resource_pools` (1) → (0..*) `numeric_entitlements`: Pools hold numeric limits and quotas. - `resource_pools` (1) → (0..*) `pool_ondemand_config`: Pools may have on-demand usage configuration. --- ### pool_provisions A **pool_provision** records that a resource pool has been provisioned with capabilities from a specific source. The source may be a subscription (recurring agreement), a purchase (one-time transaction), or a grant (administrative/promotional access). Pool provisions are the uniform interface through which all access mechanisms connect to resource pools. The materialization pipeline queries provisions to determine what capabilities a pool should have. It does not need to know whether the provision came from a subscription, purchase, or grant. | Field | Type | Purpose | |-------|------|---------| | `provision_id` | UUID | Primary key | | `pool_id` | UUID | FK → resource_pools. The pool being provisioned. | | `billing_account_id` | UUID | FK → billing.billing_accounts. The financial source. NULL for grants that target an org or person directly. | | `subscription_id` | UUID | FK → billing.subscriptions. If this provision comes from a subscription. NULL otherwise. | | `purchase_id` | UUID | FK → billing.purchases. If this provision comes from a purchase. NULL otherwise. | | `grant_id` | UUID | FK → grants. If this provision comes from a grant. NULL otherwise. | | `entitlement_set_id` | UUID | FK → entitlement_sets. Which entitlement set's rules apply. Resolved at provision-creation time. | | `quantity` | INTEGER | For per-unit sources: how many units. Determines resource value multipliers via entitlement_set_rules. | | `status` | VARCHAR(20) | `active`, `suspended`, `ended`. Derived from source status. | | `activated_at` | TIMESTAMPTZ | When this provision became active. | | `suspended_at` | TIMESTAMPTZ | When suspended, if applicable. | | `ended_at` | TIMESTAMPTZ | When ended, if applicable. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints (exclusive arc — source):** ```sql CHECK ( (subscription_id IS NOT NULL AND purchase_id IS NULL AND grant_id IS NULL) OR (subscription_id IS NULL AND purchase_id IS NOT NULL AND grant_id IS NULL) OR (subscription_id IS NULL AND purchase_id IS NULL AND grant_id IS NOT NULL) ) ``` Exactly one of `subscription_id`, `purchase_id`, or `grant_id` must be set. **Note:** `billing_account_id` is not constrained to the same `org_id` as the pool. This enables cross-organization provisioning. **How each source type creates provisions:** | Source | Trigger | Pool Provision Status | |--------|---------|----------------------| | Subscription (active/trialing) | Subscription item activated | `active` | | Subscription (past_due) | Payment failed, grace period | `active` (configurable) | | Subscription (unpaid/paused) | Retries exhausted or paused | `suspended` | | Subscription (canceled) | Agreement terminated | `ended` | | Purchase (completed) | Transaction completed | `active` (indefinitely) | | Purchase (refunded) | Full refund issued | `ended` | | Grant (active) | Grant valid_from reached | `active` | | Grant (expired) | Grant valid_until reached | `ended` | | Grant (revoked) | Admin revocation | `ended` | **Multi-item subscriptions:** A subscription with three items creates three pool_provision records — one per item. Each references a different product and activates that product's entitlement rules on the pool. **Relationships:** - `pool_provisions` (0..*) → (1) `resource_pools`: Provisions target a pool. - `pool_provisions` (0..*) → (0..1) `billing.billing_accounts`: Provisions may have a financial source. - Exclusive arc — source (`subscription_id` / `purchase_id` / `grant_id`): - `pool_provisions` (0..*) → (0..1) `billing.subscriptions`: Provisions may come from a subscription. - `pool_provisions` (0..*) → (0..1) `billing.purchases`: Provisions may come from a purchase. - `pool_provisions` (0..*) → (0..1) `grants`: Provisions may come from a grant. - `pool_provisions` (0..*) → (1) `billing.products`: Provisions reference a product for entitlement rule lookup. - `pool_provisions` (1) → (0..*) `pool_provision_ladders`: Active plan-type provisions record their ladder positions. `FK ← pool_provision_ladders.provision_id` --- ### pool_provision_ladders A **pool_provision_ladder** row records that an active pool provision currently occupies a position on a plan ladder. This junction carries the *catalog-shape fact* (this provision occupies this ladder rung) separately from the *commercial fact* (the provision itself), honoring the four-orthogonal-concerns factoring established in Doc 32 §6.1. One provision may produce multiple junction rows when the underlying product belongs to multiple ladders (the bundle case). The GiST exclusion constraint is the primary enforcement mechanism for mutual exclusivity: at any moment, at most one `active` provision per pool may occupy each ladder position. Trial state is carried on the source subscription (`billing.subscriptions.status = 'trialing'`), not on this junction — from the ladder's perspective, a trialing tier is an occupied tier (Doc 34). The denormalized columns (`pool_id`, `status`, `activated_at`, `ended_at`) are kept in sync with their authoritative source in `pool_provisions` by an AFTER UPDATE trigger on `pool_provisions`. The trigger shape: ```sql -- Trigger function (illustrative): -- On UPDATE to pool_provisions, propagate changes to pool_id, status, -- activated_at, and ended_at into all associated pool_provision_ladders rows. CREATE OR REPLACE FUNCTION entitlements.sync_pool_provision_ladders() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN UPDATE entitlements.pool_provision_ladders SET pool_id = NEW.pool_id, status = NEW.status, activated_at = NEW.activated_at, ended_at = NEW.ended_at WHERE provision_id = NEW.provision_id; RETURN NEW; END; $$; CREATE TRIGGER pool_provisions_sync_ladders AFTER UPDATE OF pool_id, status, activated_at, ended_at ON entitlements.pool_provisions FOR EACH ROW EXECUTE FUNCTION entitlements.sync_pool_provision_ladders(); ``` ```sql CREATE TABLE entitlements.pool_provision_ladders ( provision_id UUID NOT NULL REFERENCES entitlements.pool_provisions(provision_id) ON DELETE CASCADE, plan_ladder_id UUID NOT NULL REFERENCES billing.plan_ladders(plan_ladder_id), -- Denormalized from pool_provisions; kept in sync by trigger: pool_id UUID NOT NULL, status VARCHAR(20) NOT NULL, activated_at TIMESTAMPTZ NOT NULL, ended_at TIMESTAMPTZ, PRIMARY KEY (provision_id, plan_ladder_id), CONSTRAINT one_active_provision_per_pool_per_ladder EXCLUDE USING gist ( pool_id WITH =, plan_ladder_id WITH =, tstzrange(activated_at, ended_at, '[)') WITH && ) WHERE (status = 'active') ); CREATE INDEX pool_provision_ladders_pool_idx ON entitlements.pool_provision_ladders (pool_id); ``` | Field | Type | Purpose | |-------|------|---------| | `provision_id` | UUID | FK → pool_provisions. The provision occupying this ladder position. | | `plan_ladder_id` | UUID | FK → billing.plan_ladders. Which ladder is occupied. | | `pool_id` | UUID | Denormalized from pool_provisions. Used by the exclusion constraint. | | `status` | VARCHAR(20) | Denormalized from pool_provisions: `active` or `ended`. Trial state is not extended here; it is carried on the source subscription (Doc 34). | | `activated_at` | TIMESTAMPTZ | Denormalized from pool_provisions. Left bound of the exclusion window. | | `ended_at` | TIMESTAMPTZ | Denormalized from pool_provisions. Right bound (open). NULL for currently active rows. | **Constraints:** - `PRIMARY KEY (provision_id, plan_ladder_id)` — a provision occupies each ladder at most once. - `one_active_provision_per_pool_per_ladder` — GiST exclusion constraint ensures no two `active` provisions for the same pool overlap on the same ladder within the same time range. **Relationships:** - `pool_provision_ladders` (0..*) → (1) `pool_provisions`: Junction rows are children of a provision. - `pool_provision_ladders` (0..*) → (1) `billing.plan_ladders`: Junction rows reference a ladder. `FK → [billing] plan_ladders` --- ### pool_assignments A **pool_assignment** links a workspace to a resource pool. Workspaces draw their capabilities — boolean entitlements, numeric entitlements, and on-demand access — from their assigned pools. Each workspace has exactly one **primary** pool assignment. Secondary assignments are optional and provide additional capabilities. | Field | Type | Purpose | |-------|------|---------| | `assignment_id` | UUID | Primary key | | `pool_id` | UUID | FK → resource_pools. | | `workspace_id` | UUID | FK → organization.workspaces. | | `is_primary` | BOOLEAN | Whether this is the workspace's primary pool. | | `status` | VARCHAR(20) | `active`, `suspended`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - Each workspace has exactly one primary pool assignment (`is_primary` = true). - The combination of `pool_id` and `workspace_id` is unique. **Entitlement resolution across multiple pools:** - **Boolean entitlements:** Union semantics. If any assigned pool grants a capability, the workspace has it. - **Numeric entitlements (limits and quotas):** Primary pool is consumed first. If exhausted, secondary pools are checked in assignment order. - **On-demand usage:** Routed through the primary pool's on-demand configuration, with fallback to secondary pools. **Relationships:** - `pool_assignments` (1..*) → (1) `organization.workspaces`: Every workspace has at least one pool assignment. - `pool_assignments` (0..*) → (1) `resource_pools`: Assignments reference a pool. --- ### pool_ondemand_config A **pool_ondemand_config** defines on-demand resource pricing available through a pool. Unlike entitlements (which represent definite grants), on-demand resources are consumed without pre-commitment and billed after the fact. On-demand configuration is a billing-module concern: it defines pricing agreements, not capability grants. When a pool has both a numeric entitlement (quota) for a resource and an on-demand config for the same resource key, the quota is consumed first and the on-demand config handles overage. When a pool has only on-demand config and no entitlement, access is purely post-paid. | Field | Type | Purpose | |-------|------|---------| | `config_id` | UUID | Primary key | | `pool_id` | UUID | FK → resource_pools. | | `resource_key` | VARCHAR(100) | FK → resource_keys. Resource identifier (e.g., `api_calls`, `compute_minutes`, `bandwidth_bytes`). Shared namespace with entitlement resource keys (Decision 103). | | `provision_id` | UUID | FK → pool_provisions. Which provision authorized this on-demand configuration. NULL for manually configured entries. | | `billing_account_id` | UUID | FK → billing.billing_accounts. Who pays for on-demand consumption. | | `rate_plan_id` | UUID | FK → billing.prices. What rate applies to this on-demand resource. | | `soft_limit` | BIGINT | Optional spending alert threshold (smallest currency unit). NULL = no alert. | | `hard_limit` | BIGINT | Optional spending cap (smallest currency unit). NULL = unlimited. | | `status` | VARCHAR(20) | `active`, `suspended`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `pool_ondemand_config` (0..*) → (1) `resource_pools`: Config belongs to a pool. - `pool_ondemand_config` (0..*) → (0..1) `pool_provisions`: Config may be lifecycle-coupled to a provision. - `pool_ondemand_config` (0..*) → (1) `billing.billing_accounts`: Config designates a payer. - `pool_ondemand_config` (0..*) → (1) `billing.prices`: Config references a rate plan. - `pool_ondemand_config` (0..*) → (1) `resource_keys`: Resource key references the shared resource namespace. --- ### boolean_entitlements A **boolean entitlement** represents a binary capability that a resource pool has. Boolean entitlements are materialized from `rule_type = 'boolean'` product entitlement rules when pool provisions are activated. Toggle history (when `is_enabled` changes) is tracked exclusively through the audit log. | Field | Type | Purpose | |-------|------|---------| | `entitlement_id` | UUID | Primary key | | `pool_id` | UUID | FK → resource_pools. The pool that has this entitlement. | | `resource_key` | VARCHAR(100) | FK → resource_keys. Machine-readable feature identifier (e.g., `sites`, `custom_domains`, `api_access`). | | `is_enabled` | BOOLEAN | Whether this entitlement is currently active. | | `source_provision_id` | UUID | FK → pool_provisions. Which provision granted this entitlement. | | `valid_from` | TIMESTAMPTZ | When this entitlement became active. | | `valid_until` | TIMESTAMPTZ | When this entitlement expires. NULL means valid until provision ends. | | `metadata` | JSONB | Additional entitlement attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **"Last funder standing" logic:** When a provision ends, the system checks whether any other active provision on the same pool grants the same boolean entitlement. If yes, the entitlement remains active. If no, it is deactivated. **Relationships:** - `boolean_entitlements` (0..*) → (1) `resource_pools`: Boolean entitlements belong to a pool. - `boolean_entitlements` (0..*) → (1) `pool_provisions`: Boolean entitlements are granted by a provision. - `boolean_entitlements` (0..*) → (1) `resource_keys`: Resource key references the shared resource namespace. --- ### numeric_entitlements A **numeric entitlement** represents a valued capability on a resource pool — either a static allocation (limit) or a renewable consumption budget (quota). The `numeric_entitlements` table stores the definition (what the limit is); mutable usage state is separated into `numeric_entitlement_usage`. Provenance — which provisions contribute to the effective limit — is tracked in `numeric_entitlement_contributions`. | Field | Type | Purpose | |-------|------|---------| | `entitlement_id` | UUID | Primary key | | `pool_id` | UUID | FK → resource_pools. The pool this entitlement belongs to. | | `entitlement_type` | VARCHAR(20) | `limit` (static allocation, no reset) or `quota` (renewable consumption rights, has reset period). | | `resource_key` | VARCHAR(100) | FK → resource_keys. Machine-readable identifier (e.g., `workspaces`, `storage_bytes`, `api_calls`). | | `resource_limit` | BIGINT | Effective limit. -1 means unlimited. Denormalized; recomputed from `numeric_entitlement_contributions` by the materialization pipeline. | | `reset_period` | VARCHAR(20) | For quotas: `daily`, `monthly`, `yearly`. NULL for limits. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `numeric_entitlements` (0..*) → (1) `resource_pools`: Numeric entitlements belong to a pool. - `numeric_entitlements` (1) → (0..*) `numeric_entitlement_contributions`: Numeric entitlements receive contributions from provisions. - `numeric_entitlements` (1) → (1) `numeric_entitlement_usage`: Each numeric entitlement has a corresponding usage state record. - `numeric_entitlements` (0..*) → (1) `resource_keys`: Resource key references the shared resource namespace. --- ### numeric_entitlement_contributions A **numeric entitlement contribution** records a single provision's contribution to a numeric entitlement's effective limit. When a pool has multiple active provisions that grant the same resource, each provision creates a separate contribution. The entitlement's `resource_limit` is derived from its contributions using the applicable stacking policy. | Field | Type | Purpose | |-------|------|---------| | `contribution_id` | UUID | Primary key | | `entitlement_id` | UUID | FK → numeric_entitlements. The entitlement receiving this contribution. | | `provision_id` | UUID | FK → pool_provisions. The provision contributing. | | `contributed_value` | BIGINT | The value this provision contributes. For `resource_per_unit` rules, this is `resource_value × provision.quantity`. | | `stacking_policy` | VARCHAR(20) | The stacking policy that applies to this contribution. Denormalized from the rule at materialization time. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - The combination of `entitlement_id` and `provision_id` is unique. - Index on `entitlement_id` for the aggregation query. **Effective limit computation:** For `additive` stacking, the effective limit is `SUM(contributed_value)`. For `maximum`, it is `MAX(contributed_value)`. For `replace`, it is the contribution from the most recently activated provision. **Relationships:** - `numeric_entitlement_contributions` (0..*) → (1) `numeric_entitlements`: Contributions target a numeric entitlement. - `numeric_entitlement_contributions` (0..*) → (1) `pool_provisions`: Contributions originate from a provision. --- ### numeric_entitlement_usage A **numeric entitlement usage** record holds the mutable consumption state for a numeric entitlement, separated from the entitlement definition to eliminate write contention between the materialization pipeline (which updates definitions) and the usage increment path (which updates consumption). | Field | Type | Purpose | |-------|------|---------| | `usage_id` | UUID | Primary key | | `entitlement_id` | UUID | FK → numeric_entitlements. The entitlement definition this usage tracks. | | `pool_id` | UUID | FK → resource_pools. Denormalized from entitlement for indexed lookups. | | `resource_key` | VARCHAR(100) | FK → resource_keys. Denormalized from entitlement for indexed lookups without join. | | `current_usage` | BIGINT | Current usage against this entitlement in the current period. | | `current_period_start` | TIMESTAMPTZ | Start of current reset period. NULL for limits (non-resetting). | | `current_period_end` | TIMESTAMPTZ | End of current reset period. NULL for limits (non-resetting). | | `last_reset_at` | TIMESTAMPTZ | When usage was last reset. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - `entitlement_id` is unique (1:1 with numeric_entitlements, in a separate table for write isolation). - Composite index on `(pool_id, resource_key)` for the hot-path lookup. **Usage increment:** `UPDATE numeric_entitlement_usage SET current_usage = current_usage + $increment WHERE entitlement_id = $id AND current_usage + $increment <= (SELECT resource_limit FROM numeric_entitlements WHERE entitlement_id = $id)`. If no rows are returned, the limit would be exceeded. **Relationships:** - `numeric_entitlement_usage` (1) → (1) `numeric_entitlements`: Each usage record tracks one numeric entitlement. --- ### usage_events A **usage_event** records consumption of a resource by a workspace. Usage events are append-only observational data — they record *what happened*, not *what should be charged*. All consumption paths — quota decrements, credit deductions, and on-demand charges — generate usage events. Billing attribution flows through `pending_charge_usage_events` → `pending_charges` → `invoice_line_items` → `invoices`, preserving the append-only semantics of the events themselves. | Field | Type | Purpose | |-------|------|---------| | `event_id` | UUID | Primary key | | `workspace_id` | UUID | FK → organization.workspaces. Where consumption occurred. | | `pool_id` | UUID | FK → resource_pools. Which pool this was routed through. | | `resource_key` | VARCHAR(100) | FK → resource_keys. What was consumed (e.g., `api_calls`, `compute_minutes`). | | `quantity` | BIGINT | How much was consumed (in the resource's unit). | | `unit` | VARCHAR(50) | Unit of measurement (e.g., `call`, `second`, `byte`). | | `event_timestamp` | TIMESTAMPTZ | When consumption occurred. | | `resolution_path` | VARCHAR(20) | How this consumption was resolved: `quota`, `credit`, or `on_demand`. | | `billing_account_id` | UUID | FK → billing.billing_accounts. Resolved payer at time of event. NULL for quota-resolved events with no direct billing association. | | `metadata` | JSONB | Additional event attributes. | | `created_at` | TIMESTAMPTZ | | **Relationships:** - `usage_events` (0..*) → (1) `organization.workspaces`: Events occur in a workspace. - `usage_events` (0..*) → (1) `resource_pools`: Events are routed through a pool. - `usage_events` (0..*) → (0..1) `billing.billing_accounts`: Events may have a resolved payer (NULL for quota-resolved events). - `usage_events` (0..*) → (0..*) `billing.pending_charge_usage_events`: Events may be linked to the pending charges they contributed to. - `usage_events` (0..*) → (0..*) `billing.credit_transactions`: Events may be linked to real-time credit deductions (prepaid topology). - `usage_events` (0..*) → (1) `resource_keys`: Resource key references the shared resource namespace. --- ### resource_keys A **resource key** is a canonical identifier for a grantable or consumable resource. The `resource_keys` table is the shared namespace that both the entitlement system and the on-demand billing system reference. By making both sides FK into this table, a namespace mismatch (e.g., `api-calls` vs. `api_calls`) produces an FK violation instead of a silent failure in the overage bridge. | Field | Type | Purpose | |-------|------|---------| | `resource_key` | VARCHAR(100) | Primary key. Machine-readable identifier (e.g., `api_calls`, `custom_domains`, `storage_bytes`). | | `display_name` | VARCHAR(255) | Human-readable name. | | `description` | TEXT | What this resource represents. | | `unit` | VARCHAR(50) | Unit of measurement (e.g., `call`, `byte`, `seat`). NULL for boolean resources. | | `created_at` | TIMESTAMPTZ | | **Relationships:** - `resource_keys` (1) → (0..*) `entitlement_set_rules`: Rules reference resources. - `resource_keys` (1) → (0..*) `boolean_entitlements`: Boolean entitlements reference resources. - `resource_keys` (1) → (0..*) `numeric_entitlements`: Numeric entitlements reference resources. - `resource_keys` (1) → (0..*) `numeric_entitlement_usage`: Usage records reference resources (denormalized). - `resource_keys` (1) → (0..*) `pool_ondemand_config`: On-demand configs reference resources. - `resource_keys` (1) → (0..*) `usage_events`: Usage events reference resources. --- ### grants A **grant** represents administrative, promotional, or exceptional access that is not backed by a commercial transaction. No money changes hands. Access is conferred by an authorized person. Grants are the non-financial provisioning source in the resource pool system — they answer "what does this entity have?" rather than "who pays?" (Decision 106). | Field | Type | Purpose | |-------|------|---------| | `grant_id` | UUID | Primary key | | `entitlement_set_id` | UUID | FK → entitlement_sets. The entitlement set this grant confers directly. NULL if `product_id` is set. | | `product_id` | UUID | FK → billing.products. Optional. When a grant confers a specific commercial product for audit clarity. NULL if `entitlement_set_id` is set directly. | | `granted_to_billing_account_id` | UUID | FK → billing.billing_accounts. If the recipient is a billing account. NULL otherwise. | | `granted_to_org_id` | UUID | FK → organization.organizations. If the recipient is an organization. NULL otherwise. | | `granted_to_person_id` | UUID | FK → identity.persons. If the recipient is a person. NULL otherwise. | | `granted_by_person_id` | UUID | FK → identity.persons. Who authorized this grant. | | `grant_reason` | VARCHAR(50) | Classification: `promotional`, `complimentary`, `legacy`, `sponsored`, `trial_extension`, `board_decision`, `other`. | | `description` | TEXT | Human-readable explanation. | | `valid_from` | TIMESTAMPTZ | When the grant becomes effective. | | `valid_until` | TIMESTAMPTZ | When the grant expires. NULL for indefinite. | | `status` | VARCHAR(20) | `active`, `expired`, `revoked`. | | `revoked_at` | TIMESTAMPTZ | When the grant was revoked, if applicable. | | `revoked_by_person_id` | UUID | FK → identity.persons. Who revoked it. | | `revocation_reason` | TEXT | Why it was revoked. | | `metadata` | JSONB | Additional grant attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints (exclusive arc — recipient):** ```sql CHECK ( (granted_to_billing_account_id IS NOT NULL AND granted_to_org_id IS NULL AND granted_to_person_id IS NULL) OR (granted_to_billing_account_id IS NULL AND granted_to_org_id IS NOT NULL AND granted_to_person_id IS NULL) OR (granted_to_billing_account_id IS NULL AND granted_to_org_id IS NULL AND granted_to_person_id IS NOT NULL) ) ``` Exactly one of `granted_to_billing_account_id`, `granted_to_org_id`, or `granted_to_person_id` must be set. **Relationships:** - `grants` (0..*) → (1) `billing.products`: FK → [billing] products. Grants confer a product's entitlements. - Exclusive arc — recipient (`granted_to_billing_account_id` / `granted_to_org_id` / `granted_to_person_id`): - `grants` (0..*) → (0..1) `billing.billing_accounts`: FK → [billing] billing_accounts. Grants may target a billing account. - `grants` (0..*) → (0..1) `organization.organizations`: FK → [organization] organizations. Grants may target an organization. - `grants` (0..*) → (0..1) `identity.persons` (via `granted_to_person_id`): FK → [identity] persons. Grants may target a person. - `grants` (0..*) → (1) `identity.persons` (via `granted_by_person_id`): FK → [identity] persons. Grants are authorized by a person. - `grants` (1) → (0..*) `pool_provisions`: Grants create pool provisions. --- ### entitlement_sets An **entitlement set** is a named, reusable collection of entitlement rules — the canonical unit of capability specification. Entitlement sets decouple "what capabilities are conferred" from "how they are sold" (products) and "why they were given" (grants). Products reference a set to declare what a purchase or subscription confers; grants may reference a set directly for ad hoc capability conferral. | Field | Type | Purpose | |-------|------|---------| | `set_id` | UUID | Primary key | | `name` | VARCHAR(255) | Internal name (e.g., "Pro Capabilities", "API Access"). | | `description` | TEXT | What this set represents. | | `is_active` | BOOLEAN | Whether this set can be referenced by new products or grants. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `entitlement_sets` (1) → (0..*) `entitlement_set_rules`: A set contains rules. - `entitlement_sets` (1) → (0..*) `billing.products`: Products reference a set. `FK ← [billing] products.entitlement_set_id` - `entitlement_sets` (1) → (0..*) `grants`: Grants may reference a set directly. - `entitlement_sets` (1) → (0..*) `pool_provisions`: Provisions always resolve to a set. --- ### entitlement_set_rules An **entitlement set rule** defines a single capability within an entitlement set. Formerly `product_entitlement_rules` (Decision 107); re-parented from `product_id` to `set_id`. This table uses single-table inheritance: the `rule_type` discriminator determines which set of fields is populated. Four rule types exist: `boolean` (binary capabilities), `limit` (static numeric allocations), `quota` (renewable consumption budgets), and `credit` (included prepaid credits). | Field | Type | Purpose | |-------|------|---------| | `rule_id` | UUID | Primary key | | `set_id` | UUID | FK → entitlement_sets. The set this rule belongs to. | | `rule_type` | VARCHAR(20) | `boolean`, `limit`, `quota`, or `credit`. | | `resource_key` | VARCHAR(100) | FK → resource_keys. For boolean, limit, and quota rules: the resource identifier. NULL for credit rules. | | `resource_value` | BIGINT | For limit and quota rules: the amount granted. -1 for unlimited. NULL for boolean and credit rules. | | `resource_per_unit` | BOOLEAN | For limit and quota rules: whether the value is multiplied by the provision's quantity. NULL for boolean and credit rules. | | `stacking_policy` | VARCHAR(20) | For limit and quota rules: how this entitlement combines with contributions from other provisions. `additive` (sum all contributions), `maximum` (take the highest), `replace` (most recent provision wins). NULL for boolean and credit rules. Default: `additive`. | | `reset_period` | VARCHAR(20) | For quota rules: `daily`, `monthly`, or `yearly`. Required for quota rules, NULL for all other rule types. | | `credit_amount` | INTEGER | For credit rules: the credit amount in smallest currency unit. NULL for all other rule types. | | `credit_currency` | VARCHAR(3) | For credit rules: the currency (ISO 4217). NULL for all other rule types. | | `description` | TEXT | Human-readable explanation of what this rule grants. | | `is_active` | BOOLEAN | Whether this rule is currently in effect. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** ```sql CHECK ( (rule_type = 'boolean' AND resource_key IS NOT NULL AND resource_value IS NULL AND stacking_policy IS NULL AND reset_period IS NULL AND resource_per_unit IS NULL AND credit_amount IS NULL AND credit_currency IS NULL) OR (rule_type = 'limit' AND resource_key IS NOT NULL AND resource_value IS NOT NULL AND reset_period IS NULL AND credit_amount IS NULL AND credit_currency IS NULL) OR (rule_type = 'quota' AND resource_key IS NOT NULL AND resource_value IS NOT NULL AND reset_period IS NOT NULL AND credit_amount IS NULL AND credit_currency IS NULL) OR (rule_type = 'credit' AND credit_amount IS NOT NULL AND credit_currency IS NOT NULL AND resource_key IS NULL AND resource_value IS NULL AND stacking_policy IS NULL AND reset_period IS NULL AND resource_per_unit IS NULL) ) ``` **Relationships:** - `entitlement_set_rules` (0..*) → (1) `entitlement_sets`: Rules belong to a set. (intra-module) - `entitlement_set_rules` (0..*) → (0..1) `resource_keys`: Boolean, limit, and quota rules reference a resource key. (intra-module) - Consumed by the materialization pipeline when pool provisions activate or deactivate. --- ## Billing & Value — `billing` schema Billing & Value answers: "Who pays, what do they pay for?" ### billing_accounts A **billing_account** is the entity that pays. It holds payment method references, receives invoices, and accumulates patronage. A billing account is separate from the organization that owns it. This separation matters because: - An organization may have multiple billing accounts (e.g., for different cost centers, clients, or funding sources). - Billing contacts and addresses may differ from organization contacts. - The payer (billing account) is the patron for cooperative purposes, which may differ from organizational structure. - A billing account can provision pools in other organizations (the GCP orthogonality principle). | Field | Type | Purpose | |-------|------|---------| | `billing_account_id` | UUID | Primary key | | `org_id` | UUID | FK → organization.organizations. The organization that owns this billing account. | | `name` | VARCHAR(255) | Display name (e.g., "Primary", "Marketing Budget", "Client: Acme"). | | `is_default` | BOOLEAN | Whether this is the default billing account for the organization. Each org has exactly one default. | | `billing_email` | VARCHAR(255) | Where to send invoices. May differ from user or org email. | | `billing_name` | VARCHAR(255) | Name to appear on invoices. | | `billing_address_line1` | VARCHAR(255) | Billing street address. | | `billing_address_line2` | VARCHAR(255) | Billing address line 2. | | `billing_city` | VARCHAR(100) | Billing city. | | `billing_state` | VARCHAR(100) | Billing state/province. | | `billing_postal_code` | VARCHAR(20) | Billing postal code. | | `billing_country` | VARCHAR(2) | Billing country (ISO 3166-1 alpha-2). | | `tax_id` | VARCHAR(100) | Tax identifier for this billing entity (for invoices, receipts). | | `tax_exempt` | BOOLEAN | Whether this billing account is tax exempt. | | `tax_exempt_reason` | VARCHAR(255) | Reason for tax exemption if applicable. | | `currency` | VARCHAR(3) | Preferred currency for billing (ISO 4217). | | `platform_billing_mode` | VARCHAR(20) | Platform-enforced billing mode. `postpaid`, `prepaid_only`, or `hybrid`. Default: `hybrid`. | | `customer_billing_mode` | VARCHAR(20) | Customer-selected billing mode preference. `prepaid_only` or `hybrid`. Default: `hybrid`. | | `auto_recharge_enabled` | BOOLEAN DEFAULT FALSE | Whether auto-recharge of credit balance is active. | | `auto_recharge_threshold` | INTEGER | Credit balance below which a recharge is triggered (smallest currency unit). | | `auto_recharge_amount` | INTEGER | Amount to purchase when auto-recharge triggers (smallest currency unit). | | `settings` | JSONB | Billing preferences and configuration. | | `status` | VARCHAR(20) | State: `active`, `suspended`, `closed`. | | `suspended_at` | TIMESTAMPTZ | When most recently suspended. | | `suspended_by` | UUID | FK → identity.persons. Who suspended. | | `closed_at` | TIMESTAMPTZ | When closed. | | `closed_by` | UUID | FK → identity.persons. Who closed. | | `created_at` | TIMESTAMPTZ | When this billing account was created. | | `updated_at` | TIMESTAMPTZ | Last modification timestamp. | **Constraints:** - Each organization has exactly one default billing account (`is_default` = TRUE). **Relationships:** - `billing_accounts` (0..*) → (1) `organization.organizations`: Billing accounts belong to an organization. - `billing_accounts` (1) → (0..*) `subscriptions`: Billing accounts have subscriptions. - `billing_accounts` (1) → (0..*) `purchases`: Billing accounts make purchases. - `billing_accounts` (1) → (0..*) `invoices`: Billing accounts receive invoices. - `billing_accounts` (1) → (0..*) `payments`: Billing accounts make payments. - `billing_accounts` (1) → (0..*) `entitlements.pool_provisions`: Billing accounts may fund pool provisions. - `billing_accounts` (1) → (0..*) `entitlements.pool_ondemand_config`: Billing accounts may be on-demand usage payers. - `billing_accounts` (1) → (0..*) `credit_grants`: Billing accounts may hold prepaid credit grants. - `billing_accounts` (1) → (0..*) `payment_methods`: Billing accounts have payment methods on file. - `billing_accounts` (1) → (0..*) `pending_charges`: Billing accounts accumulate pending charges. --- ### products A **product** represents something the cooperative sells. It is the commercial instrument — name, description, type, visibility, and pricing — that wraps an entitlement set. The product defines how capabilities are packaged and sold; the entitlement set defines what capabilities are conferred. | Field | Type | Purpose | |-------|------|---------| | `product_id` | UUID | Primary key | | `entitlement_set_id` | UUID | FK → entitlements.entitlement_sets. The entitlement set this product confers. | | `name` | VARCHAR(255) | Display name (e.g., "Pro Plan", "Premium Support", "Custom Domain"). | | `description` | TEXT | Marketing/explanatory description. | | `product_type` | VARCHAR(50) | Classification for non-plan kinds: `addon` (supplementary capability), `usage` (metered resource), `one_time` (single purchase). The `'plan'` kind is not represented here; it is derived structurally from the presence of rows in `plan_ladder_tiers`. See the `product_kinds` view below and Doc 31 Amendment #3. | | `lifecycle_status` | VARCHAR(20) | One of `draft`, `published`, `retired`. Orthogonal to kind: carries the editorial-intent axis (is this product definition complete and offered?) separately from the structural-kind axis (what role does the product play in the catalog?). Defaults to `draft`. | | `metadata` | JSONB | Additional product attributes. | | `is_active` | BOOLEAN | Whether this product can be purchased. | | `is_public` | BOOLEAN | Whether this product appears on public pricing pages. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** ```sql ALTER TABLE billing.products ADD CONSTRAINT product_type_non_plan_domain CHECK (product_type IN ('addon', 'usage', 'one_time')), ADD CONSTRAINT lifecycle_status_domain CHECK (lifecycle_status IN ('draft', 'published', 'retired')); ``` The `'plan'` value is deliberately absent from the `product_type` domain. A product's status as a plan is a structural fact — the presence of rows in `plan_ladder_tiers` — not a label the product declares. The earlier bidirectional `product_type_plan_iff_ladder_member` CHECK introduced by Doc 31 Amendment #1 is retired under Amendment #3; the denormalization it policed has been dissolved. **Derived kind:** ```sql CREATE VIEW billing.product_kinds AS SELECT p.product_id, CASE WHEN EXISTS ( SELECT 1 FROM billing.plan_ladder_tiers t WHERE t.product_id = p.product_id ) THEN 'plan' WHEN p.product_type = 'addon' THEN 'addon' WHEN p.product_type = 'usage' THEN 'usage' WHEN p.product_type = 'one_time' THEN 'one_time' END AS product_kind FROM billing.products p; ``` The view is the authoritative articulation of the kind taxonomy. The `'plan'` case is derived structurally; the non-plan cases delegate to `product_type` as provisional sources of truth pending future amendments that identify their distinguishing structural facts. A new kind introduced without a corresponding structural fact is, by this discipline, not yet a kind — merely a label awaiting a relation to anchor it. **Relationships:** - `products` (0..*) → (1) `entitlements.entitlement_sets`: Products reference an entitlement set. `FK → [entitlements] entitlement_sets` - `products` (0..*) → (0..*) `plan_ladders` (via `plan_ladder_tiers`): Plan products occupy one or more ladders through the junction; the junction is the sole representation of ladder membership for all topologies. - `products` (1) → (0..*) `prices`: Products have one or more prices. - `products` (1) → (0..*) `entitlements.grants`: Grants may optionally reference a product. `FK ← [entitlements] grants.product_id` --- ### prices A **price** defines the commercial terms for purchasing a product: how much, in what currency, on what schedule, with what trial period. | Field | Type | Purpose | |-------|------|---------| | `price_id` | UUID | Primary key | | `product_id` | UUID | FK → products. The product this price is for. | | `nickname` | VARCHAR(255) | Display name (e.g., "Monthly", "Annual", "Starter"). | | `currency` | VARCHAR(3) | Currency code (ISO 4217). | | `unit_amount` | INTEGER | Price in smallest currency unit (e.g., cents for USD). | | `billing_scheme` | VARCHAR(20) | How pricing works: `flat` (fixed amount), `per_unit` (multiplied by quantity), `tiered` (volume brackets). | | `recurring_interval` | VARCHAR(20) | Billing frequency: `month`, `year`, or NULL for one-time prices. | | `recurring_interval_count` | INTEGER | Number of intervals between billings. | | `trial_period_days` | INTEGER | Number of days in trial period. NULL for no trial. Trial specification is placed at the price layer by deliberate architectural choice: a ladder declares which products are alternatives, not which alternatives are free for N days; that concession belongs to the pricing instrument and the subscription that references it (Doc 34). | | `usage_type` | VARCHAR(20) | For metered products: `metered` or `licensed`. NULL for non-usage pricing. | | `metadata` | JSONB | Additional price attributes. | | `is_active` | BOOLEAN | Whether this price can be used for new transactions. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `prices` (0..*) → (1) `products`: Prices belong to a product. - `prices` (1) → (0..*) `subscription_items`: Prices are referenced by subscription line items. - `prices` (1) → (0..*) `purchases`: Prices are referenced by purchases. - `prices` (1) → (0..*) `entitlements.pool_ondemand_config`: Prices may serve as on-demand rate plans. --- ### subscriptions A **subscription** is a stateful binding agreement between a billing account and one or more priced products, with a recurring billing lifecycle. It is an invoice generator: at each billing cycle, it produces an invoice. Its status determines whether the pool provisions it creates remain active. The `subscription_changes` table provides complete transition history; `paused_at` and `resumed_at` capture the most recent occurrence for hot-path queries. | Field | Type | Purpose | |-------|------|---------| | `subscription_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing_accounts. The entity paying. | | `current_period_start` | TIMESTAMPTZ | Start of current billing period. | | `current_period_end` | TIMESTAMPTZ | End of current billing period. | | `trial_start` | TIMESTAMPTZ | When trial started, if applicable. | | `trial_end` | TIMESTAMPTZ | When trial ends, if applicable. | | `cancel_at_period_end` | BOOLEAN | Whether to cancel at end of current period. | | `cancel_at` | TIMESTAMPTZ | Scheduled cancellation time. | | `canceled_at` | TIMESTAMPTZ | When cancellation was requested. | | `cancellation_reason` | TEXT | Why the subscription was canceled. | | `ended_at` | TIMESTAMPTZ | When the subscription actually ended. | | `paused_at` | TIMESTAMPTZ | When most recently paused. | | `resumed_at` | TIMESTAMPTZ | When most recently resumed. | | `status` | VARCHAR(20) | State machine: `incomplete`, `trialing`, `active`, `past_due`, `unpaid`, `paused`, `canceled`. | | `metadata` | JSONB | Additional subscription attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `subscriptions` (0..*) → (1) `billing_accounts`: Subscriptions belong to a billing account. - `subscriptions` (1) → (1..*) `subscription_items`: Subscriptions contain one or more items. - `subscriptions` (1) → (0..*) `subscription_changes`: Changes are tracked. - `subscriptions` (1) → (0..*) `invoices`: Subscriptions generate invoices. - `subscriptions` (1) → (0..*) `entitlements.pool_provisions`: Subscriptions create pool provisions (one per item). --- ### subscription_items A **subscription_item** is a line item within a subscription. Each item binds the subscription to a specific price (and by implication, a product) at a specific quantity. | Field | Type | Purpose | |-------|------|---------| | `item_id` | UUID | Primary key | | `subscription_id` | UUID | FK → subscriptions. | | `price_id` | UUID | FK → prices. The price (and thus product) for this line item. | | `quantity` | INTEGER | Number of units. | | `metadata` | JSONB | Additional item attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `subscription_items` (1..*) → (1) `subscriptions`: Items belong to a subscription. - `subscription_items` (0..*) → (1) `prices`: Items reference a price. --- ### subscription_changes A **subscription_change** records a modification to a subscription. This provides a complete audit trail of subscription lifecycle events with timestamps and actor attribution. | Field | Type | Purpose | |-------|------|---------| | `change_id` | UUID | Primary key | | `subscription_id` | UUID | FK → subscriptions. The subscription that changed. | | `change_type` | VARCHAR(50) | What kind of change: `created`, `item_added`, `item_removed`, `item_quantity_changed`, `renewed`, `trial_started`, `trial_ended`, `paused`, `resumed`, `canceled`, `reactivated`, `ended`. | | `item_id` | UUID | FK → subscription_items. Which item changed, if applicable. | | `previous_price_id` | UUID | Price before the change. | | `previous_quantity` | INTEGER | Quantity before the change. | | `previous_status` | VARCHAR(20) | Status before the change. | | `new_price_id` | UUID | Price after the change. | | `new_quantity` | INTEGER | Quantity after the change. | | `new_status` | VARCHAR(20) | Status after the change. | | `changed_by_person_id` | UUID | FK → identity.persons. Who made the change (NULL if system/automated). | | `reason` | TEXT | Explanation for the change. | | `effective_at` | TIMESTAMPTZ | When the change took/takes effect. | | `created_at` | TIMESTAMPTZ | | **Relationships:** - `subscription_changes` (0..*) → (1) `subscriptions`: Changes belong to a subscription. - `subscription_changes` (0..*) → (0..1) `subscription_items`: Changes may reference a specific item. - `subscription_changes` (0..*) → (0..1) `identity.persons`: Changes may be attributed to a person. --- ### purchases A **purchase** represents a one-time transaction: a billing account pays for a product and receives access. There is no recurring billing, no state machine. It is a completed exchange. | Field | Type | Purpose | |-------|------|---------| | `purchase_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing_accounts. Who paid. | | `price_id` | UUID | FK → prices. What they paid (must be a one-time price). | | `quantity` | INTEGER | Number of units purchased. | | `amount` | INTEGER | Total amount charged (smallest currency unit). | | `currency` | VARCHAR(3) | Currency of the transaction. | | `purchased_at` | TIMESTAMPTZ | When the purchase was completed. | | `status` | VARCHAR(20) | `completed`, `refunded`, `partially_refunded`. | | `refunded_at` | TIMESTAMPTZ | When refunded. | | `invoice_id` | UUID | FK → invoices. The invoice generated for this purchase. | | `metadata` | JSONB | Additional purchase attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `purchases` (0..*) → (1) `billing_accounts`: Purchases belong to a billing account. - `purchases` (0..*) → (1) `prices`: Purchases reference a price. - `purchases` (0..*) → (0..1) `invoices`: Purchases may generate an invoice. - `purchases` (1) → (0..*) `entitlements.pool_provisions`: Purchases create pool provisions. --- ### invoices An **invoice** is a billing document requesting payment from a billing account. Invoices may be generated from subscriptions (periodically), purchases (once), or metered usage (aggregated). Invoice address fields snapshot billing identity at issuance time — they record what appeared on the invoice regardless of subsequent billing account changes. | Field | Type | Purpose | |-------|------|---------| | `invoice_id` | UUID | Primary key | | `invoice_number` | VARCHAR(50) | Human-readable unique identifier. | | `billing_account_id` | UUID | FK → billing_accounts. The billing account being invoiced. | | `subscription_id` | UUID | FK → subscriptions. A subscription this invoice is for, if applicable. | | `purchase_id` | UUID | FK → purchases. A purchase this invoice is for, if applicable. | | `billing_name` | VARCHAR(255) | Snapshot of billing name at invoice time. | | `billing_email` | VARCHAR(255) | Snapshot of billing email at invoice time. | | `billing_address_line1` | VARCHAR(255) | Snapshot of billing street address at invoice time. | | `billing_address_line2` | VARCHAR(255) | Snapshot of billing address line 2 at invoice time. | | `billing_city` | VARCHAR(100) | Snapshot of billing city at invoice time. | | `billing_state` | VARCHAR(100) | Snapshot of billing state/province at invoice time. | | `billing_postal_code` | VARCHAR(20) | Snapshot of billing postal code at invoice time. | | `billing_country` | VARCHAR(2) | Snapshot of billing country (ISO 3166-1 alpha-2) at invoice time. | | `currency` | VARCHAR(3) | Currency for this invoice. | | `subtotal` | INTEGER | Amount before discounts and tax (smallest currency unit). | | `discount_amount` | INTEGER | Total discounts applied. | | `tax_amount` | INTEGER | Tax amount. | | `total` | INTEGER | Full financial obligation before credit application. | | `credit_applied` | INTEGER DEFAULT 0 | Total credit amount applied to this invoice from credit grants. | | `amount_paid` | INTEGER | Amount paid via payment method so far. | | `amount_due` | INTEGER | Remaining balance after credits. Equal to `total - credit_applied - amount_paid`. | | `invoice_date` | DATE | Date the invoice was issued. | | `due_date` | DATE | Date payment is due. | | `period_start` | DATE | Start of billing period. | | `period_end` | DATE | End of billing period. | | `status` | VARCHAR(20) | `draft`, `open`, `paid`, `void`, `uncollectible`, `refunded`. | | `paid_at` | TIMESTAMPTZ | When fully paid. | | `voided_at` | TIMESTAMPTZ | When voided. | | `invoice_pdf_url` | VARCHAR(500) | URL to downloadable PDF. | | `hosted_invoice_url` | VARCHAR(500) | URL to hosted invoice page. | | `memo` | TEXT | Notes or memo. | | `metadata` | JSONB | Additional invoice attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Note on source FK non-exclusivity:** The `subscription_id` and `purchase_id` columns are intentionally not mutually exclusive. An invoice may reference a subscription, a purchase, both (in mixed-source invoicing scenarios), or neither (for invoices generated from usage charges alone). These are optional associative references, not a polymorphic pattern. **Relationships:** - `invoices` (0..*) → (1) `billing_accounts`: Invoices are sent to a billing account. - `invoices` (0..*) → (0..1) `subscriptions`: Invoices may be for a subscription. - `invoices` (0..*) → (0..1) `purchases`: Invoices may be for a purchase. - `invoices` (1) → (1..*) `invoice_line_items`: Invoices have one or more line items. - `invoices` (1) → (0..*) `payments`: Invoices may have payments applied. - `invoices` (1) → (0..*) `pending_charges`: Invoices may have pending charges swept into them. - `invoices` (1) → (0..*) `credit_transactions`: Invoices may have credits applied. --- ### payments A **payment** records money received from a billing account. | Field | Type | Purpose | |-------|------|---------| | `payment_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing_accounts. The billing account that paid. | | `invoice_id` | UUID | FK → invoices. The invoice this payment is for, if applicable. | | `currency` | VARCHAR(3) | Currency of the payment. | | `amount` | INTEGER | Gross amount received (smallest currency unit). | | `processor_fee` | INTEGER | Fee charged by payment processor. | | `payment_method_type` | VARCHAR(50) | Type: `card`, `bank_transfer`, `crypto`, etc. | | `payment_method_details` | JSONB | Non-identifying descriptors only (e.g., `{"brand": "visa", "last4": "4242"}`). | | `status` | VARCHAR(20) | `pending`, `processing`, `succeeded`, `failed`, `refunded`, `partially_refunded`. | | `failure_code` | VARCHAR(100) | Error code if failed. | | `failure_message` | TEXT | Human-readable failure explanation. | | `paid_at` | TIMESTAMPTZ | When completed. | | `failed_at` | TIMESTAMPTZ | When failed. | | `refunded_at` | TIMESTAMPTZ | When refunded. | | `metadata` | JSONB | Additional payment attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `payments` (0..*) → (1) `billing_accounts`: Payments come from a billing account. - `payments` (0..*) → (0..1) `invoices`: Payments may be applied to an invoice. - `payments` (1) → (0..*) `cooperative.patronage_events`: Successful payments create patronage events. - `payments` (1) → (0..*) `refunds`: Payments may have refunds. - `payments` (1) → (0..*) `disputes`: Payments may have disputes. --- ### payment_methods A **payment_method** records that a billing account has a means of payment on file. This is a provider-agnostic domain projection: it answers "does this account have a payment method?" and "what kind?" without consulting any external service. Provider-specific details (Stripe's `pm_xxx` ID, fingerprint, vault token) live in integration schemas. Payment methods are synchronized inward from payment providers — the provider is authoritative for the payment method's existence and expiration. The core table captures the business-relevant subset needed for local decision-making (auto-collection eligibility, checkout flow routing, account health display). | Field | Type | Purpose | |-------|------|---------| | `payment_method_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing_accounts. The account this payment method belongs to. | | `type` | VARCHAR(30) | Payment method type: `card`, `bank_account`, `sepa_debit`, etc. | | `card_brand` | VARCHAR(20) | Card brand: `visa`, `mastercard`, `amex`, etc. NULL for non-card types. | | `card_last4` | VARCHAR(4) | Last four digits. NULL for non-card types. | | `card_exp_month` | SMALLINT | Card expiration month. NULL for non-card types. | | `card_exp_year` | SMALLINT | Card expiration year. NULL for non-card types. | | `is_default` | BOOLEAN | Whether this is the default payment method for the billing account. | | `status` | VARCHAR(20) | `active`, `expired`, `revoked`. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `payment_methods` (0..*) → (1) `billing_accounts`: Payment methods belong to a billing account. --- ### refunds A **refund** records a partial or full return of money from a payment. Refunds are separate from payments because a refund has its own amount (which may differ from the payment), reason, status, and timing. Modeling refunds as a status flag on payments (as v8 did) loses this information. In the cooperative model, a refund triggers a patronage reversal — the patronage event that credited value to the paying member must be partially or fully reversed. The `patronage_events` table references refunds via `source_type`. | Field | Type | Purpose | |-------|------|---------| | `refund_id` | UUID | Primary key | | `payment_id` | UUID | FK → payments. The payment being refunded. | | `amount` | INTEGER | Refund amount in smallest currency unit. May differ from payment amount (partial refund). | | `currency` | VARCHAR(3) | Currency of the refund. | | `reason` | VARCHAR(50) | Reason: `requested_by_customer`, `duplicate`, `fraudulent`, etc. | | `status` | VARCHAR(20) | `pending`, `succeeded`, `failed`. | | `refunded_at` | TIMESTAMPTZ | When the refund completed. | | `failed_at` | TIMESTAMPTZ | When the refund failed, if applicable. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `refunds` (0..*) → (1) `payments`: Refunds apply to a payment. --- ### disputes A **dispute** (chargeback) records that a payment is being contested through the payment processor's dispute resolution process. Disputes affect revenue, patronage, and potentially account standing — they are business entities, not integration details. The dispute amount, status, reason, and resolution belong in the `billing` schema. Provider-specific dispute IDs, evidence submission details, and processor-specific reason codes live in per-provider integration schemas. | Field | Type | Purpose | |-------|------|---------| | `dispute_id` | UUID | Primary key | | `payment_id` | UUID | FK → payments. The payment under dispute. | | `amount` | INTEGER | Disputed amount in smallest currency unit. | | `currency` | VARCHAR(3) | Currency of the dispute. | | `reason` | VARCHAR(50) | Reason category: `fraudulent`, `product_not_received`, `unrecognized`, `duplicate`, `subscription_canceled`, `other`. | | `status` | VARCHAR(20) | `needs_response`, `under_review`, `won`, `lost`. | | `evidence_due_by` | TIMESTAMPTZ | Deadline for submitting evidence. NULL if not applicable. | | `resolved_at` | TIMESTAMPTZ | When the dispute was resolved (won or lost). | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `disputes` (0..*) → (1) `payments`: Disputes apply to a payment. --- ### invoice_line_items An **invoice_line_item** is a single charge, credit, or adjustment on an invoice. Every invoice has at least one line item. Line items are the atoms of invoice computation — totals, discounts, and taxes are all expressed in terms of line items. The invoice's summary fields (`subtotal`, `discount_amount`, `tax_amount`, `total`) are denormalized aggregates maintained by the invoice generation process and verified against line items. | Field | Type | Purpose | |-------|------|---------| | `line_item_id` | UUID | Primary key | | `invoice_id` | UUID | FK → invoices. The invoice this line item belongs to. | | `line_type` | VARCHAR(30) | Classification: `subscription`, `usage`, `one_time`, `proration_credit`, `proration_charge`, `discount`, `adjustment`. | | `description` | TEXT | Human-readable description displayed on the invoice. | | `product_id` | UUID | FK → products. The product this line item relates to. NULL for discounts and adjustments that are not product-specific. | | `price_id` | UUID | FK → prices. The price used for this line item. NULL for adjustments. | | `subscription_item_id` | UUID | FK → subscription_items. If this line item was generated from a subscription item. | | `metered_key` | VARCHAR(100) | If this line item represents metered usage, the usage key. | | `quantity` | INTEGER | Number of units. | | `unit_amount` | INTEGER | Per-unit amount in smallest currency unit. | | `amount` | INTEGER | Line total before tax (smallest currency unit). May be negative for credits, proration credits, and discounts. | | `discount_amount` | INTEGER DEFAULT 0 | Discount apportioned to this line item (positive value representing reduction). | | `tax_rate` | DECIMAL(7,4) | Tax rate applied (e.g., 0.0875 for 8.75%). NULL if not taxable. | | `tax_amount` | INTEGER | Tax amount for this line item (smallest currency unit). | | `period_start` | DATE | Start of the service period this line item covers. | | `period_end` | DATE | End of the service period. | | `metadata` | JSONB | Additional line item attributes. No PII. | | `created_at` | TIMESTAMPTZ | | **Sum invariant:** `SUM(amount) WHERE line_type != 'discount' = invoices.subtotal`. `invoices.total = subtotal - discount_amount + tax_amount`. **Relationships:** - `invoice_line_items` (1..*) → (1) `invoices`: Line items belong to an invoice. - `invoice_line_items` (0..*) → (0..1) `products`: Line items may reference a product. - `invoice_line_items` (0..*) → (0..1) `prices`: Line items may reference a price. - `invoice_line_items` (0..*) → (0..1) `subscription_items`: Line items may trace to a subscription item. - `invoice_line_items` (1) → (0..*) `discounts`: Line items may have discounts scoped to them. --- ### coupons A **coupon** defines the terms of a price reduction: how much, for how long, for which products, and under what constraints. This is the rule entity — the abstract definition of the discount logic. Coupons are distinct from credits: coupons reduce the obligation (the customer owes less), while credits reduce the collection (the customer has already paid). | Field | Type | Purpose | |-------|------|---------| | `coupon_id` | UUID | Primary key | | `org_id` | UUID | FK → organization.organizations. The organization that created this coupon. NULL for platform-level coupons. | | `name` | VARCHAR(255) | Internal name (e.g., "Launch Promotion", "Annual Loyalty"). | | `display_name` | VARCHAR(255) | Customer-facing name displayed on invoices. | | `discount_type` | VARCHAR(20) | `percentage` or `fixed`. | | `percentage_off` | DECIMAL(5,2) | Percentage to deduct. NULL if `discount_type = 'fixed'`. | | `amount_off` | INTEGER | Fixed amount to deduct (smallest currency unit). NULL if `discount_type = 'percentage'`. | | `currency` | VARCHAR(3) | Currency for `amount_off`. NULL if `discount_type = 'percentage'`. | | `duration` | VARCHAR(20) | `once`, `repeating`, `forever`. | | `duration_months` | INTEGER | For `repeating`: number of billing cycles. NULL otherwise. | | `applies_to_products` | UUID[] | Product IDs this discount is restricted to. NULL means all products. | | `max_redemptions` | INTEGER | Maximum total redemptions. NULL for unlimited. | | `redemption_count` | INTEGER DEFAULT 0 | Current redemption count. Denormalized. | | `valid_from` | TIMESTAMPTZ | Earliest redemption time. NULL for immediate. | | `valid_until` | TIMESTAMPTZ | Latest redemption time. NULL for no expiration. | | `minimum_amount` | INTEGER | Minimum invoice subtotal for the coupon to apply. NULL for no minimum. | | `minimum_amount_currency` | VARCHAR(3) | Currency for `minimum_amount`. | | `first_time_only` | BOOLEAN DEFAULT FALSE | Whether restricted to first-time subscribers/purchasers. | | `is_active` | BOOLEAN DEFAULT TRUE | Whether available for new redemptions. | | `created_by_person_id` | UUID | FK → identity.persons. Who created this coupon. | | `metadata` | JSONB | Additional attributes. No PII. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** ```sql CHECK ( (discount_type = 'percentage' AND percentage_off IS NOT NULL AND amount_off IS NULL) OR (discount_type = 'fixed' AND amount_off IS NOT NULL AND percentage_off IS NULL) ) CHECK ( (duration = 'repeating' AND duration_months IS NOT NULL) OR (duration != 'repeating' AND duration_months IS NULL) ) ``` **Relationships:** - `coupons` (0..*) → (0..1) `organization.organizations`: Coupons may belong to an organization. - `coupons` (0..*) → (1) `identity.persons`: Coupons are created by a person. - `coupons` (1) → (0..*) `promotion_codes`: Coupons may have distribution codes. - `coupons` (1) → (0..*) `discounts`: Coupons may have active discount applications. --- ### promotion_codes A **promotion_code** is a customer-facing key that maps to a coupon with additional distribution constraints. One coupon can have many promotion codes (e.g., five influencer codes all mapping to the same "20% off" coupon, with per-code tracking). | Field | Type | Purpose | |-------|------|---------| | `promotion_code_id` | UUID | Primary key | | `coupon_id` | UUID | FK → coupons. The coupon this code provides access to. | | `code` | VARCHAR(100) | Customer-facing code (e.g., "LAUNCH50"). Case-insensitive. | | `max_redemptions` | INTEGER | Maximum redemptions for this code. NULL for unlimited. | | `redemption_count` | INTEGER DEFAULT 0 | Current redemptions through this code. Denormalized. | | `restrict_to_billing_account_id` | UUID | FK → billing_accounts. If set, restricted to a specific billing account. | | `first_time_only` | BOOLEAN DEFAULT FALSE | Whether restricted to first-time subscribers/purchasers. | | `valid_from` | TIMESTAMPTZ | Earliest time this code can be used. | | `valid_until` | TIMESTAMPTZ | Latest time this code can be used. | | `is_active` | BOOLEAN DEFAULT TRUE | Whether this code is currently usable. | | `created_by_person_id` | UUID | FK → identity.persons. Who created this code. | | `metadata` | JSONB | Additional attributes. No PII. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - `code` has a partial unique index: `UNIQUE (LOWER(code)) WHERE is_active = true`. **Relationships:** - `promotion_codes` (0..*) → (1) `coupons`: Codes map to a coupon. - `promotion_codes` (0..*) → (0..1) `billing_accounts`: Codes may be restricted to a billing account. - `promotion_codes` (0..*) → (1) `identity.persons`: Codes are created by a person. - `promotion_codes` (1) → (0..*) `discounts`: Codes may have active discount applications. --- ### discounts A **discount** records that a coupon has been applied to a specific billing relationship. It is the active state of a coupon on a billing account, subscription, or invoice line item. | Field | Type | Purpose | |-------|------|---------| | `discount_id` | UUID | Primary key | | `coupon_id` | UUID | FK → coupons. The coupon being applied. | | `promotion_code_id` | UUID | FK → promotion_codes. The code used, if any. NULL if applied administratively. | | `billing_account_id` | UUID | FK → billing_accounts. If billing-account-level discount. NULL otherwise. | | `subscription_id` | UUID | FK → subscriptions. If subscription-level discount. NULL otherwise. | | `invoice_line_item_id` | UUID | FK → invoice_line_items. If line-item-level discount. NULL otherwise. | | `applied_by_person_id` | UUID | FK → identity.persons. Who applied this discount. NULL if self-service. | | `code_used` | VARCHAR(100) | Snapshot of the code that was entered. | | `duration_remaining` | INTEGER | For `repeating`: billing cycles remaining. NULL for `once` and `forever`. | | `started_at` | TIMESTAMPTZ | When the discount was applied. | | `ends_at` | TIMESTAMPTZ | When the discount expires based on duration. NULL for `forever`. | | `ended_at` | TIMESTAMPTZ | When actually ended (if manually removed). | | `status` | VARCHAR(20) | `active`, `exhausted`, `expired`, `removed`. | | `removed_at` | TIMESTAMPTZ | When manually removed. | | `removed_by_person_id` | UUID | FK → identity.persons. Who removed it. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints (exclusive arc — scope) ⚡:** ```sql CHECK ( (billing_account_id IS NOT NULL AND subscription_id IS NULL AND invoice_line_item_id IS NULL) OR (billing_account_id IS NULL AND subscription_id IS NOT NULL AND invoice_line_item_id IS NULL) OR (billing_account_id IS NULL AND subscription_id IS NULL AND invoice_line_item_id IS NOT NULL) ) ``` **Status lifecycle:** - `active`: Discount currently applied and reducing charges on future invoices. - `exhausted`: For `once` and `repeating` — all applicable cycles used. - `expired`: `ends_at` timestamp passed. - `removed`: Manually removed before natural expiration. **Relationships:** - `discounts` (0..*) → (1) `coupons`: Discounts reference a coupon. - `discounts` (0..*) → (0..1) `promotion_codes`: Discounts may reference a code. - `discounts` (0..*) → (0..1) `billing_accounts`: Billing-account-scoped. **⚡ Exclusive arc.** - `discounts` (0..*) → (0..1) `subscriptions`: Subscription-scoped. **⚡ Exclusive arc.** - `discounts` (0..*) → (0..1) `invoice_line_items`: Line-item-scoped. **⚡ Exclusive arc.** --- ### pending_charges A **pending_charge** represents a mutable, pre-invoice financial obligation. It bridges the gap between raw inputs (usage events, subscription cycles, ad hoc charges) and frozen outputs (invoice line items). Pending charges are the "invoice items" of the billing pipeline — they represent "we intend to charge for this" and can be edited or voided before they are swept into a draft invoice. | Field | Type | Purpose | |-------|------|---------| | `charge_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing_accounts. Who will be charged. | | `subscription_id` | UUID | FK → subscriptions. If from a subscription cycle. NULL for ad hoc. | | `subscription_item_id` | UUID | FK → subscription_items. If from a specific subscription item. | | `product_id` | UUID | FK → products. The product this charge relates to. | | `price_id` | UUID | FK → prices. The price used. NULL for manual adjustments. | | `charge_type` | VARCHAR(30) | `subscription`, `usage`, `one_time`, `proration_credit`, `proration_charge`, `adjustment`. | | `description` | TEXT | Human-readable description. Carried forward to the invoice line item. | | `metered_key` | VARCHAR(100) | If metered usage, the usage key. | | `quantity` | INTEGER | Number of units. | | `unit_amount` | INTEGER | Per-unit amount (smallest currency unit). | | `amount` | INTEGER | Charge total. May be negative for credits. | | `currency` | VARCHAR(3) | Currency of the charge. | | `period_start` | DATE | Service period start. | | `period_end` | DATE | Service period end. | | `invoice_id` | UUID | FK → invoices. Set when swept into a draft invoice. | | `invoice_line_item_id` | UUID | FK → invoice_line_items. Set when materialized as a line item. | | `created_by_person_id` | UUID | FK → identity.persons. For manual charges. NULL for system-generated. | | `status` | VARCHAR(20) | `pending`, `invoiced`, `voided`. | | `invoiced_at` | TIMESTAMPTZ | When swept into an invoice. | | `voided_at` | TIMESTAMPTZ | When voided. | | `voided_by_person_id` | UUID | FK → identity.persons. Who voided. | | `metadata` | JSONB | Additional attributes. No PII. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Status lifecycle:** - `pending`: Awaiting invoice sweep. Mutable. Can be voided. - `invoiced`: Swept into a draft invoice. Immutable. - `voided`: Cancelled before invoicing. Terminal. **Relationships:** - `pending_charges` (0..*) → (1) `billing_accounts`: Charges belong to a billing account. - `pending_charges` (0..*) → (0..1) `subscriptions`: Charges may originate from a subscription. - `pending_charges` (0..*) → (0..1) `invoices`: Charges may be swept into an invoice. - `pending_charges` (0..*) → (0..1) `invoice_line_items`: Charges may materialize as a line item. - `pending_charges` (1) → (0..*) `pending_charge_usage_events`: Charges may link to contributing usage events. --- ### pending_charge_usage_events A **pending_charge_usage_event** links a usage event to the pending charge it contributed to. This join table preserves the append-only semantics of `usage_events` while providing full traceability from consumption to billing. | Field | Type | Purpose | |-------|------|---------| | `charge_id` | UUID | FK → pending_charges. | | `event_id` | UUID | FK → entitlements.usage_events. | | `created_at` | TIMESTAMPTZ | | **Constraints:** - Primary key on `(charge_id, event_id)`. - Index on `event_id` for "which charge includes this event?" queries. **Relationships:** - `pending_charge_usage_events` (0..*) → (1) `pending_charges`: Links target a charge. - `pending_charge_usage_events` (0..*) → (1) `entitlements.usage_events`: Links target an event. --- ### credit_grants A **credit grant** represents a discrete allocation of prepaid or promotional billing credits to a billing account. Multiple grants may be active simultaneously; they are consumed in priority order. Credits support two application topologies: invoice-time (credits reduce the amount due on a finalized invoice) and real-time (credits are deducted at the moment of consumption). The billing account's `platform_billing_mode` and `customer_billing_mode` fields determine which topology applies. | Field | Type | Purpose | |-------|------|---------| | `grant_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing_accounts. Who holds these credits. | | `name` | VARCHAR(200) | Human-readable description (e.g., "January credit purchase", "Welcome bonus"). | | `category` | VARCHAR(20) | `paid` (customer purchased) or `promotional` (platform granted). Determines accounting treatment. | | `currency` | VARCHAR(3) | Currency of the grant. | | `initial_amount` | INTEGER | Original credit amount in smallest currency unit. | | `balance` | INTEGER | Current available balance. Denormalized from ledger; maintained by credit transactions. | | `priority` | SMALLINT | Application priority. 0 is highest. Lower numbers consumed first. Default: 50. | | `applies_to_products` | UUID[] | Product restrictions. NULL means unrestricted. | | `effective_at` | TIMESTAMPTZ | When credits become usable. | | `expires_at` | TIMESTAMPTZ | When unused credits expire. NULL means no expiration. | | `source_provision_id` | UUID | FK → entitlements.pool_provisions. Which provision triggered this credit grant. NULL for manually created or purchased grants. | | `funded_by_invoice_id` | UUID | FK → invoices. Purchase invoice for paid grants. NULL for promotional. | | `created_by_person_id` | UUID | FK → identity.persons. Who created this grant. | | `status` | VARCHAR(20) | `pending`, `active`, `exhausted`, `expired`, `voided`. | | `voided_at` | TIMESTAMPTZ | When voided. | | `voided_by_person_id` | UUID | FK → identity.persons. Who voided. | | `metadata` | JSONB | Additional attributes. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - Index on `(billing_account_id, status)`. - CHECK: `priority BETWEEN 0 AND 100`. - CHECK: `initial_amount > 0`. - CHECK: `balance >= 0`. **Status lifecycle:** - `pending`: `effective_at` in the future. Not yet usable. - `active`: Effective, positive balance, not expired. - `exhausted`: Balance fully consumed. Terminal. - `expired`: `expires_at` passed with remaining balance. Terminal. - `voided`: Administratively cancelled. Terminal. **Relationships:** - `credit_grants` (0..*) → (1) `billing_accounts`: Grants belong to a billing account. - `credit_grants` (0..*) → (0..1) `entitlements.pool_provisions` (via `source_provision_id`): Grants may be lifecycle-coupled to a provision. - `credit_grants` (0..*) → (0..1) `invoices` (via `funded_by_invoice_id`): Paid grants may reference the purchase invoice. - `credit_grants` (0..*) → (0..1) `identity.persons` (via `created_by_person_id`): Audit attribution. - `credit_grants` (1) → (0..*) `credit_transactions`: Grants have ledger entries. --- ### credit_transactions A **credit transaction** is an immutable ledger entry recording a credit (funds in) or debit (funds out) against a specific grant. The grant's `balance` field is the running total. The ledger is append-only: corrections are made by adding compensating transactions, never by modifying existing ones. | Field | Type | Purpose | |-------|------|---------| | `transaction_id` | UUID | Primary key | | `grant_id` | UUID | FK → credit_grants. Which grant this transaction affects. | | `billing_account_id` | UUID | FK → billing_accounts. Denormalized for account-level ledger queries. | | `type` | VARCHAR(20) | `credit` (funds added) or `debit` (funds consumed). | | `amount` | INTEGER | Transaction amount (smallest currency unit). Always positive; `type` determines direction. | | `balance_after` | INTEGER | Grant balance after this transaction. | | `source_type` | VARCHAR(30) | What triggered this: `initial_funding`, `top_up`, `invoice_application`, `usage_deduction`, `expiration`, `void`, `reinstatement`, `adjustment`. | | `invoice_id` | UUID | FK → invoices. If applied to an invoice. | | `invoice_line_item_id` | UUID | FK → invoice_line_items. If applied to a specific line item. | | `usage_event_id` | UUID | FK → entitlements.usage_events. If for a real-time usage deduction. | | `description` | TEXT | Human-readable description. | | `effective_at` | TIMESTAMPTZ | When this transaction takes effect. | | `created_by_person_id` | UUID | FK → identity.persons. For manual adjustments. NULL for system-generated. | | `metadata` | JSONB | Additional attributes. | | `created_at` | TIMESTAMPTZ | Immutable. | **Constraints:** - Index on `grant_id` for per-grant ledger queries. - Index on `(billing_account_id, effective_at)` for account-level ledger queries. - Index on `invoice_id` for "which credits were applied to this invoice?" queries. - Immutability enforced by application logic. **Ledger invariant:** For any grant, `balance = initial_amount + SUM(credits) - SUM(debits)`. **Relationships:** - `credit_transactions` (0..*) → (1) `credit_grants`: Transactions belong to a grant. - `credit_transactions` (0..*) → (0..1) `invoices`: Debits may reference the invoice. - `credit_transactions` (0..*) → (0..1) `invoice_line_items`: Debits may reference the line item. - `credit_transactions` (0..*) → (0..1) `entitlements.usage_events`: Real-time debits may reference the usage event. --- ### plan_ladders A **plan ladder** is a named, catalog-shape declaration that a set of products is mutually exclusive within a commercial domain — the cooperative sells exactly one of these tiers at a time per provisioned pool. The ladder is a catalog primitive: it declares "these products are alternatives." It does not specify what capabilities each alternative confers (that is the province of `entitlement_sets`) nor which alternative is currently active for any given pool (that is the province of `entitlements.pool_provision_ladders`). ```sql CREATE TABLE billing.plan_ladders ( plan_ladder_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ladder_key VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` | Field | Type | Purpose | |-------|------|---------| | `plan_ladder_id` | UUID | Primary key | | `ladder_key` | VARCHAR(64) | Stable machine identifier (e.g., `nextcloud`, `email_hosting`). Unique across the catalog. | | `name` | VARCHAR(255) | Display name (e.g., "Nextcloud Plans"). | | `description` | TEXT | Optional narrative description of the ladder's commercial domain. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `plan_ladders` (1) → (0..*) `plan_ladder_tiers`: A ladder has one or more tier memberships. The junction is the sole representation of product-ladder membership (Doc 31 Amendment #3). - `plan_ladders` (1) → (0..*) `entitlements.pool_provision_ladders`: Active provisions record which rung they occupy. `FK ← [entitlements] pool_provision_ladders.plan_ladder_id` --- ### plan_ladder_tiers A **plan_ladder_tier** records a product's ordered position within a plan ladder. It is the authoritative and sole many-to-many junction for ladder membership: single-ladder plans, multi-ladder bundles, and every intermediate case express their ladder cardinality through rows in this table. 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, and the schema carries none. The `rank` column expresses relative tier level (lower rank = lower tier), enabling "upgrade" and "downgrade" semantics. Correspondingly, a product's status as a plan is read from the *presence* of rows here — a structural fact exposed through the `product_kinds` view on `products` — rather than from any label the product carries (Doc 31 Amendment #3). Note that "pricing tier" in the Stripe sense (volume pricing brackets on a price) is an entirely distinct concept from a ladder tier; see GLOSSARY §5.2 for the disambiguation. ```sql CREATE TABLE billing.plan_ladder_tiers ( plan_ladder_id UUID NOT NULL REFERENCES billing.plan_ladders(plan_ladder_id) ON DELETE CASCADE, product_id UUID NOT NULL REFERENCES billing.products(product_id), rank INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (plan_ladder_id, product_id), UNIQUE (plan_ladder_id, rank) ); ``` | Field | Type | Purpose | |-------|------|---------| | `plan_ladder_id` | UUID | FK → plan_ladders. The ladder this tier belongs to. | | `product_id` | UUID | FK → products. The product occupying this position. | | `rank` | INTEGER | Ordinal position within the ladder. Unique per ladder. Lower values conventionally denote lower tiers. | | `created_at` | TIMESTAMPTZ | | **Constraints:** - `UNIQUE (plan_ladder_id, rank)` — no two products may occupy the same rank within a ladder. - `PRIMARY KEY (plan_ladder_id, product_id)` — a product appears at most once per ladder. **Relationships:** - `plan_ladder_tiers` (0..*) → (1) `plan_ladders`: Tier memberships belong to a ladder. - `plan_ladder_tiers` (0..*) → (1) `products`: Tier memberships reference a product. --- ## Cooperative Governance — `cooperative` schema **Status:** Early development — module structure defined; full accounting depth deferred to Issue 16. Cooperative Governance answers: "What value has this member contributed, and how does that participation translate into cooperative standing?" This module observes billing outcomes; it does not participate in billing execution. Patronage events are written via Temporal workflows triggered by payment settlement. Billing execution continues unaffected if this module is unavailable. ### patrons A **patron** is a billing account viewed through the lens of cooperative value contribution. Patron records materialize when the first patronage event is recorded. Moved from billing module per Decision 108. | Field | Type | Purpose | |-------|------|---------| | `patron_id` | UUID | Primary key | | `billing_account_id` | UUID | FK → billing.billing_accounts. The billing account this patron represents. One-to-one. | | `patron_number` | VARCHAR(50) | Human-readable identifier (e.g., "P-2024-0001"). | | `first_patronage_date` | DATE | Date of first patronage event. | | `lifetime_gross` | INTEGER | Total gross patronage (smallest currency unit). Denormalized. | | `lifetime_net` | INTEGER | Total net patronage (after fees). Denormalized. | | `status` | VARCHAR(20) | `active`, `inactive`. | | `deactivated_at` | TIMESTAMPTZ | When deactivated. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Relationships:** - `patrons` (0..*) → (1) `billing.billing_accounts`: FK → [billing] billing_accounts. One-to-one. - `patrons` (1) → (0..*) `patronage_events`: Patrons generate patronage events. (intra-module) --- ### patronage_events A **patronage_event** records a single contribution of value from a patron. Patronage is a cooperative governance concept, not an operational billing concept. Append-only; refunds produce compensating events. Moved from billing module per Decision 108. | Field | Type | Purpose | |-------|------|---------| | `event_id` | UUID | Primary key | | `patron_id` | UUID | FK → patrons. The patron who generated this event. | | `fiscal_year` | INTEGER | Fiscal year. Pre-computed. | | `fiscal_quarter` | INTEGER | Fiscal quarter (1-4). Pre-computed. | | `fiscal_month` | INTEGER | Fiscal month (1-12). Pre-computed. | | `event_type` | VARCHAR(50) | `subscription_payment`, `usage_charge`, `one_time_purchase`, `service_fee`, `refund`, `adjustment`, `credit`. | | `source_type` | VARCHAR(50) | What generated this: `payment`, `invoice`, `credit`, `manual`. Governs which source FK is populated. | | `source_payment_id` | UUID | FK → billing.payments. If this event was sourced from a payment. NULL otherwise. | | `source_invoice_id` | UUID | FK → billing.invoices. If this event was sourced from an invoice. NULL otherwise. | | `product_id` | UUID | FK → billing.products. Product this patronage is attributed to, if applicable. | | `currency` | VARCHAR(3) | Currency of the amounts. | | `gross_amount` | INTEGER | What the patron paid. | | `fees_amount` | INTEGER | Payment processor fees. | | `net_amount` | INTEGER | What the cooperative retained. | | `event_date` | DATE | Date of the event. | | `event_timestamp` | TIMESTAMPTZ | Exact time. | | `description` | TEXT | Human-readable description. | | `metadata` | JSONB | Additional event attributes. | | `created_at` | TIMESTAMPTZ | | **Constraints (partial exclusive arc — source):** ```sql CHECK ( (source_type = 'payment' AND source_payment_id IS NOT NULL AND source_invoice_id IS NULL) OR (source_type = 'invoice' AND source_payment_id IS NULL AND source_invoice_id IS NOT NULL) OR (source_type IN ('credit', 'manual') AND source_payment_id IS NULL AND source_invoice_id IS NULL) ) ``` **Relationships:** - `patronage_events` (0..*) → (1) `patrons`: Events belong to a patron. (intra-module) - `patronage_events` (0..*) → (0..1) `billing.products`: FK → [billing] products. Product attribution. - Exclusive arc — source: - `patronage_events` (0..*) → (0..1) `billing.payments`: FK → [billing] payments. - `patronage_events` (0..*) → (0..1) `billing.invoices`: FK → [billing] invoices. --- ## Audit — `audit` schema ### audit_logs An **audit_log** records actions taken within the system. It provides an immutable trail of who did what, when, and to what. The actor model distinguishes the *identity* (person or service account) from the *credential* (session, PAT, API key) used to authenticate. Serves as the complete history for repeated state transition cycles and entitlement toggle history. The table is partitioned by monthly RANGE on `created_at`. The composite primary key `(log_id, created_at)` is required because PostgreSQL enforces that unique constraints on partitioned tables must include all partition key columns. Per-partition uniqueness is acceptable — UUIDv7 collision probability is negligible, and audit records are never referenced by foreign keys from other tables. Events are classified at write time into one of five retention tiers. The tier determines how long the event is retained before archival or deletion: `critical` (20 years — financial/patronage), `security` (7 years — auth/access), `compliance` (7 years — SOC 2 evidence), `operational` (1 year — API calls, navigation), `debug` (90 days — diagnostic). Tier assignment is a write-time classification; changing the tier of an existing event is not expected. | Field | Type | Purpose | |-------|------|---------| | `log_id` | UUID | Composite PK with `created_at`. | | `created_at` | TIMESTAMPTZ | When the action occurred. Partition key. Composite PK with `log_id`. | | `actor_type` | VARCHAR(20) | Identity type: `person`, `service_account`, `system`. | | `actor_person_id` | UUID | FK → identity.persons. If actor is a person. | | `actor_service_account_id` | UUID | FK → organization.service_accounts. If actor is a service account. | | `actor_credential_type` | VARCHAR(20) | How they authenticated: `session` (browser), `pat` (personal access token), `api_key` (service account key), `oidc_client` (Keycloak client credentials), `system` (internal). | | `actor_credential_id` | UUID | ID of the credential used (token_id or key_id). NULL for session and system. | | `actor_ip` | INET | IP address of the actor. | | `actor_user_agent` | TEXT | Browser/client user agent string. | | `entity_type` | VARCHAR(50) | What kind of entity was affected. | | `entity_id` | UUID | ID of the affected entity. | | `org_id` | UUID | FK → organization.organizations. Organization context, for filtering. | | `action` | VARCHAR(50) | What happened (e.g., `create`, `update`, `delete`, `login`). | | `from_status` | VARCHAR(50) | Previous status value for status transition events. NULL for non-status-change actions. Materialized from `changes` JSONB for query performance — JSONB lacks column-level statistics and requires 2× storage overhead for expression indexes. | | `to_status` | VARCHAR(50) | New status value for status transition events. NULL for non-status-change actions. Materialized alongside `from_status`. | | `changes` | JSONB | Details of what changed. Must not contain PII. Status transitions also recorded here as `{"status": {"from": "X", "to": "Y"}}` for backward compatibility. | | `metadata` | JSONB | Additional context. Must not contain PII. | | `request_id` | VARCHAR(100) | Request correlation ID for log tracing. | | `tier` | VARCHAR(20) | Retention tier classification: `critical`, `security`, `compliance`, `operational`, `debug`. Assigned at write time. Determines retention duration via `audit_retention_policies`. | | `severity` | VARCHAR(20) | Event severity: `critical`, `high`, `medium`, `low`, `info`. | | `status` | VARCHAR(20) | Outcome: `success`, `failure`, `partial`. | **Constraints:** ```sql CHECK (severity IN ('critical', 'high', 'medium', 'low', 'info')) CHECK (tier IN ('critical', 'security', 'compliance', 'operational', 'debug')) ``` **Indexing budget (6 indexes):** - 3 composite B-tree: `(entity_type, entity_id, created_at)` for entity lookup; `(actor_person_id, created_at)` and `(actor_service_account_id, created_at)` for actor lookup; `(org_id, action, created_at)` for action filtering. - 1 BRIN: `created_at` for time-range scans (BRIN exploits the near-perfect correlation between insert order and timestamp on append-only tables). - 2 partial: `WHERE status = 'failure' AND action = 'login'` for failed authentication monitoring; `WHERE severity IN ('critical', 'high')` for high-severity event alerting. **Write architecture:** Governance-critical events (financial transactions, access control changes, authentication events) are written synchronously within the business transaction. Operational and debug events may use buffered batch inserts for throughput efficiency. The specific buffering implementation is deferred to build time. **Polymorphic association rationale:** This table contains two intentional bare polymorphic associations that are exceptions to the model's general preference for exclusive arcs: - **`entity_type` + `entity_id`**: The target entity can be any of the 53 tables in the system. Neither an exclusive arc (48 nullable FK columns) nor class table inheritance is appropriate. The entity reference is a metadata annotation on an append-only record, not a navigable relational reference. Per the polymorphic pattern policy (Gate 0), metadata annotations on append-only observational tables may use bare polymorphic associations. - **`actor_credential_type` + `actor_credential_id`**: Only two of five credential types (`pat`, `api_key`) have table-backed FK targets. Sessions, OIDC client credentials, and system credentials do not have dedicated tables. The credential reference is informational metadata, not a relational concern. The actor *identity* side (`actor_person_id`, `actor_service_account_id`) uses proper FK columns because identity is a relational concern. **Relationships:** - `audit_logs` (0..*) → (0..1) `identity.persons`: Logs may reference an acting person. - `audit_logs` (0..*) → (0..1) `organization.service_accounts`: Logs may reference an acting service account. - `audit_logs` (0..*) → (0..1) `organization.organizations`: Logs may be scoped to an organization. --- ### audit_retention_policies An **audit_retention_policy** defines the lifecycle duration for each retention tier. This is a static configuration table, seeded at deployment and rarely changed. Each row maps a tier to its hot, warm, cold, and frozen durations, defining how long audit events in that tier remain at each storage temperature before transitioning to the next. | Field | Type | Purpose | |-------|------|---------| | `policy_id` | UUID | Primary key | | `tier` | VARCHAR(20) | The retention tier this policy governs. Unique. | | `description` | TEXT | Human-readable description of what this tier covers. | | `hot_duration_months` | INTEGER | Months in hot storage (primary PostgreSQL, full indexes). | | `warm_duration_months` | INTEGER | Months in warm storage (PostgreSQL, reduced indexes). NULL if tier transitions directly to cold. | | `cold_duration_months` | INTEGER | Months in cold storage (columnar files outside PostgreSQL). NULL if tier has no cold phase. | | `total_retention_months` | INTEGER | Total retention obligation in months. Computed from tier requirements (e.g., 240 for `critical`). | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - `tier` is unique. **Seed data:** | Tier | Hot | Warm | Cold | Total | |------|-----|------|------|-------| | `critical` | 12 | 72 | 156 | 240 (20yr) | | `security` | 12 | 36 | 36 | 84 (7yr) | | `compliance` | 12 | 36 | 36 | 84 (7yr) | | `operational` | 6 | 6 | — | 12 (1yr) | | `debug` | 3 | — | — | 3 (90d) | --- ### audit_legal_holds An **audit_legal_hold** records an active legal hold that blocks automated archival or anonymization of audit event data within scope. This is structurally parallel to `retention_holds` (which protects person PII from premature erasure) but scoped to audit event data by time range, tenant, actor, and entity. The key distinction: `retention_holds` protects **person PII** at the record level — a specific person's data cannot be anonymized while a hold is active. `audit_legal_holds` protects **audit event data** at the partition level — audit partitions within the hold's scope cannot be archived, anonymized, or dropped while a hold is active. Both are "hold" mechanisms from the same compliance family, with different targets. | Field | Type | Purpose | |-------|------|---------| | `hold_id` | UUID | Primary key | | `hold_name` | VARCHAR(255) | Human-readable name for this hold (e.g., "SEC Investigation 2026-Q3", "Member dispute #4412"). | | `legal_authority` | VARCHAR(100) | The legal basis or requesting authority (e.g., `sec_investigation`, `subpoena`, `internal_audit`, `member_dispute`). | | `description` | TEXT | Detailed explanation of what triggered the hold and what it protects. | | `scope_start` | TIMESTAMPTZ | Earliest `created_at` of audit events covered by this hold. | | `scope_end` | TIMESTAMPTZ | Latest `created_at` of audit events covered by this hold. NULL for open-ended holds. | | `scope_org_id` | UUID | FK → organization.organizations. If the hold is scoped to a specific organization. NULL for platform-wide holds. | | `scope_actor_person_id` | UUID | FK → identity.persons. If the hold is scoped to a specific actor. NULL for broader holds. | | `scope_entity_type` | VARCHAR(50) | If the hold is scoped to a specific entity type. NULL for all entity types. | | `scope_entity_id` | UUID | If the hold is scoped to a specific entity. NULL for broader holds. | | `hold_placed_at` | TIMESTAMPTZ | When the hold was placed. | | `hold_placed_by` | UUID | FK → identity.persons. Who placed the hold. | | `hold_expires_at` | TIMESTAMPTZ | When the hold is scheduled to expire. NULL if expiration depends on a future event. | | `hold_released_at` | TIMESTAMPTZ | When the hold was released. NULL if still active. | | `hold_released_by` | UUID | FK → identity.persons. Who released the hold. | | `release_reason` | TEXT | Why the hold was released. | | `status` | VARCHAR(20) | `active`, `released`, `expired`. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Behavioral contract:** The archival pipeline must check for active `audit_legal_holds` before archiving or dropping any partition. If any active hold's scope overlaps with the partition's time range (and optionally org/actor/entity scope), the partition must not be archived or dropped. This check operates at the partition level, unlike `retention_holds` which operates at the record level. **Relationships:** - `audit_legal_holds` (0..*) → (0..1) `organization.organizations` (via `scope_org_id`): Holds may be scoped to an organization. - `audit_legal_holds` (0..*) → (0..1) `identity.persons` (via `scope_actor_person_id`): Holds may be scoped to a specific actor. - `audit_legal_holds` (0..*) → (1) `identity.persons` (via `hold_placed_by`): Holds are placed by a person. --- ### audit_archive_manifest An **audit_archive_manifest** tracks the lifecycle state of each audit log partition as it transitions through storage tiers: hot → warm → cold → frozen. This is operational metadata for the archival pipeline — it records where each partition is, when it transitioned, and integrity verification status. | Field | Type | Purpose | |-------|------|---------| | `manifest_id` | UUID | Primary key | | `partition_name` | VARCHAR(255) | PostgreSQL partition name (e.g., `audit_logs_2026_01`). Unique. | | `partition_start` | TIMESTAMPTZ | Start of the partition's time range. | | `partition_end` | TIMESTAMPTZ | End of the partition's time range. | | `row_count` | BIGINT | Number of rows in the partition at time of last transition. | | `storage_tier` | VARCHAR(20) | Current storage tier: `hot`, `warm`, `cold`, `frozen`. | | `hot_at` | TIMESTAMPTZ | When the partition was created (entered hot tier). | | `warm_at` | TIMESTAMPTZ | When transitioned to warm tier. NULL if still hot. | | `cold_at` | TIMESTAMPTZ | When transitioned to cold tier (detached from PostgreSQL, exported to columnar files). NULL if not yet cold. | | `frozen_at` | TIMESTAMPTZ | When transitioned to frozen tier (long-term archival). NULL if not yet frozen. | | `dropped_at` | TIMESTAMPTZ | When the partition was dropped from PostgreSQL. NULL if still attached. | | `archive_path` | TEXT | Location of the archived partition file (e.g., S3 path, local path). NULL while hot/warm. | | `archive_checksum` | VARCHAR(128) | Integrity checksum of the archived file. NULL while hot/warm. | | `archive_verified_at` | TIMESTAMPTZ | When the archive integrity was last verified. | | `legal_hold_block` | BOOLEAN DEFAULT FALSE | Whether an active legal hold is preventing this partition from advancing. | | `created_at` | TIMESTAMPTZ | | | `updated_at` | TIMESTAMPTZ | | **Constraints:** - `partition_name` is unique. - `storage_tier` CHECK: `IN ('hot', 'warm', 'cold', 'frozen')`. **Relationships:** - `audit_archive_manifest` is a standalone operational table. It does not have FK relationships to other tables — it references partitions by name and time range, not by relational join. --- ### audit_outbox An **audit_outbox** entry represents an audit event that needs to be delivered to an external consumer (SIEM, analytics pipeline, compliance reporting). The outbox INSERT is performed in the same transaction as the audit event INSERT, guaranteeing at-least-once delivery semantics without distributed transactions. | Field | Type | Purpose | |-------|------|---------| | `outbox_id` | UUID | Primary key | | `log_id` | UUID | The audit event this outbox entry refers to. Not an FK (audit_logs has a composite PK and is partitioned). | | `created_at` | TIMESTAMPTZ | When this outbox entry was created (matches the audit event's `created_at`). | | `destination` | VARCHAR(100) | Target system identifier (e.g., `siem`, `analytics`, `compliance_export`). | | `payload` | JSONB | Event payload for the external consumer. May be a projection of the full audit event. | | `status` | VARCHAR(20) | `pending`, `delivered`, `failed`, `expired`. | | `attempts` | INTEGER DEFAULT 0 | Number of delivery attempts. | | `last_attempt_at` | TIMESTAMPTZ | When delivery was last attempted. | | `delivered_at` | TIMESTAMPTZ | When successfully delivered. | | `error_message` | TEXT | Last delivery error, if any. | **Constraints:** - Index on `(status, created_at)` for the relay query pattern: `WHERE status = 'pending' ORDER BY created_at LIMIT $batch_size`. **Relay pattern:** An external relay process polls the outbox, delivers events to external consumers, and marks entries as `delivered`. The specific relay implementation (polling interval, batch size, consumer protocol) is deferred to build time. --- ## Integration Infrastructure — `integration` schema These tables support integration with external services (payment processors, infrastructure provisioning, tax computation, notification delivery). They live in the `integration` schema because they are shared across all providers — unlike provider-specific mapping tables, which live in per-provider schemas (e.g., `stripe`, `polar`). ### webhook_events A **webhook_event** records an inbound event received from an external provider. This table provides idempotency (deduplication by provider event ID), auditability, and queryable webhook history for debugging and replay. Processing of webhook events is handled by the application layer (e.g., Temporal workflows). The table is partitioned by monthly RANGE on `received_at`, matching the audit log partitioning pattern. | Field | Type | Purpose | |-------|------|---------| | `event_id` | UUID | Composite PK with `received_at`. | | `received_at` | TIMESTAMPTZ | When the event was received. Partition key. Composite PK with `event_id`. | | `provider` | VARCHAR(50) | Which provider sent this event: `stripe`, `polar`, `nextcloud`, etc. | | `provider_event_id` | VARCHAR(500) | The provider's unique identifier for this event. | | `event_type` | VARCHAR(100) | Event type as classified by the provider (e.g., `invoice.finalized`, `subscription.updated`). | | `payload` | JSONB | Full raw webhook payload. Must not contain PII per JSONB governance policy. | | `status` | VARCHAR(20) | Processing state: `received`, `processing`, `completed`, `failed`, `skipped`. | | `processing_started_at` | TIMESTAMPTZ | When processing began. | | `completed_at` | TIMESTAMPTZ | When processing completed. | | `error_message` | TEXT | Error detail if processing failed. | | `retry_count` | INTEGER | Number of processing attempts. | **Constraints:** - `UNIQUE(provider, provider_event_id)` — enforces idempotency. Duplicate webhook deliveries are detected at insert time. **Relationships:** - `webhook_events` is a standalone operational table. Provider and event type are text fields, not FK references. --- ### integration_outbox An **integration_outbox** entry represents a domain state change that must trigger an action in an external system (e.g., provisioning a Nextcloud instance when an entitlement activates, syncing a product to Stripe when it's created). The outbox INSERT is performed in the same transaction as the domain state change, guaranteeing that no integration trigger is lost even if the application crashes after commit. A background process (e.g., a Temporal workflow) polls the outbox and executes the external action. This is the same transactional outbox pattern used by `audit_outbox` for audit event delivery. | Field | Type | Purpose | |-------|------|---------| | `outbox_id` | UUID | Primary key | | `aggregate_type` | VARCHAR(50) | The type of core entity that changed (e.g., `subscription`, `entitlement`, `invoice`). | | `aggregate_id` | UUID | ID of the core entity that changed. | | `event_type` | VARCHAR(100) | What happened (e.g., `entitlement.activated`, `invoice.finalized`, `product.created`). | | `target_provider` | VARCHAR(50) | Which provider should handle this event: `stripe`, `nextcloud`, `notify`, etc. | | `payload` | JSONB | Event payload for the handler. Must not contain PII per JSONB governance policy. | | `status` | VARCHAR(20) | `pending`, `processing`, `completed`, `failed`, `dead_letter`. | | `attempts` | INTEGER | Number of delivery attempts. | | `max_attempts` | INTEGER | Maximum delivery attempts before moving to dead letter. | | `next_attempt_at` | TIMESTAMPTZ | When the next attempt should be made. Supports exponential backoff. | | `last_error` | TEXT | Last delivery error, if any. | | `created_at` | TIMESTAMPTZ | | | `completed_at` | TIMESTAMPTZ | When successfully processed. | **Constraints:** - Index on `(status, next_attempt_at)` for the polling query pattern: `WHERE status IN ('pending', 'failed') AND next_attempt_at <= NOW() ORDER BY next_attempt_at LIMIT $batch_size`. **Relationships:** - `integration_outbox` is a standalone operational table. `aggregate_type` and `target_provider` are text fields, not FK references. --- ## Polymorphic Pattern Policy This model uses a decision heuristic to determine the appropriate pattern for each polymorphic relationship. The heuristic is applied per-relationship, not globally; different patterns coexist within the schema. Exclusive arcs are noted inline throughout this document. **Gate 0: Is this reference a relational concern or a metadata annotation?** If the reference is a metadata annotation — recorded for observability, not for relational navigation; the referencing table is append-only; the target may not exist by the time the reference is read — a bare polymorphic association (`type` + `id`) is acceptable. Document the rationale explicitly. If the reference is a relational concern — downstream logic depends on it, queries JOIN through it, referential integrity matters — proceed with the pattern selection heuristic: 1. Parent types are **not** subtypes of a meaningful abstraction → **Exclusive arc** 2. Multiple tables reference the polymorphic set → **Class table inheritance** 3. Type set is large (>5–8) or expected to grow → **Class table inheritance** 4. Queries frequently aggregate across all types → **Class table inheritance** 5. Otherwise → **Exclusive arc** (simpler, fewer moving parts) --- ## Index of Tables | # | Table | Module | Purpose | |---|-------|-------|---------| | 1 | `users` | Identity | Authentication identity (Keycloak) | | 2 | `persons` | Identity | Human/business identity | | 3 | `personal_access_tokens` | Identity | Programmatic credentials for persons | | 4 | `retention_holds` | Identity | Legal retention obligations on person PII | | 5 | `person_merges` | Identity | Duplicate person resolution tracking | | 6 | `organizations` | Organization | Top-level container | | 7 | `service_accounts` | Organization | Non-human actors | | 8 | `service_account_keys` | Organization | Credentials for service accounts | | 9 | `roles` | Organization | Permission definitions | | 10 | `org_members` | Organization | Organization membership | | 11 | `invitations` | Organization | Invitation lifecycle management | | 12 | `workspaces` | Organization | Resource containers | | 13 | `role_assignments` | Organization | Scoped permission grants | | 14 | `resource_pools` | Entitlements | Capability aggregation and distribution | | 15 | `pool_provisions` | Entitlements | Sources of pool capabilities | | 16 | `pool_provision_ladders` | Entitlements | Junction: which ladder rung each active provision occupies | | 17 | `pool_assignments` | Entitlements | Pool-to-workspace links | | 18 | `pool_ondemand_config` | Entitlements | On-demand resource pricing configuration | | 19 | `boolean_entitlements` | Entitlements | Binary capabilities on pools | | 20 | `numeric_entitlements` | Entitlements | Numeric limits and quotas on pools (definition) | | 21 | `numeric_entitlement_contributions` | Entitlements | Per-provision contributions to numeric entitlement limits | | 22 | `numeric_entitlement_usage` | Entitlements | Mutable numeric entitlement consumption state | | 23 | `usage_events` | Entitlements | Resource consumption records | | 24 | `resource_keys` | Entitlements | Shared resource key namespace | | 25 | `grants` | Entitlements | Non-commercial access provisioning | | 26 | `entitlement_sets` | Entitlements | Named, reusable collections of entitlement rules | | 27 | `entitlement_set_rules` | Entitlements | Set-to-entitlement configuration (formerly product_entitlement_rules) | | 28 | `billing_accounts` | Billing | Payment entity | | 29 | `products` | Billing | Product catalog | | 30 | `prices` | Billing | Commercial terms | | 31 | `subscriptions` | Billing | Recurring billing agreements | | 32 | `subscription_items` | Billing | Subscription line items | | 33 | `subscription_changes` | Billing | Subscription audit trail | | 34 | `purchases` | Billing | One-time transactions | | 35 | `invoices` | Billing | Billing documents | | 36 | `invoice_line_items` | Billing | Per-item invoice detail | | 37 | `payments` | Billing | Money received | | 38 | `payment_methods` | Billing | Payment methods on file | | 39 | `refunds` | Billing | Partial or full payment returns | | 40 | `disputes` | Billing | Payment chargebacks and disputes | | 41 | `coupons` | Billing | Coupon rule definitions | | 42 | `promotion_codes` | Billing | Customer-facing distribution codes | | 43 | `discounts` | Billing | Active discount applications | | 44 | `pending_charges` | Billing | Mutable pre-invoice financial obligations | | 45 | `pending_charge_usage_events` | Billing | Links usage events to pending charges | | 46 | `credit_grants` | Billing | Prepaid or promotional credit allocations | | 47 | `credit_transactions` | Billing | Immutable credit/debit ledger | | 48 | `plan_ladders` | Billing | Named mutual-exclusion sets declaring which products are alternatives | | 49 | `plan_ladder_tiers` | Billing | Ordered product membership within a plan ladder | | 50 | `patrons` | Cooperative | Cooperative member identity overlay on billing accounts | | 51 | `patronage_events` | Cooperative | Append-only cooperative value contribution history | | 52 | `audit_logs` | Audit | Action history (partitioned) | | 53 | `audit_retention_policies` | Audit | Retention tier lifecycle configuration | | 54 | `audit_legal_holds` | Audit | Legal holds on audit event data | | 55 | `audit_archive_manifest` | Audit | Partition lifecycle and archival tracking | | 56 | `audit_outbox` | Audit | Transactional outbox for external consumers | | 57 | `webhook_events` | Integration | Inbound webhook events (partitioned) | | 58 | `integration_outbox` | Integration | Transactional outbox for external integrations |