Files
member-console/design/documents/early-phase1-data-model-reference.md
T
cgalo5758 bfe9cee0fe Consolidate design docs into documents directory
- Remove per-module projection files (README, architecture, companion,
  interfaces, model) under design/<module>/
- Add design/documents/ with numbered design docs, references, policies,
  and manifest
- Update design/README.md to describe the directory as a mirror of
  membcons-db's normative surfaces
- Record Decisions 140-141 in companion and glossary; update
  data-model.md schema organization
2026-08-21 00:55:42 -05:00

40 KiB

Data Model Reference

Foundational Concepts

This data model separates three concerns that are often conflated:

  1. Identity: Who is this human, and how do they authenticate?
  2. Organization & Access: What containers exist, and who can access them?
  3. Billing & Value: Who pays, 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. Separating these concerns allows each to evolve independently.


Layer 1: Identity

Identity answers: "Who is this human, and how do they authenticate?"

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 BIGSERIAL 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.
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 BIGSERIAL Primary key
user_id BIGINT Link to authentication identity. NULL if invited but not yet signed up.
invite_email VARCHAR(255) Email address for pending invitation. Used when person exists before user.
invite_token VARCHAR(255) Secure token for accepting invitation.
invite_expires_at TIMESTAMPTZ When the invitation expires.
invited_by_person_id BIGINT Who sent the invitation. For audit trail.
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.
status VARCHAR(20) State: pending (invited), active, inactive, merged (duplicate resolved).
created_at TIMESTAMPTZ When this person record was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • A person must have either a user_id (signed up) or an invite_email (invited). A person cannot exist without some form of identity.

Relationships:

  • persons (0..1) → (1) users: A person may be linked to a user for authentication.
  • persons (1) → (0..1) organizations: A person may own a personal organization.
  • persons (1) → (0..*) org_members: A person may be a member of multiple organizations.
  • persons (1) → (0..*) workspaces: A person may have created workspaces.
  • persons (1) → (0..*) role_assignments: A person may have workspace-level role assignments.

Layer 2: Organization & Access

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)
Field Type Purpose
org_id BIGSERIAL 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 BIGINT 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.
created_at TIMESTAMPTZ When this organization was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • Personal organizations must have an owner_person_id. Team and enterprise organizations have membership but no single owner.
  • slug is globally unique to enable clean URLs.

Relationships:

  • organizations (0..1) → (1) 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_accounts: Organizations have billing accounts.
  • organizations (1) → (0..*) roles: Organizations may define custom roles.

roles

A role defines a named set of permissions. Roles determine what actions a person can perform within an organization or workspace.

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.

Field Type Purpose
role_id BIGSERIAL Primary key
org_id BIGINT 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.
permissions TEXT[] Array of permission strings this role grants. Format: resource:action (e.g., billing:manage, workspace:delete).
parent_role_id BIGINT Optional parent role for permission inheritance.
created_at TIMESTAMPTZ When this role was created.
updated_at TIMESTAMPTZ Last modification timestamp.

System roles:

  • owner: Full access including organization deletion and ownership transfer.
  • admin: Full access except organization deletion and ownership transfer.
  • member: Can access workspaces and manage resources.
  • billing: Can only access billing information.
  • viewer: Read-only access.

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 workspace level.
  • roles (0..1) → (0..1) roles: Roles may inherit from a parent role.

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. An organization may have many members with different roles. Personal organizations have exactly one member (the owner). Team organizations may have many members with varying permissions.

Field Type Purpose
org_member_id BIGSERIAL Primary key
org_id BIGINT The organization.
person_id BIGINT The person who is a member.
role_id BIGINT The role this person has in this organization.
invited_by_person_id BIGINT Who invited this person. For audit trail.
invited_at TIMESTAMPTZ When the invitation was sent.
accepted_at TIMESTAMPTZ When the invitation was accepted.
status VARCHAR(20) Membership state: pending (invited), active, suspended, 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) persons: Memberships belong to a person.
  • org_members (0..*) → (1) roles: Memberships have a role.

