SaaS Data Model
Accounts move from marketing and trials through subscriptions and seat expansion, with usage, support and billing signals deciding who stays, and every plan change captured as an MRR movement.
A B2B subscription software business modeled end to end — from the marketing that brings accounts in, through trials, subscriptions and seat expansion, to the product usage, support experience and billing that decide whether they stay. Recurring revenue is the spine: every plan change flows through an MRR movement (new, expansion, contraction, churn), while engagement, support and payment health all feed the retention story.
Example Questions
- What is net revenue retention, and how much of it is expansion versus what contraction and churn take back?
- Which acquisition channels pay back their cost fastest once you account for the revenue those
accountsactually retain? - Do
accountswith deeperproduct usageand bettersupport experiencesexpand more and churn less?
Account
Every customer account (a company), with the signals that frame the whole relationship: industry, size and region, the plan tier they are on, a revenue size band, the channel that acquired them, their success-manager owner, a product-health score, and where they sit in the lifecycle (trial, active, at-risk, churned). The dimension you slice the entire business by.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
account_id | STRING | Account ID | PK. Unique account identifier. |
name | STRING | Name | Company/account name. |
industry | STRING | Industry | Industry vertical of the account. |
employee_band | STRING | Employee Band | Company-size bucket by headcount. |
plan_tier | STRING | Plan Tier | Subscription plan tier. |
mrr_band | STRING | MRR Band | Monthly-recurring-revenue size bucket. |
region | STRING | Region | Sales/geographic region. |
acquisition_channel | STRING | Acquisition Channel | Marketing channel that sourced the account — blended-CAC join key. |
signup_date | DATE | Signup Date | Date the account first signed up. |
csm_owner | STRING | CSM Owner | Customer success manager who owns the account. |
health_score | INTEGER | Health Score | 0–100 product-health composite. |
lifecycle_stage | STRING | Lifecycle Stage | trial / active / at-risk / churned. |
Invoices
Every invoice and how it was paid — amount, tax, discounts and credits, payment status, and where it sits in the dunning cycle when a payment fails. This is where voluntary revenue meets involuntary churn: failed payments and collections stages that quietly erode the customer base.
This is the model's revenue mart, and it carries two figures on purpose: amount is what was billed on every invoice regardless of outcome, and net_amount is what was actually recognised — zero for a voided or written-off invoice, equal to amount otherwise. Any revenue total should sum net_amount, never amount. The four collection signals — status, dunning_stage, is_failed and paid_at — always agree with each other: a paid invoice always has a paid_at and never is_failed or a write-off, and is_write_off is exactly dunning_stage = 'write_off'.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
invoice_id | STRING | Invoice ID | PK. Unique invoice identifier. |
account_id | STRING | Account ID | Account billed. FK to Account |
subscription_id | STRING | Subscription ID | Subscription being billed. FK to Subscription |
issued_at | DATE | Issued At | Date the invoice was issued. |
period_start | DATE | Period Start | Start of the billing period. |
period_end | DATE | Period End | End of the billing period. |
amount | NUMERIC | Amount | Invoice amount before tax. Booked on every status, including void and write-off — see net_amount for the recognised figure. |
net_amount | NUMERIC | Net Amount | Recognised revenue: 0 when the invoice is void or written off, otherwise equal to amount. Sum this for revenue totals, not amount. |
tax | NUMERIC | Tax | Tax charged on the invoice. |
status | STRING | Status | Payment status of the invoice. |
currency | STRING | Currency | Invoice currency, aligned to the account's region. |
discount_amount | NUMERIC | Discount Amount | Discount applied to the invoice, if any. Always a fraction of amount, never larger than it. |
credit_applied | NUMERIC | Credit Applied | Account credit applied to the invoice, if any. |
dunning_stage | STRING | Dunning Stage | Collections stage: none / retry_1 / retry_2 / final_notice / write_off. |
paid_at | DATE | Paid At | Date the invoice was paid. Set if and only if status = 'paid'. |
is_failed | BOOLEAN | Is Failed | Failed payment — involuntary-churn signal. Never true on a paid invoice. |
is_write_off | BOOLEAN | Is Write Off | Uncollectible invoice, exactly dunning_stage = 'write_off'. Use this to gate money rather than string-matching the dunning label. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account billed on this invoice. |
| Subscription | subscription_id = subscription_id | N:1 | The subscription this invoice bills. |
Marketing Spend
Marketing investment by channel, campaign and day — cost, leads, and signups — attributed to the cohort of accounts each channel brings in rather than to individual accounts. The numerator and denominator of customer acquisition cost, and the starting point for payback analysis.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
spend_id | STRING | Spend ID | PK. Unique identifier for each spend record. |
spend_date | DATE | Spend Date | Day the spend was incurred. |
channel | STRING | Channel | Acquisition channel the spend targeted — attributed to the cohort of accounts acquired through it, not to individual accounts. FK to Account |
campaign | STRING | Campaign | Campaign the spend belongs to. |
cost | NUMERIC | Cost | Money spent — the CAC numerator. |
leads | INTEGER | Leads | Leads generated by the spend. |
signups | INTEGER | Signups | Accounts created — CAC denominator. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | channel = acquisition_channel | N:N | Accounts acquired through this channel — a cohort, not one account per row. |
Plan
The price book: every sellable plan and price point — four tiers (Starter, Pro, Business, Enterprise), each offered monthly or annually, with its list price and whether it is currently sellable. The reference that gives every subscription its list pricing.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
plan_id | STRING | Plan ID | PK. Unique plan/price-point identifier. |
plan_name | STRING | Plan Name | Human-readable plan name (tier + billing interval). |
tier | STRING | Tier | Product tier: Starter / Pro / Business / Enterprise. |
billing_interval | STRING | Billing Interval | Billing cadence: monthly / annual. |
list_price | NUMERIC | List Price | Published list price for this plan at this interval. |
currency | STRING | Currency | Currency of the list price. |
is_active | BOOLEAN | Is Active | Whether the plan is currently sellable. |
Subscription
The billing spine: one row per subscription showing which account is on which plan, at what monthly recurring revenue and seat count, on what cadence, and with any negotiated discount. It also reveals where a subscription's plan has drifted from the account's current tier under grandfathered or discounted pricing. The source of truth for what each customer pays today.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
subscription_id | STRING | Subscription ID | PK. Unique subscription identifier. |
account_id | STRING | Account ID | Account that owns the subscription. FK to Account |
plan_id | STRING | Plan ID | Plan the subscription is billed on. May differ from the account's current tier under grandfathered pricing or a negotiated discount. FK to Plan |
status | STRING | Status | Subscription status: active / past_due / canceled. |
seats_licensed | INTEGER | Seats Licensed | Number of seats licensed on the subscription. |
mrr | NUMERIC | MRR | Contracted monthly recurring revenue — the rate on the agreement, unchanged by cancellation. |
active_mrr | NUMERIC | Active MRR | Live monthly recurring revenue: still contracted unless canceled, when it drops to 0. Equal to mrr for active and past_due subscriptions alike — a failed payment does not by itself stop the contract. |
currency | STRING | Currency | Billing currency, aligned to the account's region. |
billing_interval | STRING | Billing Interval | Billing cadence: monthly / annual. |
started_at | DATE | Started At | Date the subscription started. |
current_period_start | DATE | Current Period Start | Start of the current billing period. |
current_period_end | DATE | Current Period End | End of the current billing period. |
canceled_at | DATE | Canceled At | Date the subscription was canceled, if it was. |
discount_pct | NUMERIC | Discount % | Negotiated discount fraction applied to list price, if any. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account paying for this subscription. |
| Plan | plan_id = plan_id | N:1 | The plan this subscription is billed on. |
Subscription Events
The movement history behind recurring revenue: one row per subscription change — new, expansion, contraction, reactivation, or churn — with the signed revenue and seat deltas and the running revenue after each change. This is what reconstructs the revenue waterfall and the retention rates the business lives or dies by.
Running revenue reconciles with each subscription's own contracted rate. A subscription still on the books finishes the series on that rate; a subscription that churned was carrying it in the period immediately before the churn and the series steps away from it — so a churn reads as revenue lost from a customer's full rate, not as a downgrade on the way up to it.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
event_id | STRING | Event ID | PK. Unique subscription-event identifier. |
account_id | STRING | Account ID | Account the event belongs to. FK to Account |
subscription_id | STRING | Subscription ID | Subscription the event belongs to. FK to Subscription |
event_ts | TIMESTAMP | Event Time | When the subscription change occurred. |
event_type | STRING | Event Type | MRR-movement type: new / expansion / contraction / reactivation / churn. |
plan_from | STRING | Plan From | Plan before the change. |
plan_to | STRING | Plan To | Plan after the change. |
mrr_delta | NUMERIC | MRR Delta | Signed MRR change — the MRR-movement waterfall. Always the step between two consecutive levels, so the deltas of a subscription add up to the movement in its recurring revenue over the period. |
seats_delta | INTEGER | Seats Delta | Signed change in seat count. |
mrr_after | NUMERIC | MRR After | Total MRR after the change, reconciled with the subscription's contracted rate: a subscription with no churn in the period ends on that rate, and one that churned was carrying it immediately before the churn. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account whose recurring revenue moved. |
| Subscription | subscription_id = subscription_id | N:1 | The subscription this change was made to. |
Support Tickets
Every support interaction — priority, topic, satisfaction score, time to first response, and resolution time. Support experience is an early churn-risk signal: unhappy, slow-to-resolve accounts are the ones that quietly leave.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
ticket_id | STRING | Ticket ID | PK. Unique support-ticket identifier. |
account_id | STRING | Account ID | Account that opened the ticket. FK to Account |
user_id | STRING | User ID | User who opened the ticket, if attributable. FK to User |
opened_at | TIMESTAMP | Opened At | When the ticket was opened. |
closed_at | TIMESTAMP | Closed At | When the ticket was closed. |
priority | STRING | Priority | Ticket priority level. |
category | STRING | Category | Ticket topic/category. |
csat_score | INTEGER | Csat Score | Customer satisfaction rating for the ticket. |
first_response_mins | INTEGER | First Response Mins | Minutes to first agent response. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account that opened the ticket. |
| User | user_id = user_id | N:1 | The seat that opened the ticket, when known. |
Trials
Every trial and how it ended — converted to paid or not — with when it started and expired, where it came from (self-serve, sales-assisted, product-led upsell), and the plan it was evaluating. The top of the funnel for new recurring revenue.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
trial_id | STRING | Trial ID | PK. Unique trial identifier. |
account_id | STRING | Account ID | Account running the trial. FK to Account |
started_at | TIMESTAMP | Started At | When the trial began. |
ends_at | TIMESTAMP | Ends At | Scheduled trial expiry. |
converted_at | TIMESTAMP | Converted At | When the trial converted to a paid plan, if it did. |
is_converted | BOOLEAN | Is Converted | Trial-to-paid outcome flag. |
trial_source | STRING | Trial Source | Where the trial came from (self-serve, sales-assisted, PLG upsell). |
requested_plan | STRING | Requested Plan | Plan tier the trial is evaluating. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account that ran the trial. |
Usage (daily)
Daily product engagement at the account-and-user level: active minutes, high-value actions taken, and how many distinct features were touched — the breadth signal for activation. The behavioral pulse that explains why accounts expand, stall, or churn.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
usage_id | STRING | Usage ID | PK. Unique daily-usage record identifier. |
account_id | STRING | Account ID | Account that generated the usage. FK to Account |
user_id | STRING | User ID | User that generated the usage. FK to User |
usage_date | DATE | Usage Date | Calendar day of the usage. |
active_minutes | INTEGER | Active Minutes | Minutes the user was active in-product. |
key_actions | INTEGER | Key Actions | Count of high-value actions taken. |
distinct_features_used | INTEGER | Distinct Features Used | Count of distinct product features touched that day — activation breadth. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account that generated the usage. |
| User | user_id = user_id | N:1 | The seat that generated the usage. |
User
The people inside each account: one row per user seat, with role, seat type, when they were invited, and how recently they were active. Seat-level activity is what turns a licensed account into an adopted one — and idle seats are early signs of shrinking value.
Fields
| Column | Type | Alias | Description |
|---|---|---|---|
user_id | STRING | User ID | PK. Unique user identifier. |
account_id | STRING | Account ID | Owning account. FK to Account |
email | STRING | User's email address. | |
role | STRING | Role | User's role within the account. |
seat_type | STRING | Seat Type | Type of seat assigned (e.g. full / viewer). |
invited_at | TIMESTAMP | Invited At | When the user was invited. |
last_active_at | TIMESTAMP | Last Active At | Most recent activity timestamp. |
is_active | BOOLEAN | Is Active | Whether the seat is currently active. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account a seat belongs to. |
- 1
Install the Import Model plugin
One plugin, installed once, in your own OWOX workspace.
- 2
Import this model
Point it at this bundle and it creates every data mart above, joins and all.
- 3
Plug in your data and destinations
Connect your own sources and send the results where your team already works.
4. Optional — customize as you wish. Rename a column, drop a mart, add your own: once it is imported it is yours, and nothing here syncs back.