EntityModel.comDocs
Contents

Start here

Guides

Examples

Platforms

Compare

Reference

Learn

SaaS subscription billing data model

This example models how a SaaS company bills its customers: organizations and their users, a catalog of plans with prices per currency and interval, subscriptions made of items (seats, flat fees or metered usage), the usage reported against them, and the invoices and payments each billing period produces.

Design notes

  • Plans and prices are separate. A plan is what you sell (Team, Business); a price is how much, in which currency, how often and on which model (flat, per seat, metered, tiered). Prices are never edited, only replaced, so old invoices stay explainable.
  • A subscription has items. Each item is one price with a quantity: 25 seats of Team monthly, plus a metered add-on. Upgrades change items rather than the subscription itself.
  • Usage is recorded, not summed in place. usage_record keeps every report with an idempotency key; invoicing sums it for the period.
  • Invoices copy amounts. Invoice lines store quantity, unit amount and amount as billed, and flag prorations from mid-period changes.
  • Payments are attempts. Failed attempts are kept with their reason, which drives dunning and the PAST_DUE status.
  • Status, interval and pricing model are reference lists with their values.

To extend it: add coupon and discount, credit_note, tax_rate, payment_method (stored as a provider token), or entitlement for feature flags per plan.

Diagram

SaaS Subscription Billing: 10 entities, 6 reference lists and 11 relationships. Each box is an entity with its attributes (PK marks the primary key); solid lines are relationships in crow's foot notation, dashed lines point to reference lists.

User Member Of Organization (N:1)Member OfPrice Price Of Plan (N:1)Price OfSubscription Held By Organization (N:1)Held BySubscription Item Item Of Subscription (N:1)Item OfSubscription Item Pays Price (N:1)PaysUsage Record Reported For Subscription Item (N:1)Reported ForInvoice Billed To Organization (N:1)Billed ToInvoice Bills Subscription (N:1)BillsInvoice Line Line On Invoice (N:1)Line OnInvoice Line Charges Subscription Item (N:1)ChargesPayment Pays Invoice (N:1)PaysOrganization: A customer company or team that subscribes. The billing entity.OrganizationorganizationPKorganization_idBIGINTorganization_nameSTRINGbilling_emailSTRINGcountry_code→ countrytax_idSTRINGcreated_atTIMESTAMPUser: A person who signs in. Belongs to one organization; seat-based plans count active users.Userapp_userPKuser_idBIGINTorganization_idBIGINTemailSTRINGfull_nameSTRINGroleSTRINGis_activeBOOLEANPlan: A product tier customers can subscribe to, such as Team or Business.PlanplanPKplan_idINTplan_codeSTRINGplan_nameSTRINGis_publicBOOLEANPrice: A price for a plan in one currency and billing interval. Prices are never edited; a new price replaces an old one.Priceplan_pricePKprice_idINTplan_idINTcurrency_code→ currencybilling_interval_code→ billing_intervalpricing_model→ pricing_modelunit_amountDECIMALis_activeBOOLEANSubscription: An organization's subscription. Its items say which prices it pays and in what quantity.SubscriptionsubscriptionPKsubscription_idBIGINTorganization_idBIGINTsubscription_status_code→ subscription_statusstarted_atTIMESTAMPcurrent_period_startDATEcurrent_period_endDATEtrial_ends_atTIMESTAMPcancel_at_period_endBOOLEANcanceled_atTIMESTAMPSubscription Item: One price on a subscription with its quantity, such as 25 seats of Team monthly.Subscription Itemsubscription_itemPKsubscription_item_idBIGINTsubscription_idBIGINTprice_idINTquantityINTUsage Record: Metered usage reported against a subscription item, summed at the end of each period.Usage Recordusage_recordPKusage_record_idBIGINTsubscription_item_idBIGINTquantityBIGINTrecorded_atTIMESTAMPidempotency_keySTRINGInvoice: A bill for one billing period of a subscription, or a one-off charge.InvoiceinvoicePKinvoice_idBIGINTinvoice_numberSTRINGorganization_idBIGINTsubscription_idBIGINTinvoice_status_code→ invoice_statuscurrency_code→ currencyperiod_startDATEperiod_endDATEsubtotal_amountDECIMALtax_amountDECIMALtotal_amountDECIMALissued_atTIMESTAMPdue_atTIMESTAMPInvoice Line: A charge on an invoice: a subscription item for the period, prorations, or usage.Invoice Lineinvoice_linePKinvoice_line_idBIGINTinvoice_idBIGINTsubscription_item_idBIGINTdescriptionSTRINGquantityBIGINTunit_amountDECIMALamountDECIMALis_prorationBOOLEANPayment: A payment attempt for an invoice through the payment provider.PaymentpaymentPKpayment_idBIGINTinvoice_idBIGINTprovider_referenceSTRINGamountDECIMALsucceededBOOLEANattempted_atTIMESTAMPfailure_reasonSTRINGSubscription Status: Lifecycle states of a subscription.Subscription Statussubscription_statusREFERENCEPKstatus_codeSTRINGstatus_nameSTRING5 valuesInvoice Status: Lifecycle states of an invoice.Invoice Statusinvoice_statusREFERENCEPKstatus_codeSTRINGstatus_nameSTRING5 valuesBilling Interval: How often a price is billed.Billing Intervalbilling_intervalREFERENCEPKinterval_codeSTRINGinterval_nameSTRINGmonthsINT3 valuesPricing Model: How a price turns into an amount.Pricing Modelpricing_modelREFERENCEPKmodel_codeSTRINGmodel_nameSTRING4 valuesCountry: Countries, identified by their ISO 3166-1 alpha-2 code.CountrycountryREFERENCEPKcountry_codeSTRINGcountry_nameSTRING6 valuesCurrency: Currencies, identified by their ISO 4217 code.CurrencycurrencyREFERENCEPKcurrency_codeSTRINGcurrency_nameSTRING5 values
Open the full-size diagram (SVG)

