Module Dependencies
Modules may depend on other modules.
This document defines how dependency relationships are described for product design and implementation. Runtime enforcement is implemented via module_dependencies, ModuleDependencyResolver, and ModuleSubscriptionService (install blocks when required dependencies are missing).
Categories
Dependencies should be categorized as:
| Category | Meaning |
|---|---|
| Required | The dependent module cannot function correctly without the dependency installed and available |
| Optional | The dependent module works alone; the dependency unlocks extra integrations or features |
Commercial nature of a dependency
Independently of required vs optional, a dependency relationship may be:
| Nature | Meaning |
|---|---|
| Free | The dependency module is free (or included) for the workspace |
| Billable | The dependency module is (or may become) a paid marketplace module |
A required dependency can be free or billable. An optional dependency can be free or billable. Licensing of each module remains independent — see Module Licensing.
Examples
These examples illustrate the intended design language. They are not a commitment that every example is already implemented.
Meetings → Calendar (required)
Meetings
└── depends on Calendar (required)Meeting scheduling assumes calendar concepts (availability, time ranges, calendar views). Meetings should declare Calendar as a required dependency.
Status: Calendar and Meetings are shipped. Meetings projects onto CalendarEvent via CalendarEventService::upsertFromSource (source=meeting, morph alias meeting).
Leads → Contacts (optional)
Leads
└── may depend on Contacts (optional — unlocks full lead convert)Leads works without Contacts. When Contacts is entitled, POST /leads/{id}/convert creates (or links) a Contact and sets leads.contact_id. Without Contacts, convert remains the status-oriented placeholder (conversion_meta.stub = true).
Status: Contacts is shipped. No hard module_dependencies row — soft entitlement check in LeadService::convert.
Contacts → Companies (optional)
Contacts
└── may depend on Companies (optional — unlocks company picker / company_id)Contacts works without Companies (legacy free-text company string only). When Companies is entitled, Contact create/update can set company_id and sync the legacy string from the Company name. No hard module_dependencies row — soft entitlement / SPA gating.
Status: Companies is shipped.
Activities → Contacts / Companies / Leads (optional)
Activities
├── may depend on Contacts (optional — contact_id link)
├── may depend on Companies (optional — company_id link)
└── may depend on Leads (optional — lead_id link)Activities works as a licensed module on its own catalog row, but create/update requires at least one related FK. Each FK is validated only when that module is entitled (soft entitlement; no hard module_dependencies row).
Status: Activities is shipped.
Opportunities → Contacts / Companies / Leads (optional)
Opportunities
├── may depend on Contacts (optional — contact_id link)
├── may depend on Companies (optional — company_id link)
└── may depend on Leads (optional — lead_id link)Opportunities works as a licensed Sales module on its own catalog row. Related FKs are optional; each is validated only when that module is entitled (soft entitlement; no hard module_dependencies row).
Sales Pipeline is not a separate module — stages and the Kanban board live inside Opportunities.
Status: Opportunities is shipped.
Quotations / Contracts → Opportunities (required)
Quotations
└── depends on Opportunities (required)
Contracts
└── depends on Opportunities (required)Quotations and Contracts each declare Opportunities as a required hard dependency (module_dependencies) — Marketplace install blocks until Opportunities is entitled.
Status: Quotations and Contracts are shipped.
Contracts → Quotations (optional)
Contracts
└── may depend on Quotations (optional — unlocks quotation_id link)Contracts works without Quotations. When Quotations is entitled, a Contract may optionally link quotation_id; validated by LinkableQuotation (soft entitlement + assignee scope). No hard module_dependencies row for this optional link.
Quotations / Contracts → Invoices (optional, shipped, reverse direction)
Quotations
└── may use Invoices (optional — convert to invoice)
Contracts
└── may use Invoices (optional — create invoice)POST /quotations/{id}/convert creates a draft CustomerInvoice from a sent/accepted quotation. POST /contracts/{id}/convert creates a draft CustomerInvoice from an active contract (repeatable). Both are soft, call-time entitlement checks inside QuotationService::convertToInvoice() / ContractService::createInvoice() — not module_dependencies rows — so Quotations and Contracts keep working with Invoices uninstalled; only those endpoints 422 until Invoices is installed.
Quotation convert is one-shot: any invoice with that quotation_id (including from an estimate convert or a contract bill) blocks a second quote convert. Contract billing stays repeatable. Estimate convert is also blocked when the estimate’s quotation is already invoiced (QuotationInvoiceGuard).
Status: shipped with quotations 1.4.0, contracts 1.2.0, invoices 1.6.0, estimates 1.3.2.
Invoices → Contacts / Companies / Quotations (optional)
Invoices
├── may depend on Contacts (optional — contact_id link)
├── may depend on Companies (optional — company_id link)
└── may depend on Quotations (optional — quotation_id link)Invoices works as a licensed Billing module on its own catalog row with no required module_dependencies row — unlike Quotations/Contracts, it does not require Opportunities. Contact and Company links are optional and only surfaced/validated when that module is entitled. quotation_id is a plain tenant-scoped existence check (not gated by a LinkableQuotation-style entitlement rule, unlike the Contracts → Quotations link above).
Status: Invoices is shipped.
Payments → Invoices (required)
Payments
└── depends on Invoices (required)Payments records amounts against an invoice's amount_paid and allocates them to one or more CustomerInvoice rows on post, declaring Invoices as a required hard dependency (module_dependencies) — the first Phase 3 module to require another. Marketplace blocks installing Payments on a workspace that doesn't already have Invoices entitled.
Status: Payments is shipped — see Product Roadmap Phase 3.
Resellers → Payments (required)
Resellers
└── depends on Payments (required)Reseller commission accrues only when a linked customer invoice becomes fully Paid via the Payments ledger. Resellers declares Payments as a required hard dependency (module_dependencies). Marketplace blocks installing Resellers until Payments is entitled.
Status: Resellers Phase 1 is shipped (free Sales opt-in). Cross-workspace reseller identity is deferred.
Reseller Payouts → Resellers (required)
Reseller Payouts
└── depends on Resellers (required)The commission ledger (reseller_commission_entries) always references a Reseller. Reseller Payouts declares Resellers as a required hard dependency (module_dependencies). Install chain: Payments → Resellers → Reseller Payouts.
Status: Reseller Payouts Phase 1 is shipped (free Sales opt-in).
Credit Notes → Invoices (required)
Credit Notes
└── depends on Invoices (required)Every CustomerCreditNote references a CustomerInvoice (customer_invoice_id, required). Credit Notes declares Invoices as a required hard dependency (module_dependencies), the same pattern as Payments → Invoices. Applying a credit note credits the linked invoice's amount_credited and recalculates balance_due (without changing invoice status). Marketplace blocks installing Credit Notes on a workspace that doesn't already have Invoices entitled.
Status: Credit Notes is shipped — see Product Roadmap Phase 3.
Estimates → Invoices (required)
Estimates
└── depends on Invoices (required)POST /estimates/{id}/convert creates a draft CustomerInvoice from an accepted (or sent) estimate, copying its lines and linking it back via customer_invoices.estimate_id. Estimates declares Invoices as a required hard dependency (module_dependencies), the same pattern as Payments/Credit Notes → Invoices. Estimates also has optional soft links to Contacts, Companies, Opportunities, and Quotations (each validated only when entitled; no hard dependency rows). Marketplace blocks installing Estimates on a workspace that doesn't already have Invoices entitled.
Status: Estimates is shipped — see Product Roadmap Phase 3.
Purchase Orders → Vendors (required)
Purchase Orders
└── depends on Vendors (required)A Purchase Order cannot exist without a supplier — every PurchaseOrder requires a vendor_id. Purchase Orders declares Vendors as a required hard dependency (module_dependencies), the same pattern as Payments → Invoices / Estimates → Invoices — Marketplace blocks installing Purchase Orders until Vendors is entitled.
Status: Vendors and Purchase Orders are both shipped — see Product Roadmap Phase 4.
Expenses → Vendors, Purchase Orders (optional, shipped)
Expenses
├── may depend on Vendors (optional — unlocks vendor_id link)
└── may depend on Purchase Orders (optional — unlocks purchase_order_id link)Expenses installs standalone with no module_dependencies rows — it works as a generic expense record with no supplier or purchase order context. When Vendors is entitled, an Expense may optionally link vendor_id; when Purchase Orders is entitled, an Expense may optionally link purchase_order_id. Both links are validated only at the point of use (LinkableVendor, LinkablePurchaseOrder rules) — soft entitlement, no hard dependency rows, so Vendors/Purchase Orders can be uninstalled later without breaking Expenses (existing links are simply no longer enforced/displayed as active relations).
Status: Vendors, Purchase Orders, and Expenses are all shipped, completing Phase 4 — see Product Roadmap Phase 4.
Purchase Orders → Expenses (optional, shipped, reverse direction)
Purchase Orders
└── may use Expenses (optional — unlocks "Convert to expense" action)POST /purchase-orders/{id}/convert creates a draft Expense from a sent/partially_received/received purchase order. This is a soft, call-time entitlement check inside PurchaseOrderService::convertToExpense() — not a module_dependencies row — so Purchase Orders keeps working with Expenses uninstalled; only the convert endpoint itself returns a 422 until Expenses is installed. The conversion is one-way and one-time (an existing Expense with that purchase_order_id, including soft-deleted, blocks re-conversion).
Status: Shipped — see Product Roadmap Phase 4 and Tenant Purchase Orders API.
Leave Management → Employees (required, shipped)
Leave Management
└── depends on Employees (required)Leave Management declares Employees as a required hard dependency (module_dependencies). Marketplace blocks installation until Employees is entitled. Leave balances and requests reference employees.id.
Status: Shipped — see Product Roadmap Phase 7.
Attendance → Employees (required, shipped)
Attendance
└── depends on Employees (required)Attendance declares Employees as a required hard dependency. Daily records reference employees.id.
Status: Shipped — see Product Roadmap Phase 7.
Payroll → Employees (required, shipped)
Payroll
└── depends on Employees (required)Payroll declares Employees as a required hard dependency. Payroll profiles and pay-run lines reference employee records; Payroll should not re-implement the employee directory.
Status: Shipped — see Product Roadmap Phase 7.
Payroll → Accounting (optional, shipped)
Payroll
└── may use Accounting (optional — unlocks pay-run journal post)Payroll installs and runs without Accounting. POST /pay-runs/{id}/post is a soft, call-time entitlement check inside PayRunService::postToJournal() — also registered as an optional module_dependencies row — so Marketplace can surface the integration while Payroll remains usable alone. Posting creates a draft journal (expense debit / liability credit) and stores journal_entry_id.
Status: Shipped — see Tenant Payroll API.
Payments → Accounting (optional, shipped)
Payments
└── may use Accounting (optional — deposit account + auto-posted journal on payment post)Payments installs without Accounting (still requires Invoices). When Accounting is entitled, payment post creates a posted journal (Dr cash/bank deposit / Cr AR) via CashMovementJournalService and stores deposit_account_id + journal_entry_id. Void voids the linked journal. Optional module_dependencies row.
Expenses → Accounting (optional, shipped)
Expenses
└── may use Accounting (optional — paid-from account + auto-posted journal on mark paid)Expenses installs standalone. When Accounting is entitled, POST /expenses/{id}/pay requires paid_from_account_id (cash/bank) and posts Dr expense / Cr paid-from for amount+tax. Optional module_dependencies row.
Help Desk → Contacts, Companies (optional, shipped)
Help Desk
├── may depend on Contacts (optional — unlocks contact_id link)
└── may depend on Companies (optional — unlocks company_id link)Help Desk installs standalone with no module_dependencies rows — it works as an internal ticket queue with no CRM context. When Contacts is entitled, a ticket may optionally link contact_id; when Companies is entitled, a ticket may optionally link company_id. Both links are validated only at the point of use (LinkableContact, LinkableCompany rules) — soft entitlement, no hard dependency rows.
Status: Shipped — see Help Desk Overview and Product Roadmap Phase 8.
WhatsApp Cloud → Leads (optional, shipped)
WhatsApp Cloud
└── may depend on Leads (optional — unlocks lead_id link + timeline mirrors)WhatsApp Cloud installs standalone with no module_dependencies rows. Conversations work with lead_id null. When Leads is entitled, agents may link a Lead and outbound/inbound messages mirror onto the Lead timeline via LeadService::recordActivity. Communication Templates (wa.me) remains a separate free module and is not a hard dependency.
Status: Shipped MVP 1.0.0 — see WhatsApp Cloud Overview and Product Roadmap Phase 1.
Inventory → Products (required, shipped)
Inventory
└── depends on Products (required)Inventory declares Products as a required hard dependency (module_dependencies), so Marketplace blocks Inventory installation until Products is entitled. Stock levels, adjustments, transfers, and receipt posting all reference Products.
Inventory → Warehouses (optional, shipped)
Inventory
└── may use Warehouses (optional — locations and default warehouse)Inventory soft-uses Warehouses through WarehouseService::ensureDefaultWarehouse() to provide the MAIN location when a stock action omits warehouse_id. The Warehouses module gates its own management UI and CRUD; it is not a hard Inventory install dependency.
Purchase Orders → Inventory (optional, shipped)
Purchase Orders
└── may use Inventory (optional — receipt stock posting)Purchase Orders continues to work without Inventory. When Inventory and Products are entitled, receiving a PO posts stock-in for each line with a nullable product_id whose linked Product tracks stock; LinkableProduct validates that link. Partially received remains acknowledgement-only. This is a soft integration, not a module_dependencies row.
Financial Reports → Accounting (required, shipped)
Financial Reports
└── depends on Accounting (required)Financial Reports declares Accounting as a required hard dependency (module_dependencies). Marketplace blocks installation until Accounting is entitled. Reports read posted journal lines only.
Accounting → Inventory (optional, future)
Accounting
└── may depend on Inventory (optional)Accounting ships standalone (manual double-entry) plus soft cash movements from Payments/Expenses and Finance → Transfers. A future milestone may optionally integrate stock valuations or COGS-related flows through contracts/services. Auto-posting from Invoices / Credit Notes / Purchase Orders / Inventory remains deferred.
Projects (standalone) + Tasks → Projects (optional, shipped)
Projects
├── may depend on Contacts (optional — contact_id link)
├── may depend on Companies (optional — company_id link)
└── may depend on Opportunities (optional — opportunity_id link)
Tasks
└── may depend on Projects (optional — project_id link)Projects installs standalone with no module_dependencies rows — title, status board, assignee/members, notes/timeline work without CRM modules. Soft FKs to Contact / Company / Opportunity are validated only when those modules are entitled. Tasks may optionally set project_id via LinkableProject (Projects entitled + project visible to the actor); uninstalling Projects nulls the FK (nullOnDelete). No hard install dependency either direction.
Status: Projects shipped lean v1.0.0; Tasks catalog 1.2.0 adds soft project_id.
Knowledge Base (standalone, shipped)
Knowledge Base
└── (no hard dependencies)Knowledge Base installs as a free Operations Marketplace opt-in (knowledge-base 1.0.0) with no module_dependencies rows. Internal workspace articles only — Help Desk links, public URLs, and Automation triggers are deferred and must remain soft/optional if added later.
Status: Shipped — see Knowledge Base Overview and Product Roadmap.
Assets (standalone, shipped)
Assets
├── may link Vendor (soft, when Vendors entitled)
└── may link Employee (soft, when Employees entitled)Assets installs as a free Operations Marketplace opt-in (assets 1.0.0) with no module_dependencies rows. The register works alone (name, number, status, category, location, purchase/warranty, assignee, notes/timeline). Optional vendor_id / employee_id are validated only at the point of use (LinkableVendor, LinkableEmployee) — soft entitlement, no hard install dependency. Location is free-text in v1 (no Warehouse FK). Depreciation / Accounting, Products / Inventory coupling, and Help Desk maintenance tickets are deferred.
Status: Shipped — see Assets Overview and Product Roadmap.
Documents → Storage (required, shipped)
Documents
└── depends on Storage (required)Documents installs as a free Operations Marketplace opt-in (documents 1.0.0) with a hard module_dependencies row on free Storage. Install is blocked until Storage is entitled. Uploaded file bytes (documents.size_bytes) count toward workspace Storage used quota; soft-deleted documents are excluded until restored. Flat categories only — soft links to other modules and nested folders are deferred and must remain soft/optional if added later.
Status: Shipped — see Documents Overview and Product Roadmap.
Analytics / Reports → CRM / Sales / Billing / Purchasing modules (optional, shipped)
Reports (slug analytics)
├── may use Leads / Tasks (CRM report + overview)
├── may use Opportunities / Quotations / Contracts (Sales report)
├── may use Invoices / Payments / Credit Notes (Billing report)
├── may use Vendors / Purchase Orders / Expenses (Purchasing report)
└── may use Help Desk / Projects (overview sections)Reports installs as a free Operations Marketplace opt-in (analytics 1.1.0, display name Reports) with no module_dependencies rows. Overview sections and domain reports include data only when that source module is entitled and the actor has {module}.view. Accounting statements stay in Financial Reports.
Status: Shipped — see Analytics Overview and Product Roadmap.
AI → domain modules (optional)
AI
├── may optionally integrate with Leads
├── may optionally integrate with Tasks
├── may optionally integrate with CRM (Contacts, Companies, …)
└── … other domain modules as neededAI Integration is planned as a cross-cutting capability. Integrations with Leads, Tasks, CRM, and similar modules should be optional — AI must not require every domain module to be installed.
Storage packs → Storage (required)
storage-10 / storage-50 / storage-100 / storage-500 / storage-1000
└── depends on storage (required)Billable capacity packs cannot install until free Storage is entitled. Packs are mutually exclusive in product logic (cancel current pack before buying another size). Content upload surfaces (Team Chat, Feedback, Lead imports) soft-check Storage allowance; they do not declare a hard module_dependencies row on Storage. Documents is different: it declares a required dependency on Storage. Team Chat soft-companion installs free Storage when Team Chat is activated.
Status: Storage shipped.
Design rules
When designing a new module:
- List required dependencies explicitly in the module’s documentation.
- List optional integrations separately so install and licensing expectations stay clear.
- Keep business logic inside the owning module; call dependents through contracts/services only (Module Architecture).
- Do not assume a dependency is always present unless it is marked required and the platform later enforces that rule.
- Do not implement dependency resolution in application code based on this document alone — that remains future work.
Related
- Module Architecture
- Module Licensing
- Product Roadmap (Calendar, Meetings, AI)
- Entitlements