workspaces

A workspace is a container for resources within an organization. Workspaces provide isolation, organization, and optionally different access controls within a single organization.

Workspaces serve several purposes:

  • Isolation: Resources in one workspace are separate from another (e.g., production vs. staging).
  • Organization: Logical grouping of related resources (e.g., by project, client, or team).
  • Access control: Permissions can be granted at workspace level, not just organization level.
  • Quota allocation: Organization-wide quotas can be subdivided across workspaces.
Field Type Purpose
workspace_id BIGSERIAL Primary key
org_id BIGINT 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 BIGINT Who created this workspace.
status VARCHAR(20) State: active, archived, 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) persons: Workspaces track who created them.
  • workspaces (1) → (0..*) role_assignments: Workspaces may have person-specific role assignments.
  • workspaces (1) → (0..*) quotas: Workspaces may have allocated quotas.
  • workspaces (1) → (0..*) patronage_events: Usage may be attributed to a workspace.

role_assignments

A role_assignment grants a role to a person for a specific scope (currently workspace-level). This enables finer-grained access control than organization-level membership.

Organization membership (via org_members) grants baseline access. Role assignments allow exceptions and refinements:

  • 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.
  • Temporary access can be granted with an expiration date.
Field Type Purpose
assignment_id BIGSERIAL Primary key
person_id BIGINT The person receiving the role.
role_id BIGINT The role being granted.
scope_type VARCHAR(20) What kind of resource this applies to. Currently only workspace.
scope_id BIGINT The ID of the resource (e.g., workspace_id).
granted_by_person_id BIGINT 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.
status VARCHAR(20) State: active, revoked, expired.
created_at TIMESTAMPTZ When this assignment was created.

Constraints:

  • A person can only have one assignment of a specific role for a specific scope. The combination of person_id, role_id, scope_type, and scope_id is unique.

Relationships:

  • role_assignments (0..*) → (1) persons: Assignments belong to a person.
  • role_assignments (0..*) → (1) roles: Assignments grant a role.
  • role_assignments (0..*) → (1) workspaces: Assignments are scoped to a workspace (when scope_type = workspace).

Layer 3: Billing & Value

Billing & Value answers: "Who pays, what do they pay for, and what value have they contributed?"

billing_accounts

A billing_account is the entity that pays. It holds payment methods, 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.
  • Future flexibility: billing accounts could potentially span organizations (enterprise billing) or be shared.
Field Type Purpose
billing_account_id BIGSERIAL Primary key
org_id BIGINT 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).
stripe_customer_id VARCHAR(255) Reference to Stripe customer object.
default_payment_method_id VARCHAR(255) Reference to default payment method in Stripe.
settings JSONB Billing preferences and configuration.
status VARCHAR(20) State: active, suspended, 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).
  • stripe_customer_id is unique when set.

Relationships:

  • billing_accounts (0..*) → (1) organizations: Billing accounts belong to an organization.
  • billing_accounts (1) → (0..*) subscriptions: Billing accounts have subscriptions.
  • billing_accounts (1) → (0..*) invoices: Billing accounts receive invoices.
  • billing_accounts (1) → (0..*) payments: Billing accounts make payments.
  • billing_accounts (1) → (0..1) patrons: A billing account is a patron (for cooperative tracking).
  • billing_accounts (1) → (0..*) entitlements: Billing accounts have entitlements.
  • billing_accounts (1) → (0..*) quotas: Billing accounts have quota pools.

products

A product represents something you sell. Products define what features or capabilities a subscription grants, independent of pricing.

Products are separated from prices because:

  • A single product may have multiple prices (monthly vs. annual, different tiers, different currencies).
  • Product features define capabilities; prices define cost.
  • Products can be deactivated while preserving historical price records.