Open this model in EntityModel.com to explore it, change it and export it. Nothing is uploaded: the model travels in the link. Or download model.json.

Entities

Entity Type Description Primary Key Display Attribute Attributes
Invoice Entity A bill for one billing period of a subscription, or a one-off charge. invoice_id invoice_number 13
Invoice Line Entity A charge on an invoice: a subscription item for the period, prorations, or usage. invoice_line_id description 8
Organization Entity A customer company or team that subscribes. The billing entity. organization_id organization_name 6
Payment Entity A payment attempt for an invoice through the payment provider. payment_id provider_reference 7
Plan Entity A product tier customers can subscribe to, such as Team or Business. plan_id plan_name 4
Price Entity A price for a plan in one currency and billing interval. Prices are never edited; a new price replaces an old one. price_id unit_amount 7
Subscription Entity An organization's subscription. Its items say which prices it pays and in what quantity. subscription_id started_at 9
Subscription Item Entity One price on a subscription with its quantity, such as 25 seats of Team monthly. subscription_item_id quantity 4
Usage Record Entity Metered usage reported against a subscription item, summed at the end of each period. usage_record_id quantity 5
User Entity A person who signs in. Belongs to one organization; seat-based plans count active users. user_id full_name 6
Billing Interval Reference How often a price is billed. interval_code interval_name 3
Country Reference Countries, identified by their ISO 3166-1 alpha-2 code. country_code country_name 2
Currency Reference Currencies, identified by their ISO 4217 code. currency_code currency_name 2
Invoice Status Reference Lifecycle states of an invoice. status_code status_name 2
Pricing Model Reference How a price turns into an amount. model_code model_name 2
Subscription Status Reference Lifecycle states of a subscription. status_code status_name 2

Attributes

Invoice

A bill for one billing period of a subscription, or a one-off charge.

Attribute Data Type Flags Reference Example Description
invoice_id BIGINT PK, Required 300045
invoice_number STRING Label, Required INV-2026-0045
organization_id BIGINT Required 4100
subscription_id BIGINT 7001
invoice_status_code REFERENCE Required → invoice_status.status_code OPEN
currency_code REFERENCE Required → currency.currency_code USD
period_start DATE 2026-11-01
period_end DATE 2026-11-30
subtotal_amount DECIMAL(12,2) Required 300.00
tax_amount DECIMAL(12,2) Required 24.00
total_amount DECIMAL(12,2) Required 324.00
issued_at TIMESTAMP Required 2026-11-01T00:00:00Z
due_at TIMESTAMP Required 2026-11-15T00:00:00Z

Invoice Line

A charge on an invoice: a subscription item for the period, prorations, or usage.

