Semantius Logo

SaaS Expense Tracker & Budget — Semantic Model

1. Overview

An internal SaaS spend management system that records the company’s SaaS subscriptions (the app, the vendor, the commercial terms, and the contract details on a single record), the departments that own the spend, the budgets planned against them, and which internal users consume seats. Finance and IT use it to track planned vs. expected spend, allocate costs to departments, detect unused licenses, and manage upcoming renewals.

2. Entity summary

#Table nameSingular labelPurpose
1vendorsVendorThe company that sells a SaaS product (e.g. Slack Technologies, Atlassian)
2subscriptionsSubscriptionA SaaS subscription we pay for — combines the product, commercial terms (seats, price, cadence, dates), and contract details on one record
3departmentsDepartmentCost center / org unit that owns part of the spend
4budget_periodsBudget PeriodA fiscal year or quarter container for budgeting
5budget_linesBudget LinePlanned spend allocated to a department / subscription for a budget period
6license_assignmentsLicense AssignmentWhich internal user is consuming a seat on which subscription
7usersUserInternal employee — subscription owner, license holder, budget owner, approver

Entity-relationship diagram

flowchart LR
    vendors -->|sells| subscriptions
    users -->|owns| subscriptions
    users -->|signs| subscriptions
    departments -->|funds| subscriptions
    users -->|manages| departments
    departments -->|parent of| departments
    departments -->|employs| users
    budget_periods -->|contains| budget_lines
    departments -->|budgeted in| budget_lines
    subscriptions -->|budgeted in| budget_lines
    subscriptions -->|granted via| license_assignments
    users -->|holds| license_assignments

3. Entities

3.1 vendors — Vendor

Plural label: Vendors Label column: vendor_name (the human-identifying field; auto-wired by Semantius) Description: A company that sells one or more SaaS products we pay for. Created when we onboard a new supplier.

Fields

Field nameFormatRequiredLabelReference / Notes
vendor_namestringyesVendor Namelabel_column; unique
website_urlurlnoWebsite
support_emailemailnoSupport Email
billing_contact_emailemailnoBilling Contact
tax_idstringnoTax IDVAT / EIN
notestextnoNotes

Do not include id, created_at, updated_at, or the auto-generated label field — Semantius creates these automatically.

Relationships

  • A vendor may sell many subscriptions (1:N, via subscriptions.vendor_id).

3.2 subscriptions — Subscription

Plural label: Subscriptions Label column: subscription_name (the human-identifying field; auto-wired by Semantius) Description: A SaaS subscription we pay for. Each record represents one product-commercial pairing: which app, from which vendor, on what terms (seats, price, cadence, dates), under which contract. Created when a subscription starts; superseded when renewed.

Fields

Field nameFormatRequiredLabelReference / Notes
subscription_namestringyesSubscription Namelabel_column; e.g. “Slack Business+ — Engineering”
vendor_idreferenceyesVendorvendors (N:1, restrict)
business_owner_idreferencenoBusiness Ownerusers (N:1, clear); internal owner responsible for the app
primary_department_idreferencenoOwning Departmentdepartments (N:1, clear)
signatory_user_idreferencenoInternal Signatoryusers (N:1, clear); signed the contract
categoryenumnoCategoryvalues listed in §5.1
criticalityenumnoCriticalityvalues listed in §5.2
descriptiontextnoDescription
website_urlurlnoProduct Website
billing_cycleenumyesBilling Cyclevalues listed in §5.3
seat_countintegernoSeat Count
unit_pricefloatnoUnit Priceprice per seat per billing cycle
recurring_amountfloatyesRecurring Amounttotal per billing cycle
currencystringyesCurrencyISO 4217 (e.g. USD, EUR)
start_datedateyesStart Date
end_datedatenoEnd Date
auto_renewbooleannoAuto-Renew
payment_methodenumnoPayment Methodvalues listed in §5.4
payment_termsenumnoPayment Termsvalues listed in §5.5
contract_numberstringnoContract Numberfrom the signed agreement, if any
signed_datedatenoContract Signed Date
total_contract_valuefloatnoTotal Contract Valuewhole-contract value if multi-period
renewal_notice_daysintegernoRenewal Notice Daysdays before end_date to give notice
negotiated_savingsfloatnoNegotiated Savingsvs list price
document_urlurlnoContract Documentsigned PDF link
statusenumyesStatusvalues listed in §5.6
notestextnoNotes

