Back to BlogArchitecture & Engineering

Building the SaaS Dashboard: Metrics, Charts and Date Ranges That Stay Fast

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

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.

StrategyHow it readsFreshnessUse when
Query raw tables liveScans on every loadReal-timeTiny data, or an internal tool
Materialised view, refreshedReads a stored resultMinutes behindDashboards that tolerate a short lag
Rollup tables, updated on write or on a scheduleReads one row per bucketMinutes, or near real-timeMost SaaS dashboards at scale
A separate analytics storeColumnar, built for aggregationDepends on the pipelineVery 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.

OptionStrengthTrade-offChoose when
SVG component libraryStyles with your design system; good for typical panelsStruggles past a few thousand pointsMost SaaS dashboards
Canvas-based libraryHandles many points smoothlyLess flexible styling and accessibilityDense time series
Low-level toolkitFull control over bespoke visualsYou build axes, legends and interaction yourselfUnusual 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?

OptionExamplesChoose this whenWatch out for
An embedded analytics productHosted dashboards you embed per tenantYou want customer-facing analytics fast and will pay per seat or queryTheming limits, per-tenant data modelling and cost at scale
A BI tool for internal useSelf-serve BI connected to your databaseYour own team needs ad-hoc analysis, not an in-product viewNot built to expose safely to customers
Custom build on your dataRollups plus a charting library, as hereThe dashboard is part of the product and must match your UI and tenancyYou own the rollups, time zones and caching
Hire a team to build itRAITHub or another studioIt must stay fast as data grows and never mix tenantsGet 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.

dashboardchartsmetricsSQL aggregationSaaStime zonesPostgreSQL

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.