Attribute Data Type Flags Reference Example Description
invoice_line_id BIGINT PK, Required 3000451
invoice_id BIGINT Required 300045
subscription_item_id BIGINT 70011
description STRING Label, Required Team (25 seats) Nov 2026
quantity BIGINT Required 25
unit_amount DECIMAL(12,2) Required 12.00
amount DECIMAL(12,2) Required 300.00
is_proration BOOLEAN Required false Credit or charge for a mid-period change.

Organization

A customer company or team that subscribes. The billing entity.

Attribute Data Type Flags Reference Example Description
organization_id BIGINT PK, Required 4100
organization_name STRING Label, Required Acme Robotics
billing_email STRING Required, PII billing@acme.example
country_code REFERENCE Required → country.country_code US Drives tax.
tax_id STRING VAT or tax number.
created_at TIMESTAMP Required 2025-04-02T09:00:00Z

Payment

A payment attempt for an invoice through the payment provider.

Attribute Data Type Flags Reference Example Description
payment_id BIGINT PK, Required 610201
invoice_id BIGINT Required 300045
provider_reference STRING Label, Required ch_3Q... Payment provider's charge ID.
amount DECIMAL(12,2) Required 324.00
succeeded BOOLEAN Required true
attempted_at TIMESTAMP Required 2026-11-01T00:05:00Z
failure_reason STRING

Plan

A product tier customers can subscribe to, such as Team or Business.

Attribute Data Type Flags Reference Example Description
plan_id INT PK, Required 3
plan_code STRING Required TEAM
plan_name STRING Label, Required Team
is_public BOOLEAN Required true Shown on the pricing page.

Price

A price for a plan in one currency and billing interval. Prices are never edited; a new price replaces an old one.

Attribute Data Type Flags Reference Example Description
price_id INT PK, Required 31
plan_id INT Required 3
currency_code REFERENCE Required → currency.currency_code USD
billing_interval_code REFERENCE Required → billing_interval.interval_code MONTH
pricing_model REFERENCE Required → pricing_model.model_code PER_SEAT
unit_amount DECIMAL(12,2) Label, Required 12.00 Price per unit (seat, unit of usage or flat).
is_active BOOLEAN Required true

Subscription

An organization's subscription. Its items say which prices it pays and in what quantity.

Attribute Data Type Flags Reference Example Description
subscription_id BIGINT PK, Required 7001
organization_id BIGINT Required 4100
subscription_status_code REFERENCE Required → subscription_status.status_code ACTIVE
started_at TIMESTAMP Label, Required 2025-04-02T09:05:00Z
current_period_start DATE Required 2026-11-01
current_period_end DATE Required 2026-11-30
trial_ends_at TIMESTAMP
cancel_at_period_end BOOLEAN Required false
canceled_at TIMESTAMP

Subscription Item

One price on a subscription with its quantity, such as 25 seats of Team monthly.

Attribute Data Type Flags Reference Example Description
subscription_item_id BIGINT PK, Required 70011
subscription_id BIGINT Required 7001
price_id INT Required 31
quantity INT Label, Required 25 Seats or units; 1 for flat and metered prices.

Usage Record

Metered usage reported against a subscription item, summed at the end of each period.

Attribute Data Type Flags Reference Example Description
usage_record_id BIGINT PK, Required 9900122
subscription_item_id BIGINT Required 70012
quantity BIGINT Label, Required 15000
recorded_at TIMESTAMP Required 2026-11-03T12:00:00Z
idempotency_key STRING Required evt_5521 Stops duplicate reports.

User

A person who signs in. Belongs to one organization; seat-based plans count active users.

Attribute Data Type Flags Reference Example Description
user_id BIGINT PK, Required 88120
organization_id BIGINT Required 4100
email STRING Required, PII dev@acme.example
full_name STRING Label, PII Robin Ito
role STRING Required ADMIN Owner, admin or member.
is_active BOOLEAN Required true

Billing Interval

How often a price is billed.

Attribute Data Type Flags Reference Example Description
interval_code STRING PK, Required MONTH
interval_name STRING Label, Required Monthly
months INT Required 1

Country

Countries, identified by their ISO 3166-1 alpha-2 code.

Attribute Data Type Flags Reference Example Description
country_code STRING PK, Required US ISO 3166-1 alpha-2 code.
country_name STRING Label, Required United States English short name.

Currency