Field Type Purpose
product_id BIGSERIAL Primary key
name VARCHAR(255) Display name (e.g., "Pro Plan", "API Access").
description TEXT Marketing/explanatory description.
product_type VARCHAR(50) Classification: service (subscription), addon (additional feature), usage (metered), one_time (single purchase).
features JSONB Array of feature keys this product grants (e.g., ["sites", "custom_domains", "api_access"]).
stripe_product_id VARCHAR(255) Reference to Stripe product object.
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 When this product was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Relationships:

  • products (1) → (0..*) prices: Products have one or more prices.
  • products (1) → (0..*) patronage_events: Patronage can be attributed to a product.

prices

A price defines how much a product costs and what billing terms apply. A price also specifies what quotas (numeric limits) are included.

Prices capture the commercial terms of a subscription:

  • Amount and currency
  • Billing frequency (monthly, annual)
  • Trial periods
  • Included quotas (how many sites, how much storage, etc.)
Field Type Purpose
price_id BIGSERIAL Primary key
product_id BIGINT 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: per_unit (simple) or tiered (volume-based).
recurring_interval VARCHAR(20) Billing frequency: day, week, month, year, or NULL for one-time.
recurring_interval_count INTEGER Number of intervals between billings (e.g., 1 for monthly, 3 for quarterly).
trial_period_days INTEGER Number of days in trial period. NULL for no trial.
usage_type VARCHAR(20) For usage-based pricing: metered (pay per use) or licensed (pay per seat).
included_quotas JSONB Quotas this price grants (e.g., {"sites_limit": 5, "storage_gb": 10}).
stripe_price_id VARCHAR(255) Reference to Stripe price object.
metadata JSONB Additional price attributes.
is_active BOOLEAN Whether this price can be used for new subscriptions.
created_at TIMESTAMPTZ When this price was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Relationships:

  • prices (0..*) → (1) products: Prices belong to a product.
  • prices (1) → (0..*) subscriptions: Prices are used by subscriptions.
  • prices (1) → (0..*) subscription_changes: Price changes are recorded.

subscriptions

A subscription represents an active billing relationship between a billing account and a price. Subscriptions grant entitlements and quotas for as long as they remain active.

Subscriptions are the link between payment and access:

  • An active subscription grants the features and quotas defined by its product/price.
  • Subscription status determines whether access is granted.
  • Subscriptions track billing periods, trials, and cancellation state.
Field Type Purpose
subscription_id BIGSERIAL Primary key
billing_account_id BIGINT The billing account that is subscribed.
price_id BIGINT The price (and thus product) subscribed to.
quantity INTEGER Number of units (for per-seat pricing).
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.
status VARCHAR(20) State: trialing, active, past_due, canceled, unpaid, paused, incomplete.
stripe_subscription_id VARCHAR(255) Reference to Stripe subscription object.
metadata JSONB Additional subscription attributes.
created_at TIMESTAMPTZ When this subscription was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Relationships:

  • subscriptions (0..*) → (1) billing_accounts: Subscriptions belong to a billing account.
  • subscriptions (0..*) → (1) prices: Subscriptions are for a specific price.
  • subscriptions (1) → (0..*) subscription_changes: Changes to subscriptions are tracked.
  • subscriptions (1) → (0..*) invoices: Subscriptions generate invoices.
  • subscriptions (1) → (0..*) entitlements: Subscriptions grant entitlements.
  • subscriptions (1) → (0..*) quotas: Subscriptions grant quotas.

subscription_changes

A subscription_change records a modification to a subscription. This provides an audit trail of subscription lifecycle events.

Tracking changes separately from current state allows:

  • Historical analysis of subscription behavior
  • Understanding churn and upgrade/downgrade patterns
  • Debugging billing issues
  • Audit compliance
Field Type Purpose
change_id BIGSERIAL Primary key
subscription_id BIGINT The subscription that changed.
change_type VARCHAR(50) What kind of change: created, upgraded, downgraded, quantity_changed, renewed, trial_started, trial_ended, paused, resumed, canceled, reactivated, ended.
previous_price_id BIGINT Price before the change.
previous_quantity INTEGER Quantity before the change.
previous_status VARCHAR(20) Status before the change.
new_price_id BIGINT Price after the change.
new_quantity INTEGER Quantity after the change.
new_status VARCHAR(20) Status after the change.
changed_by_person_id BIGINT Who made the change (NULL if system/automated).
reason TEXT Explanation for the change.
stripe_event_id VARCHAR(255) Stripe event that triggered this change, if applicable.
effective_at TIMESTAMPTZ When the change took/takes effect.
created_at TIMESTAMPTZ When this record was created.

