Building the SaaS Dashboard: Metrics, Charts and Date Ranges That Stay Fast
Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
Build a SaaS dashboard back-to-front: pre-aggregate metrics into summary tables so a page load reads thousands of rows, not millions; bucket every time series in the viewer's time zone, not the server's; scope every query to one tenant; cache each panel by tenant, metric and range; and only then pick a chart library. The charts are the easy 10%.
This is engineering guidance for SaaS on PostgreSQL. If you would rather have it built and tested for you, see how RAITHub would build this below.
Why do SaaS dashboards get slow?
Because the first version computes every number live, on every load, over the full history. "Orders this month" becomes a scan of the whole orders table; six panels become six scans; and the page that was instant with 10,000 rows crawls at 10 million. The fix is to separate how data is stored from how it is read: keep raw events for correctness, and read pre-aggregated summaries for the dashboard.
| Strategy | How it reads | Freshness | Use when |
|---|---|---|---|
| Query raw tables live | Scans on every load | Real-time | Tiny data, or an internal tool |
| Materialised view, refreshed | Reads a stored result | Minutes behind | Dashboards that tolerate a short lag |
| Rollup tables, updated on write or on a schedule | Reads one row per bucket | Minutes, or near real-time | Most SaaS dashboards at scale |
| A separate analytics store | Columnar, built for aggregation | Depends on the pipeline | Very large event volumes |
If the dashboard is customer-facing analytics rather than your own operational view, weigh building against embedding, as in customer-facing analytics in SaaS: build or embed.
How do you pre-aggregate metrics without going stale?
Keep a rollup table bucketed by day (or hour) per tenant and metric, and update it as events arrive or on a short schedule. A daily bucket turns "revenue over 90 days" into reading 90 rows.
CREATE TABLE metric_daily (
tenant_id uuid NOT NULL REFERENCES tenants(id),
day date NOT NULL, -- a calendar day in UTC
metric text NOT NULL, -- 'orders' | 'revenue_cents' | 'signups'
value bigint NOT NULL DEFAULT 0,
PRIMARY KEY (tenant_id, day, metric)
);
-- Upsert as events happen, so the dashboard never scans the raw table.
INSERT INTO metric_daily (tenant_id, day, metric, value)
VALUES ($1, $2::date, 'revenue_cents', $3)
ON CONFLICT (tenant_id, day, metric)
DO UPDATE SET value = metric_daily.value + EXCLUDED.value;
Store money as integer minor units (cents), never floats, for the reasons in the multi-currency support guide. For metrics you cannot simply add up, such as unique active users, a daily bucket of raw counts plus a periodic exact recount is usually enough; reserve approximate-distinct structures for very large volumes.
Why are date ranges the hardest part of a dashboard?
Because "today" depends on the viewer. A user in Auckland and a user in Los Angeles disagree about when a day starts by nearly a full day, so a chart bucketed in the server's time zone shows the wrong totals to almost everyone. Bucket in the viewer's time zone. PostgreSQL's date/time functions let you shift a timestamp into a named zone before truncating it to a day.
-- Daily revenue for a tenant, bucketed in the VIEWER'S time zone.
SELECT date_trunc('day', occurred_at AT TIME ZONE $2) AS day, -- $2 = 'Pacific/Auckland'
sum(amount_cents) AS revenue_cents
FROM orders
WHERE tenant_id = $1
AND occurred_at >= $3 AND occurred_at < $4
GROUP BY 1
ORDER BY 1;
Compute the range boundaries in the same zone, and fill empty buckets to zero in the query or the code, so a quiet day shows a zero rather than a gap the chart interpolates over. Store all timestamps in UTC; convert only for display and bucketing.
How do you keep dashboard queries tenant-safe?
Every dashboard query is filtered by tenant_id, and the (tenant_id, day, metric) key on the rollup serves those reads directly. Back it with a row-level security policy so a mistake in one query cannot leak another tenant's numbers, as in the PostgreSQL row-level security guide. A dashboard that silently mixes tenants is worse than a slow one: it is a data breach that looks like a feature.
How should you cache dashboard panels?
Cache each panel's result keyed by tenant, metric, resolution and range, with a short time-to-live that matches how fresh the data must be. A 60-second cache turns a burst of refreshes into one query, and a dashboard with six panels into six cached reads. Invalidate a tenant's cache when its rollups change, or just let the short TTL expire.
Load panels independently so a slow one does not block the page, and return a date range and a "generated at" timestamp with each result, so the UI can show the user exactly what window they are looking at.
Do-it-yourself estimate: 1–2 weeks for rollups, time-zone-correct ranges, tenant-scoped queries and caching, plus a few days to wire charts, if the data model is in place. The main risks are server-time-zone bucketing and live full-table scans that only hurt once data grows.
Which charting library should you use?
Pick the library last, and pick it for your rendering model and data sizes, not its gallery.
| Option | Strength | Trade-off | Choose when |
|---|---|---|---|
| SVG component library | Styles with your design system; good for typical panels | Struggles past a few thousand points | Most SaaS dashboards |
| Canvas-based library | Handles many points smoothly | Less flexible styling and accessibility | Dense time series |
| Low-level toolkit | Full control over bespoke visuals | You build axes, legends and interaction yourself | Unusual charts a library cannot do |
Whatever you choose, give every chart a text alternative (a data table behind it) and keyboard-accessible controls. An accessible dashboard is also a testable one, because the numbers are in the DOM, not only in pixels.
Buy, build or hire?
| Option | Examples | Choose this when | Watch out for |
|---|---|---|---|
| An embedded analytics product | Hosted dashboards you embed per tenant | You want customer-facing analytics fast and will pay per seat or query | Theming limits, per-tenant data modelling and cost at scale |
| A BI tool for internal use | Self-serve BI connected to your database | Your own team needs ad-hoc analysis, not an in-product view | Not built to expose safely to customers |
| Custom build on your data | Rollups plus a charting library, as here | The dashboard is part of the product and must match your UI and tenancy | You own the rollups, time zones and caching |
| Hire a team to build it | RAITHub or another studio | It must stay fast as data grows and never mix tenants | Get the isolation and aggregation tests in the handover |
How do you test a dashboard?
- Isolation: seed two tenants and assert each sees only its own totals.
- Time zones: place an event near midnight and assert it lands in the right day for viewers in different zones.
- Empty buckets: assert a quiet day shows zero, not a skipped point.
- Aggregation: compare a rollup total against a slow count of the raw table and assert they match.
- Performance: load a dashboard against a large seed and assert the query count and latency stay bounded.
How RAITHub would build this
- Data model: pre-aggregated, tenant-scoped rollup tables updated on write or schedule, with money in minor units.
- Correctness: time-zone-aware bucketing and range boundaries, zero-filled series, and row-level security behind every query.
- Performance: per-panel caching keyed by tenant, metric and range, with independent panel loading.
- Tests: isolation, time-zone, aggregation and performance tests in CI.
Timeline: a dashboard inside a new SaaS build fits the 4–6 week fixed scope; added to an existing product it is a bounded piece in the 6–12 week backend range. You receive: aggregation and isolation tests in CI, handover docs and runbooks, and full IP under NDA.
Next step: a free 15-minute technical audit, then a written fixed quote. See the SaaS development service, compare the related PDF report generation build, and book the audit.
Frequently asked questions
Why is my SaaS dashboard slow, and how do I fix it?
Usually because it computes every metric live over the full history on each load. Pre-aggregate metrics into per-tenant rollup tables updated on write or on a schedule, so a page load reads one row per time bucket instead of scanning raw tables.
Should I use a materialised view or rollup tables?
A materialised view is simplest when a few-minute lag is fine and the refresh is cheap. Incrementally updated rollup tables scale better and stay fresher, because each write touches one bucket rather than rebuilding the whole view.
How do I handle time zones in dashboard date ranges?
Store timestamps in UTC and bucket them in the viewer's time zone using AT TIME ZONE before truncating to a day. Compute the range boundaries in the same zone, or users in different zones see wrong daily totals.
How do I stop a dashboard mixing tenants' data?
Filter every query by tenant_id, index the rollups by tenant, and enforce a row-level security policy so a single mistaken query cannot return another tenant's numbers. Test it by seeding two tenants and asserting isolation.
Which charting library is best for a SaaS dashboard?
There is no single answer. An SVG component library suits most panels and themes with your design system; a canvas library handles dense time series; a low-level toolkit suits bespoke visuals. Choose for your rendering model and data size, and give each chart a text alternative.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.