Relationships

  • A subscription belongs to one vendor (N:1, required, delete: restrict).
  • A subscription may have one business_owner user (N:1, optional, delete: clear).
  • A subscription may have one primary_department (N:1, optional, delete: clear).
  • A subscription may have one signatory_user (N:1, optional, delete: clear).
  • A subscription may appear on many budget_lines (1:N, via budget_lines.subscription_id).
  • subscriptionuser is many-to-many through the license_assignments junction.

3.3 departments — Department

Plural label: Departments Label column: department_name (the human-identifying field; auto-wired by Semantius) Description: A cost center or organizational unit against which spend is allocated and budgets are set. Self-referencing for hierarchy.

Fields

Field nameFormatRequiredLabelReference / Notes
department_namestringyesDepartment Namelabel_column
department_codestringnoCodeunique; e.g. “ENG”, “MKT”
manager_user_idreferencenoManagerusers (N:1, clear)
parent_department_idreferencenoParent Departmentdepartments (N:1, clear); self-ref for hierarchy
statusenumyesStatusvalues listed in §5.7

Relationships

  • A department may have one manager user (N:1, optional, delete: clear).
  • A department may have one parent_department (N:1, optional, delete: clear — self-reference for hierarchy).
  • A department may have many child departments (1:N, via self-reference).
  • A department may employ many users (1:N, via users.department_id).
  • A department may fund many subscriptions (1:N, via subscriptions.primary_department_id).
  • A department may have many budget_lines allocated to it (1:N, via budget_lines.department_id).

3.4 budget_periods — Budget Period

Plural label: Budget Periods Label column: period_name (the human-identifying field; auto-wired by Semantius) Description: A time container (fiscal year, quarter, or custom range) inside which budgets are planned and tracked. Created at planning time.

Fields

Field nameFormatRequiredLabelReference / Notes
period_namestringyesPeriod Namelabel_column; e.g. “FY2026”, “Q1 2026”
period_typeenumyesPeriod Typevalues listed in §5.8
start_datedateyesStart Date
end_datedateyesEnd Date
currencystringyesCurrencyISO 4217; reporting currency for this period
statusenumyesStatusvalues listed in §5.9

Relationships

  • A budget_period may contain many budget_lines (1:N, parent, cascade on delete).

3.5 budget_lines — Budget Line

Plural label: Budget Lines Label column: budget_line_name (the human-identifying field; auto-wired by Semantius) Description: A single planned spend allocation within a budget period. Typically allocates an amount to a department, a subscription, or a category combination. Created in the context of a budget period.

Fields

Field nameFormatRequiredLabelReference / Notes
budget_line_namestringyesBudget Line Namelabel_column; e.g. “Engineering — Dev Tools — FY2026”
budget_period_idparentyesBudget Periodbudget_periods (N:1, cascade)
department_idreferencenoDepartmentdepartments (N:1, clear)
subscription_idreferencenoSubscriptionsubscriptions (N:1, clear); null if allocated at category level
categoryenumnoCategoryvalues listed in §5.10
planned_amountfloatyesPlanned Amount
currencystringyesCurrencyISO 4217
notestextnoNotes

Relationships

  • A budget_line belongs to one budget_period (N:1, required, delete: cascade — parent).
  • A budget_line may link to one department (N:1, optional, delete: clear).
  • A budget_line may link to one subscription (N:1, optional, delete: clear).

3.6 license_assignments — License Assignment

Plural label: License Assignments Label column: assignment_label (the human-identifying field; auto-wired by Semantius) Description: Junction record showing that a specific user is consuming a seat of a specific subscription. Used for per-seat chargeback and unused-license detection. Caller populates assignment_label on create (e.g. "{user.full_name} / {subscription.subscription_name}") because the junction has no natural string key.

Fields