Relationships:

  • subscription_changes (0..*) → (1) subscriptions: Changes belong to a subscription.
  • subscription_changes (0..*) → (0..1) prices: Changes may reference previous and new prices.
  • subscription_changes (0..*) → (0..1) persons: Changes may be attributed to a person.

invoices

An invoice is a billing document requesting payment from a billing account. Invoices may be generated from subscriptions or created manually.

Invoices capture the billing snapshot at a point in time:

  • Billing details are copied (not referenced) because they may change later
  • Amounts are calculated and frozen
  • Status tracks the payment lifecycle
Field Type Purpose
invoice_id BIGSERIAL Primary key
invoice_number VARCHAR(50) Human-readable unique identifier (e.g., "INV-2024-0001").
billing_account_id BIGINT The billing account being invoiced.
subscription_id BIGINT The subscription 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 JSONB Snapshot of full billing address 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 Final amount due (subtotal - discounts + tax).
amount_paid INTEGER Amount paid so far.
amount_due INTEGER Remaining balance (total - amount_paid).
invoice_date DATE Date the invoice was issued.
due_date DATE Date payment is due.
period_start DATE Start of billing period this invoice covers.
period_end DATE End of billing period this invoice covers.
status VARCHAR(20) State: draft, open, paid, void, uncollectible, refunded.
paid_at TIMESTAMPTZ When the invoice was fully paid.
stripe_invoice_id VARCHAR(255) Reference to Stripe invoice object.
invoice_pdf_url VARCHAR(500) URL to downloadable PDF.
hosted_invoice_url VARCHAR(500) URL to hosted invoice page.
memo TEXT Notes or memo on the invoice.
metadata JSONB Additional invoice attributes.
created_at TIMESTAMPTZ When this invoice was created.
updated_at TIMESTAMPTZ Last modification timestamp.

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 (1) → (0..*) payments: Invoices may have payments applied.

payments

A payment records money received from a billing account. Payments may be applied to invoices or exist as account credits.

Payments track the actual money received:

  • Amount and currency
  • Payment method used
  • Processor fees (important for net patronage calculation)
  • Success or failure state
Field Type Purpose
payment_id BIGSERIAL Primary key
billing_account_id BIGINT The billing account that paid.
invoice_id BIGINT 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. Used to calculate net patronage.
payment_method_type VARCHAR(50) Type of payment method: card, bank_transfer, crypto, etc.
payment_method_details JSONB Details about the payment method (e.g., {"brand": "visa", "last4": "4242"}).
status VARCHAR(20) State: pending, processing, succeeded, failed, refunded, partially_refunded.
failure_code VARCHAR(100) Error code if payment failed.
failure_message TEXT Human-readable failure explanation.
paid_at TIMESTAMPTZ When the payment was completed.
stripe_payment_intent_id VARCHAR(255) Reference to Stripe PaymentIntent.
stripe_charge_id VARCHAR(255) Reference to Stripe Charge.
metadata JSONB Additional payment attributes.
created_at TIMESTAMPTZ When this payment was created.
updated_at TIMESTAMPTZ Last modification timestamp.

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..*) patronage_events: Successful payments create patronage events.

patrons

A patron is a billing account viewed through the lens of cooperative value contribution. Every billing account that has made a payment is a patron.

The patron entity exists to:

  • Assign a human-readable patron number for statements and tax forms
  • Track lifetime patronage totals for quick access
  • Serve as the anchor point for patronage events
  • Eventually link to cooperative membership (in later phases)
