Files
member-console/design/data-model.md

155 KiB
Raw Blame History

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, 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:

  • pendingactive: Invitation accepted, account established.
  • activeinactive: Administrative deactivation. Reversible.
  • activepartially_erased: Erasure requested but retention holds prevent full anonymization. Non-retained PII scrubbed. Not reversible.
  • active or inactiveanonymized: Full PII destruction. Irrevocable terminal state.
  • partially_erasedanonymized: All retention holds released. Remaining PII scrubbed. Irrevocable.
  • activemerged: 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_<random> — 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_idtarget_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_<random> (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):

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):

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_<random>. Token is regenerated on resend (old token becomes invalid, new hash stored).

Status lifecycle:

  • pendingsent: Notification delivered.
  • sentsent: Resend (resets expires_at, increments send_count).
  • sentaccepted, declined, expired, revoked: Terminal states, all irrevocable.
  • pendingexpired, 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):

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):

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):

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:

-- 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();
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_eventspending_chargesinvoice_line_itemsinvoices, 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):

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:

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:

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:

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:

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) :

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).

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.

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):

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:

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)

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 (>58) 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