Field nameFormatRequiredLabelReference / Notes
assignment_labelstringyesAssignmentlabel_column; caller-populated scalar (must not be a FK per Semantius label rules)
subscription_idparentyesSubscriptionsubscriptions (N:1, cascade)
user_idparentyesUserusers (N:1, cascade)
assigned_datedatenoAssigned Date
last_active_datedatenoLast Activefor unused-license detection
monthly_cost_allocationfloatnoMonthly Cost Allocationper-seat chargeback
statusenumyesStatusvalues listed in §5.11

Relationships

  • A license_assignment belongs to one subscription (N:1, required, delete: cascade — parent).
  • A license_assignment belongs to one user (N:1, required, delete: cascade — parent).
  • Together the two parent FKs form the M:N junction between subscriptions and users.

3.7 users — User

Plural label: Users Label column: full_name (the human-identifying field; auto-wired by Semantius) Description: An internal employee who may own subscriptions, sign contracts, manage departments, or hold license assignments. The table_name matches the Semantius built-in users table so the downstream implementer can deduplicate — fields here describe the domain-required shape; the implementer reconciles with built-in fields and adds only what’s missing.

Fields

Field nameFormatRequiredLabelReference / Notes
full_namestringyesFull Namelabel_column
emailemailyesEmailunique
department_idreferencenoDepartmentdepartments (N:1, clear)
job_titlestringnoJob Title
employee_idstringnoEmployee IDunique
statusenumyesStatusvalues listed in §5.12

Relationships

  • A user may belong to one department (N:1, optional, delete: clear).
  • A user may manage many departments (1:N, via departments.manager_user_id).
  • A user may be the business owner of many subscriptions (1:N, via subscriptions.business_owner_id).
  • A user may be signatory on many subscriptions (1:N, via subscriptions.signatory_user_id).
  • usersubscription is many-to-many through the license_assignments junction.

4. Relationship summary

FromFieldToCardinalityKindDelete behavior
subscriptionsvendor_idvendorsN:1referencerestrict
subscriptionsbusiness_owner_idusersN:1referenceclear
subscriptionsprimary_department_iddepartmentsN:1referenceclear
subscriptionssignatory_user_idusersN:1referenceclear
departmentsmanager_user_idusersN:1referenceclear
departmentsparent_department_iddepartmentsN:1referenceclear
budget_linesbudget_period_idbudget_periodsN:1parentcascade
budget_linesdepartment_iddepartmentsN:1referenceclear
budget_linessubscription_idsubscriptionsN:1referenceclear
license_assignmentssubscription_idsubscriptionsN:1parent (junction)cascade
license_assignmentsuser_idusersN:1parent (junction)cascade
usersdepartment_iddepartmentsN:1referenceclear

5. Enumerations

5.1 subscriptions.category

  • communication
  • dev_tools
  • productivity
  • marketing
  • sales
  • hr
  • finance
  • security
  • design
  • analytics
  • infrastructure
  • other

5.2 subscriptions.criticality

  • critical
  • important
  • nice_to_have

5.3 subscriptions.billing_cycle

  • monthly
  • quarterly
  • annual
  • multi_year
  • one_time

5.4 subscriptions.payment_method

  • credit_card
  • ach
  • wire
  • invoice
  • purchase_order

5.5 subscriptions.payment_terms

  • net_15
  • net_30
  • net_60
  • net_90
  • prepaid
  • monthly
  • quarterly
  • annual

5.6 subscriptions.status

  • active
  • trialing
  • pending
  • cancelled
  • expired
  • deprecated
  • archived

5.7 departments.status

  • active
  • inactive

5.8 budget_periods.period_type

  • fiscal_year
  • quarter
  • month
  • custom

5.9 budget_periods.status

  • draft
  • open
  • closed
  • archived

5.10 budget_lines.category

  • communication
  • dev_tools
  • productivity
  • marketing
  • sales
  • hr
  • finance
  • security
  • design
  • analytics
  • infrastructure
  • other
  • unallocated

(Note: shares most values with subscriptions.category plus unallocated for budget lines not tied to a specific category.)

5.11 license_assignments.status

  • active
  • inactive
  • pending
  • revoked

5.12 users.status

  • active
  • inactive
  • offboarded

6. Open questions

6.1 🔴 Decisions needed

None.