Currencies, identified by their ISO 4217 code.

Attribute Data Type Flags Reference Example Description
currency_code STRING PK, Required USD ISO 4217 code.
currency_name STRING Label, Required US Dollar

Invoice Status

Lifecycle states of an invoice.

Attribute Data Type Flags Reference Example Description
status_code STRING PK, Required OPEN
status_name STRING Label, Required Open

Pricing Model

How a price turns into an amount.

Attribute Data Type Flags Reference Example Description
model_code STRING PK, Required PER_SEAT
model_name STRING Label, Required Per seat

Subscription Status

Lifecycle states of a subscription.

Attribute Data Type Flags Reference Example Description
status_code STRING PK, Required ACTIVE
status_name STRING Label, Required Active

Relationships

Relationship Source Target Cardinality Forward Label Reverse Label Key Description
member_of User Organization N:1 Member Of Has User app_user.organization_id
price_of Price Plan N:1 Price Of Priced As plan_price.plan_id A plan has a price per currency and interval, and keeps its old prices.
subscribed_by Subscription Organization N:1 Held By Subscribes subscription.organization_id
item_of Subscription Item Subscription N:1 Item Of Has Item subscription_item.subscription_id
item_price Subscription Item Price N:1 Pays Paid In subscription_item.price_id
usage_for Usage Record Subscription Item N:1 Reported For Has Usage usage_record.subscription_item_id
billed_to Invoice Organization N:1 Billed To Billed invoice.organization_id
bills Invoice Subscription N:1 Bills Billed On invoice.subscription_id Empty for one-off invoices.
line_on Invoice Line Invoice N:1 Line On Has Line invoice_line.invoice_id
line_for_item Invoice Line Subscription Item N:1 Charges Charged On invoice_line.subscription_item_id
pays Payment Invoice N:1 Pays Paid By payment.invoice_id

Reference data

Billing Interval

3 values · Source: Example values

interval_code interval_name months
MONTH Monthly 1
QUARTER Quarterly 3
YEAR Yearly 12

Country

6 values · Source: ISO 3166-1 (excerpt)

country_code country_name
US United States
CA Canada
GB United Kingdom
DE Germany
FR France
AU Australia

Currency

5 values · Source: ISO 4217 (excerpt)

currency_code currency_name
USD US Dollar
CAD Canadian Dollar
GBP Pound Sterling
EUR Euro
AUD Australian Dollar

Invoice Status

5 values · Source: Example values

status_code status_name
DRAFT Draft
OPEN Open
PAID Paid
VOID Void
UNCOLLECTIBLE Uncollectible

Pricing Model

4 values · Source: Example values

model_code model_name
FLAT Flat fee
PER_SEAT Per seat
METERED Metered usage
TIERED Tiered

Subscription Status

5 values · Source: Example values

status_code status_name
TRIALING Trialing
ACTIVE Active
PAST_DUE Past due
PAUSED Paused
CANCELED Canceled

SQL DDL (PostgreSQL)

Generated from the model. In the app, Export → Code also writes MySQL, Snowflake and Databricks DDL, DBML, dbt schema.yml and data contracts.

-- SaaS Subscription Billing — data model
-- PostgreSQL DDL, generated 2026-11-03
-- Made with EntityModel.com

-- Reference list: Subscription Status
CREATE TABLE subscription_status (
  status_code TEXT NOT NULL,
  status_name TEXT NOT NULL,
  PRIMARY KEY (status_code)
);
COMMENT ON TABLE subscription_status IS 'Lifecycle states of a subscription.';

-- Reference list: Invoice Status
CREATE TABLE invoice_status (
  status_code TEXT NOT NULL,
  status_name TEXT NOT NULL,
  PRIMARY KEY (status_code)
);
COMMENT ON TABLE invoice_status IS 'Lifecycle states of an invoice.';

-- Reference list: Billing Interval
CREATE TABLE billing_interval (
  interval_code TEXT NOT NULL,
  interval_name TEXT NOT NULL,
  months INTEGER NOT NULL,
  PRIMARY KEY (interval_code)
);
COMMENT ON TABLE billing_interval IS 'How often a price is billed.';

-- Reference list: Pricing Model
CREATE TABLE pricing_model (
  model_code TEXT NOT NULL,
  model_name TEXT NOT NULL,
  PRIMARY KEY (model_code)
);
COMMENT ON TABLE pricing_model IS 'How a price turns into an amount.';