Field Type Purpose
patron_id BIGSERIAL Primary key
billing_account_id BIGINT The billing account this patron represents. One-to-one relationship.
patron_number VARCHAR(50) Human-readable identifier (e.g., "P-2024-0001"). Used on statements and tax forms.
first_patronage_date DATE Date of first patronage event.
lifetime_gross INTEGER Total gross patronage ever (smallest currency unit). Denormalized for quick access.
lifetime_net INTEGER Total net patronage ever (after fees). Denormalized for quick access.
status VARCHAR(20) State: active, inactive.
created_at TIMESTAMPTZ When this patron record was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Relationships:

  • patrons (1) → (1) billing_accounts: A patron is a billing account. One-to-one.
  • patrons (1) → (0..*) patronage_events: Patrons generate patronage events.

patronage_events

A patronage_event records a single contribution of value from a patron. Each successful payment creates a patronage event. Refunds create negative patronage events.

Patronage events are the source of truth for cooperative value calculation:

  • Gross amount is what the patron paid
  • Net amount is what the cooperative retained after payment processor fees
  • Fiscal period allows annual patronage calculations
  • Product and workspace attribution enable detailed analysis
Field Type Purpose
event_id BIGSERIAL Primary key
patron_id BIGINT The patron who generated this event.
fiscal_year INTEGER Fiscal year for this event. Used for annual patronage calculations.
fiscal_quarter INTEGER Fiscal quarter (1-4).
fiscal_month INTEGER Fiscal month (1-12).
event_type VARCHAR(50) Classification: subscription_payment, usage_charge, one_time_purchase, service_fee, refund, adjustment, credit.
source_type VARCHAR(50) What generated this event: payment, invoice, credit, manual.
source_id BIGINT ID of the source record (e.g., payment_id).
product_id BIGINT Product this patronage is attributed to, if applicable.
workspace_id BIGINT Workspace this patronage is attributed to, if applicable.
currency VARCHAR(3) Currency of the amounts.
gross_amount INTEGER What the patron paid (smallest currency unit).
fees_amount INTEGER Payment processor fees.
net_amount INTEGER What the cooperative retained (gross - fees).
event_date DATE Date of the event.
event_timestamp TIMESTAMPTZ Exact time of the event.
description TEXT Human-readable description.
metadata JSONB Additional event attributes.
created_at TIMESTAMPTZ When this record was created.

Relationships:

  • patronage_events (0..*) → (1) patrons: Events belong to a patron.
  • patronage_events (0..*) → (0..1) products: Events may be attributed to a product.
  • patronage_events (0..*) → (0..1) workspaces: Events may be attributed to a workspace.
  • patronage_events (0..*) → (0..1) payments: Events are typically sourced from payments.

Layer 4: Entitlements & Quotas

Entitlements & Quotas answers: "What capabilities does this billing account have, and what are the limits?"

entitlements

An entitlement represents a capability that a billing account has. Entitlements are typically granted by subscriptions but can also be granted manually (trials, promotions, legacy access).

Entitlements are binary (enabled/disabled) capabilities:

  • "Can create sites" (yes/no)
  • "Can use custom domains" (yes/no)
  • "Can access API" (yes/no)

For numeric limits, see quotas.

Field Type Purpose
entitlement_id BIGSERIAL Primary key
billing_account_id BIGINT The billing account that has this entitlement.
feature_key VARCHAR(100) Machine-readable feature identifier (e.g., sites, custom_domains, api_access).
is_enabled BOOLEAN Whether this entitlement is currently active.
source_type VARCHAR(50) What granted this entitlement: subscription, trial, grant, legacy, promotion.
source_id BIGINT ID of the source (e.g., subscription_id).
valid_from TIMESTAMPTZ When this entitlement became active.
valid_until TIMESTAMPTZ When this entitlement expires. NULL means valid until source ends.
metadata JSONB Additional entitlement attributes.
created_at TIMESTAMPTZ When this entitlement was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • An entitlement is unique per billing account, feature, source type, and source ID. This allows multiple sources to grant the same feature (e.g., subscription + promotional grant).