6.2 🟡 Future considerations

  • Should an exchange_rates (date, from_currency, to_currency, rate) entity be added for multi-currency reporting? currency is stored per record as an ISO 4217 string, but there is no FX table — budget-vs-actual in a single reporting currency would require one.
  • Should invoices and invoice_line_items be added once AP-level tracking (paid vs. due, dispute handling, line-level allocations) is in scope? This model intentionally omits them; “expected” spend is computed from subscriptions.recurring_amount × cadence across a budget period, not from received bills.
  • Should contracts be promoted out of subscriptions into a separate contracts entity to support MSAs covering multiple sub-products? A subscription currently carries its own contract_number, signed_date, document_url, total_contract_value, renewal_notice_days, negotiated_savings — fine for 1:1 contract-to-subscription, but not for one contract spanning several subscriptions.
  • Should product identity be re-split from commercial terms into a separate saas_applications entity? A single subscriptions record carries both product and terms; multiple concurrent subscriptions for the same product work as multiple rows, but product-level reporting (without double-counting) would require the split.
  • Should approval_requests / purchase_orders be modelled in this system? No approval workflow entities are present; significant addition if purchase, renewal, or budget-change approvals must be tracked here rather than in a separate tool.
  • Should a usage_events entity be added for richer engagement analytics? license_assignments.last_active_date captures a single timestamp for basic unused-license detection; per-user activity or feature adoption would need a dedicated event log.
  • Should subscriptions.category and budget_lines.category be promoted to a shared lookup table to avoid enum drift? The two enums diverge only in that budget_lines has unallocated; a lookup table would keep them aligned as the taxonomy evolves.

7. Implementation notes for the downstream agent

  1. Create one module named saas_expense_tracker and two baseline permissions (saas_expense_tracker:read, saas_expense_tracker:manage) before any entity.
  2. Create entities in this order so referenced tables exist first: departmentsusersvendorssubscriptionsbudget_periodsbudget_lineslicense_assignments. Note that departmentsusers have a mutual reference (departments.manager_user_id and users.department_id); create both entities first, then add the cross-references as a second pass.
  3. For each entity: set label_column to the snake_case field marked as label in §3, pass module_id, view_permission: "saas_expense_tracker:read", edit_permission: "saas_expense_tracker:manage". Do not manually create id, created_at, updated_at, or the auto-label field.
  4. For each field in §3: pass table_name, field_name, format, title (the Label column), is_nullable (inverse of Required), and for reference/parent fields also reference_table and a reference_delete_mode consistent with §4.
  5. Fix up label-column titles. create_entity auto-creates a field whose field_name equals label_column and whose title defaults to singular_label. Every entity in this model has a Label for the label_column row that differs from singular_label (intentional — singular_label stays a bare singular for plural/singular symmetry, while the field-level title is more specific). After each create_entity, call update_field with the composite string id "{table_name}.{label_column}" (passed as a string, not an integer) to set the correct title:
    • "vendors.vendor_name""Vendor Name"
    • "subscriptions.subscription_name""Subscription Name"
    • "departments.department_name""Department Name"
    • "budget_periods.period_name""Period Name"
    • "budget_lines.budget_line_name""Budget Line Name"
    • "license_assignments.assignment_label""Assignment"
    • "users.full_name""Full Name"
  6. Deduplicate against Semantius built-in tables. This model declares users which already exists as a Semantius built-in. Read the built-in first: if it already covers the required shape, skip the create_entity call and reuse the built-in as the reference_table target for departments.manager_user_id, subscriptions.business_owner_id, subscriptions.signatory_user_id, license_assignments.user_id, and users.department_id. Add only the missing fields in §3.7 that are not present on the built-in (department_id, job_title, employee_id are the likely additions; check before creating each). Additive changes only. If the built-in is reused, skip the step 5 update_field for "users.full_name" unless the built-in’s existing title needs correcting.
  7. Caller-populated label columns. Two entities have label_columns with no natural source field; front-end callers must populate them on create:
    • license_assignments.assignment_label — e.g. "{user.full_name} / {subscription.subscription_name}" (junction has no natural string identifier).
    • budget_lines.budget_line_name — e.g. "{department.department_name} — {category} — {budget_period.period_name}" (no single source field identifies a budget line).
  8. After creation, spot-check that label_column on each entity resolves to a real field, that all reference_table targets exist, and that each label-column field’s title matches the §3 Label (not singular_label).