Rollups over orders, order lines, deliveries and visits, and the dashboard over them. Core.
analytics is the pre-aggregated read side of the transactional modules, and the dashboard that renders it.
Five materialized views in every tenant database, one per grain: orders by day, orders by customer and month, products sold by month, deliveries by day, visits by day. Every grain that sums money carries the currency, because summing minor units across currencies is silently wrong. Every grain carries its owner column, because a materialized view has no row-level security of its own, so a wrapper view filters on it: an admin sees the tenant, a rep sees their own rows.
A KPI tile is a sum over a window of these, never a new view.
A fleet job refreshes only the tenants whose data has changed. A dirty flag lives in the platform database, one row per tenant, stamped by the server-side write paths, so the job never wakes a tenant's compute to ask whether it needs refreshing. The interval is a tenant setting floored by the plan.
/dashboard renders module-gated stat tiles and charts over a period. A tenant with visits but not orders gets the visits tile and chart, not zeroed commerce numbers. Every tile drills through to its own surface pre-filtered to the same period, using the grid's own filter URL contract. The page states when the numbers were last refreshed and marks them once the gap exceeds the tenant's interval.
The vocabulary is a semantic layer's: a cube, its measures and dimensions, a granularity, a time range. Swapping the Postgres adapter for a hosted semantic layer is a new adapter, not a re-authoring.
analytics: the refresh interval, the window in days, and the order and visibility of tiles and charts.
None of its own; each rollup carries its base table's read permission.
Never. The plan's refresh floor hangs off it.