Relationships:

  • entitlements (0..*) → (1) billing_accounts: Entitlements belong to a billing account.
  • entitlements (0..*) → (0..1) subscriptions: Entitlements may be granted by a subscription.

quotas

A quota represents a numeric limit on resource usage. Quotas can exist at the billing account level (the "pool") or workspace level (an "allocation" from the pool).

Quotas enable:

  • Limiting resource creation (max 5 sites)
  • Limiting storage (max 10 GB)
  • Limiting API usage (max 10,000 calls per month)
  • Subdividing organization-wide limits across workspaces
Field Type Purpose
quota_id BIGSERIAL Primary key
billing_account_id BIGINT If set, this is a billing account-level quota (the pool).
workspace_id BIGINT If set, this is a workspace-level quota (an allocation).
quota_key VARCHAR(100) Machine-readable identifier (e.g., sites_limit, storage_bytes, api_calls_monthly).
quota_limit BIGINT The limit. -1 means unlimited.
current_usage BIGINT Current usage against this quota.
reset_period VARCHAR(20) For periodic quotas: daily, monthly, yearly, or NULL for non-resetting.
current_period_start TIMESTAMPTZ Start of current reset period.
current_period_end TIMESTAMPTZ End of current reset period.
last_reset_at TIMESTAMPTZ When usage was last reset.
source_type VARCHAR(50) What granted this quota: subscription, grant, legacy, allocation.
source_id BIGINT ID of the source.
created_at TIMESTAMPTZ When this quota was created.
updated_at TIMESTAMPTZ Last modification timestamp.

Constraints:

  • A quota must have either billing_account_id (account-level) or workspace_id (workspace-level), not both.
  • A quota key is unique within its scope.

Relationships:

  • quotas (0..*) → (0..1) billing_accounts: Account-level quotas belong to a billing account.
  • quotas (0..*) → (0..1) workspaces: Workspace-level quotas belong to a workspace.
  • quotas (0..*) → (0..1) subscriptions: Quotas may be granted by a subscription.

Audit

audit_logs

An audit_log records actions taken within the system. It provides an immutable trail of who did what, when, and to what.

Audit logs serve:

  • Security investigations
  • Compliance requirements
  • Debugging and support
  • Understanding user behavior
Field Type Purpose
log_id BIGSERIAL Primary key
timestamp TIMESTAMPTZ When the action occurred.
actor_type VARCHAR(20) What kind of actor: user, system, api_key, webhook.
actor_user_id BIGINT The user who performed the action, if applicable.
actor_person_id BIGINT The person who performed the action, if applicable.
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 (e.g., user, org, workspace, subscription).
entity_id BIGINT ID of the affected entity.
org_id BIGINT Organization context, for filtering.
action VARCHAR(50) What happened (e.g., create, update, delete, login).
changes JSONB Details of what changed (e.g., {"name": {"old": "A", "new": "B"}}).
metadata JSONB Additional context.
request_id VARCHAR(100) Request correlation ID for log tracing.
status VARCHAR(20) Outcome: success, failure, partial.

Relationships:

  • audit_logs (0..*) → (0..1) users: Logs may reference an acting user.
  • audit_logs (0..*) → (0..1) organizations: Logs may be scoped to an organization.

Index of Tables

# Table Layer Purpose
1 users Identity Authentication identity
2 persons Identity Human/business identity
3 organizations Organization Top-level container
4 roles Organization Permission definitions
5 org_members Organization Organization membership
6 workspaces Organization Resource containers
7 role_assignments Organization Workspace-level permissions
8 billing_accounts Billing Payment entity
9 products Billing What you sell
10 prices Billing How much things cost
11 subscriptions Billing Active billing relationships
12 subscription_changes Billing Subscription audit trail
13 invoices Billing Billing documents
14 payments Billing Money received
15 patrons Billing Cooperative value identity
16 patronage_events Billing Cooperative value transactions
17 entitlements Entitlements Binary capabilities
18 quotas Entitlements Numeric limits
19 audit_logs Audit Action history