-- Reference list: Country
CREATE TABLE country (
  country_code TEXT NOT NULL,
  country_name TEXT NOT NULL,
  PRIMARY KEY (country_code)
);
COMMENT ON TABLE country IS 'Countries, identified by their ISO 3166-1 alpha-2 code.';
COMMENT ON COLUMN country.country_code IS 'ISO 3166-1 alpha-2 code.';
COMMENT ON COLUMN country.country_name IS 'English short name.';

-- Reference list: Currency
CREATE TABLE currency (
  currency_code TEXT NOT NULL,
  currency_name TEXT NOT NULL,
  PRIMARY KEY (currency_code)
);
COMMENT ON TABLE currency IS 'Currencies, identified by their ISO 4217 code.';
COMMENT ON COLUMN currency.currency_code IS 'ISO 4217 code.';

CREATE TABLE organization (
  organization_id BIGINT NOT NULL,
  organization_name TEXT NOT NULL,
  billing_email TEXT NOT NULL,
  country_code TEXT NOT NULL,
  tax_id TEXT,
  created_at TIMESTAMP NOT NULL,
  PRIMARY KEY (organization_id)
);
COMMENT ON TABLE organization IS 'A customer company or team that subscribes. The billing entity.';
COMMENT ON COLUMN organization.country_code IS 'Drives tax.';
COMMENT ON COLUMN organization.tax_id IS 'VAT or tax number.';

CREATE TABLE app_user (
  user_id BIGINT NOT NULL,
  organization_id BIGINT NOT NULL,
  email TEXT NOT NULL,
  full_name TEXT,
  role TEXT NOT NULL,
  is_active BOOLEAN NOT NULL,
  PRIMARY KEY (user_id)
);
COMMENT ON TABLE app_user IS 'A person who signs in. Belongs to one organization; seat-based plans count active users.';
COMMENT ON COLUMN app_user.role IS 'Owner, admin or member.';

CREATE TABLE plan (
  plan_id INTEGER NOT NULL,
  plan_code TEXT NOT NULL,
  plan_name TEXT NOT NULL,
  is_public BOOLEAN NOT NULL,
  PRIMARY KEY (plan_id)
);
COMMENT ON TABLE plan IS 'A product tier customers can subscribe to, such as Team or Business.';
COMMENT ON COLUMN plan.is_public IS 'Shown on the pricing page.';

CREATE TABLE plan_price (
  price_id INTEGER NOT NULL,
  plan_id INTEGER NOT NULL,
  currency_code TEXT NOT NULL,
  billing_interval_code TEXT NOT NULL,
  pricing_model TEXT NOT NULL,
  unit_amount NUMERIC(12,2) NOT NULL,
  is_active BOOLEAN NOT NULL,
  PRIMARY KEY (price_id)
);
COMMENT ON TABLE plan_price IS 'A price for a plan in one currency and billing interval. Prices are never edited; a new price replaces an old one.';
COMMENT ON COLUMN plan_price.unit_amount IS 'Price per unit (seat, unit of usage or flat).';

CREATE TABLE subscription (
  subscription_id BIGINT NOT NULL,
  organization_id BIGINT NOT NULL,
  subscription_status_code TEXT NOT NULL,
  started_at TIMESTAMP NOT NULL,
  current_period_start DATE NOT NULL,
  current_period_end DATE NOT NULL,
  trial_ends_at TIMESTAMP,
  cancel_at_period_end BOOLEAN NOT NULL,
  canceled_at TIMESTAMP,
  PRIMARY KEY (subscription_id)
);
COMMENT ON TABLE subscription IS 'An organization''s subscription. Its items say which prices it pays and in what quantity.';

CREATE TABLE subscription_item (
  subscription_item_id BIGINT NOT NULL,
  subscription_id BIGINT NOT NULL,
  price_id INTEGER NOT NULL,
  quantity INTEGER NOT NULL,
  PRIMARY KEY (subscription_item_id)
);
COMMENT ON TABLE subscription_item IS 'One price on a subscription with its quantity, such as 25 seats of Team monthly.';
COMMENT ON COLUMN subscription_item.quantity IS 'Seats or units; 1 for flat and metered prices.';

CREATE TABLE usage_record (
  usage_record_id BIGINT NOT NULL,
  subscription_item_id BIGINT NOT NULL,
  quantity BIGINT NOT NULL,
  recorded_at TIMESTAMP NOT NULL,
  idempotency_key TEXT NOT NULL,
  PRIMARY KEY (usage_record_id)
);
COMMENT ON TABLE usage_record IS 'Metered usage reported against a subscription item, summed at the end of each period.';
COMMENT ON COLUMN usage_record.idempotency_key IS 'Stops duplicate reports.';

CREATE TABLE invoice (
  invoice_id BIGINT NOT NULL,
  invoice_number TEXT NOT NULL,
  organization_id BIGINT NOT NULL,
  subscription_id BIGINT,
  invoice_status_code TEXT NOT NULL,
  currency_code TEXT NOT NULL,
  period_start DATE,
  period_end DATE,
  subtotal_amount NUMERIC(12,2) NOT NULL,
  tax_amount NUMERIC(12,2) NOT NULL,
  total_amount NUMERIC(12,2) NOT NULL,
  issued_at TIMESTAMP NOT NULL,
  due_at TIMESTAMP NOT NULL,
  PRIMARY KEY (invoice_id)
);
COMMENT ON TABLE invoice IS 'A bill for one billing period of a subscription, or a one-off charge.';

CREATE TABLE invoice_line (
  invoice_line_id BIGINT NOT NULL,
  invoice_id BIGINT NOT NULL,
  subscription_item_id BIGINT,
  description TEXT NOT NULL,
  quantity BIGINT NOT NULL,
  unit_amount NUMERIC(12,2) NOT NULL,
  amount NUMERIC(12,2) NOT NULL,
  is_proration BOOLEAN NOT NULL,
  PRIMARY KEY (invoice_line_id)
);
COMMENT ON TABLE invoice_line IS 'A charge on an invoice: a subscription item for the period, prorations, or usage.';
COMMENT ON COLUMN invoice_line.is_proration IS 'Credit or charge for a mid-period change.';

CREATE TABLE payment (
  payment_id BIGINT NOT NULL,
  invoice_id BIGINT NOT NULL,
  provider_reference TEXT NOT NULL,
  amount NUMERIC(12,2) NOT NULL,
  succeeded BOOLEAN NOT NULL,
  attempted_at TIMESTAMP NOT NULL,
  failure_reason TEXT,
  PRIMARY KEY (payment_id)
);
COMMENT ON TABLE payment IS 'A payment attempt for an invoice through the payment provider.';
COMMENT ON COLUMN payment.provider_reference IS 'Payment provider''s charge ID.';

-- Keys between tables
ALTER TABLE app_user ADD CONSTRAINT uq_app_user_email UNIQUE (email);
ALTER TABLE plan ADD CONSTRAINT uq_plan_plan_code UNIQUE (plan_code);
ALTER TABLE usage_record ADD CONSTRAINT uq_usage_record_idempotency_key UNIQUE (idempotency_key);
ALTER TABLE invoice ADD CONSTRAINT uq_invoice_invoice_number UNIQUE (invoice_number);
ALTER TABLE organization ADD CONSTRAINT fk_organization_country_code FOREIGN KEY (country_code) REFERENCES country (country_code);
ALTER TABLE plan_price ADD CONSTRAINT fk_plan_price_currency_code FOREIGN KEY (currency_code) REFERENCES currency (currency_code);
ALTER TABLE plan_price ADD CONSTRAINT fk_plan_price_billing_interval_code FOREIGN KEY (billing_interval_code) REFERENCES billing_interval (interval_code);
ALTER TABLE plan_price ADD CONSTRAINT fk_plan_price_pricing_model FOREIGN KEY (pricing_model) REFERENCES pricing_model (model_code);
ALTER TABLE subscription ADD CONSTRAINT fk_subscription_subscription_status_code FOREIGN KEY (subscription_status_code) REFERENCES subscription_status (status_code);
ALTER TABLE invoice ADD CONSTRAINT fk_invoice_invoice_status_code FOREIGN KEY (invoice_status_code) REFERENCES invoice_status (status_code);
ALTER TABLE invoice ADD CONSTRAINT fk_invoice_currency_code FOREIGN KEY (currency_code) REFERENCES currency (currency_code);
ALTER TABLE app_user ADD CONSTRAINT fk_member_of FOREIGN KEY (organization_id) REFERENCES organization (organization_id);
ALTER TABLE plan_price ADD CONSTRAINT fk_price_of FOREIGN KEY (plan_id) REFERENCES plan (plan_id);
ALTER TABLE subscription ADD CONSTRAINT fk_subscribed_by FOREIGN KEY (organization_id) REFERENCES organization (organization_id);
ALTER TABLE subscription_item ADD CONSTRAINT fk_item_of FOREIGN KEY (subscription_id) REFERENCES subscription (subscription_id);
ALTER TABLE subscription_item ADD CONSTRAINT fk_item_price FOREIGN KEY (price_id) REFERENCES plan_price (price_id);
ALTER TABLE usage_record ADD CONSTRAINT fk_usage_for FOREIGN KEY (subscription_item_id) REFERENCES subscription_item (subscription_item_id);
ALTER TABLE invoice ADD CONSTRAINT fk_billed_to FOREIGN KEY (organization_id) REFERENCES organization (organization_id);
ALTER TABLE invoice ADD CONSTRAINT fk_bills FOREIGN KEY (subscription_id) REFERENCES subscription (subscription_id);
ALTER TABLE invoice_line ADD CONSTRAINT fk_line_on FOREIGN KEY (invoice_id) REFERENCES invoice (invoice_id);
ALTER TABLE invoice_line ADD CONSTRAINT fk_line_for_item FOREIGN KEY (subscription_item_id) REFERENCES subscription_item (subscription_item_id);
ALTER TABLE payment ADD CONSTRAINT fk_pays FOREIGN KEY (invoice_id) REFERENCES invoice (invoice_id);

-- Reference values
INSERT INTO subscription_status (status_code, status_name) VALUES
  ('TRIALING', 'Trialing'),
  ('ACTIVE', 'Active'),
  ('PAST_DUE', 'Past due'),
  ('PAUSED', 'Paused'),
  ('CANCELED', 'Canceled');

INSERT INTO invoice_status (status_code, status_name) VALUES
  ('DRAFT', 'Draft'),
  ('OPEN', 'Open'),
  ('PAID', 'Paid'),
  ('VOID', 'Void'),
  ('UNCOLLECTIBLE', 'Uncollectible');

INSERT INTO billing_interval (interval_code, interval_name, months) VALUES
  ('MONTH', 'Monthly', 1),
  ('QUARTER', 'Quarterly', 3),
  ('YEAR', 'Yearly', 12);

INSERT INTO pricing_model (model_code, model_name) VALUES
  ('FLAT', 'Flat fee'),
  ('PER_SEAT', 'Per seat'),
  ('METERED', 'Metered usage'),
  ('TIERED', 'Tiered');

INSERT INTO country (country_code, country_name) VALUES
  ('US', 'United States'),
  ('CA', 'Canada'),
  ('GB', 'United Kingdom'),
  ('DE', 'Germany'),
  ('FR', 'France'),
  ('AU', 'Australia');

INSERT INTO currency (currency_code, currency_name) VALUES
  ('USD', 'US Dollar'),
  ('CAD', 'Canadian Dollar'),
  ('GBP', 'Pound Sterling'),
  ('EUR', 'Euro'),
  ('AUD', 'Australian Dollar');

Mermaid

Paste into a mermaid code block on GitHub, GitLab or Notion to show the diagram there.

---
title: SaaS Subscription Billing
---
%% Made with EntityModel.com
erDiagram
  organization {
    bigint organization_id PK "Organization ID"
    string organization_name "Organization Name"
    string billing_email "Billing Email"
    string country_code FK "Drives tax."
    string tax_id "VAT or tax number."
    timestamp created_at "Created At"
  }
  app_user {
    bigint user_id PK "User ID"
    bigint organization_id FK "Organization ID"
    string email "Email"
    string full_name "Full Name"
    string role "Owner, admin or member."
    boolean is_active "Is Active"
  }
  plan {
    int plan_id PK "Plan ID"
    string plan_code "Plan Code"
    string plan_name "Plan Name"
    boolean is_public "Shown on the pricing page."
  }
  plan_price {
    int price_id PK "Price ID"
    int plan_id FK "Plan ID"
    string currency_code FK "Currency Code"
    string billing_interval_code FK "Billing Interval Code"
    string pricing_model FK "Pricing Model"
    decimal unit_amount "Price per unit (seat, unit of usage or flat)."
    boolean is_active "Is Active"
  }
  subscription {
    bigint subscription_id PK "Subscription ID"
    bigint organization_id FK "Organization ID"
    string subscription_status_code FK "Subscription Status Code"
    timestamp started_at "Started At"
    date current_period_start "Current Period Start"
    date current_period_end "Current Period End"
    timestamp trial_ends_at "Trial Ends At"
    boolean cancel_at_period_end "Cancel At Period End"
    timestamp canceled_at "Canceled At"
  }
  subscription_item {
    bigint subscription_item_id PK "Subscription Item ID"
    bigint subscription_id FK "Subscription ID"
    int price_id FK "Price ID"
    int quantity "Seats or units; 1 for flat and metered prices."
  }
  usage_record {
    bigint usage_record_id PK "Usage Record ID"
    bigint subscription_item_id FK "Subscription Item ID"
    bigint quantity "Quantity"
    timestamp recorded_at "Recorded At"
    string idempotency_key "Stops duplicate reports."
  }
  invoice {
    bigint invoice_id PK "Invoice ID"
    string invoice_number "Invoice Number"
    bigint organization_id FK "Organization ID"
    bigint subscription_id FK "Subscription ID"
    string invoice_status_code FK "Invoice Status Code"
    string currency_code FK "Currency Code"
    date period_start "Period Start"
    date period_end "Period End"
    decimal subtotal_amount "Subtotal Amount"
    decimal tax_amount "Tax Amount"
    decimal total_amount "Total Amount"
    timestamp issued_at "Issued At"
    timestamp due_at "Due At"
  }
  invoice_line {
    bigint invoice_line_id PK "Invoice Line ID"
    bigint invoice_id FK "Invoice ID"
    bigint subscription_item_id FK "Subscription Item ID"
    string description "Description"
    bigint quantity "Quantity"
    decimal unit_amount "Unit Amount"
    decimal amount "Amount"
    boolean is_proration "Credit or charge for a mid-period change."
  }
  payment {
    bigint payment_id PK "Payment ID"
    bigint invoice_id FK "Invoice ID"
    string provider_reference "Payment provider's charge ID."
    decimal amount "Amount"
    boolean succeeded "Succeeded"
    timestamp attempted_at "Attempted At"
    string failure_reason "Failure Reason"
  }
  subscription_status {
    string status_code PK "Status Code"
    string status_name "Status Name"
  }
  invoice_status {
    string status_code PK "Status Code"
    string status_name "Status Name"
  }
  billing_interval {
    string interval_code PK "Interval Code"
    string interval_name "Interval Name"
    int months "Months"
  }
  pricing_model {
    string model_code PK "Model Code"
    string model_name "Model Name"
  }
  country {
    string country_code PK "ISO 3166-1 alpha-2 code."
    string country_name "English short name."
  }
  currency {
    string currency_code PK "ISO 4217 code."
    string currency_name "Currency Name"
  }
  app_user }o--|| organization : "Member Of"
  plan_price }o--|| plan : "Price Of"
  subscription }o--|| organization : "Held By"
  subscription_item }o--|| subscription : "Item Of"
  subscription_item }o--|| plan_price : "Pays"
  usage_record }o--|| subscription_item : "Reported For"
  invoice }o--|| organization : "Billed To"
  invoice }o--|| subscription : "Bills"
  invoice_line }o--|| invoice : "Line On"
  invoice_line }o--|| subscription_item : "Charges"
  payment }o--|| invoice : "Pays"
  organization }o..|| country : "Country Code"
  plan_price }o..|| currency : "Currency Code"
  plan_price }o..|| billing_interval : "Billing Interval Code"
  plan_price }o..|| pricing_model : "Pricing Model"
  subscription }o..|| subscription_status : "Subscription Status Code"
  invoice }o..|| invoice_status : "Invoice Status Code"
  invoice }o..|| currency : "Currency Code"

Frequently asked questions

How do you model subscriptions in a database?

Separate the catalog (plan, price) from what a customer holds (subscription and subscription items with quantities). Generate an invoice per billing period from the items, with invoice lines that copy the amounts charged, and record payments against invoices.

How should SaaS pricing changes be stored?

Never update a price in place. Add a new price row and point new or migrated subscription items at it, keeping the old one for history. Invoices store the amounts they charged, so past bills never change.