Browse Templates/Analytics Dashboard

Analytics Dashboard

A comprehensive analytics dashboard database schema designed for building data visualization platforms. Includes workspace-based multi-tenancy, customizable dashboards with drag-and-drop widgets, pluggable data source connections, event tracking with flexible JSONB payloads, and scheduled report generation. Suitable for business intelligence tools, product analytics platforms, or internal metrics dashboards.

PostgreSQL6 tablesSaaS
analyticsdashboardwidgetsdata-visualizationeventsreporting
Created with ERD Studio

About this schema

events is a different kind of table from everything else here. The other five hold configuration — workspaces, dashboards, widgets, connections, schedules — written occasionally by people. events is raw telemetry written continuously by machines, and it will outgrow all of them combined within days of launch.

A widget stores its query_config rather than its results. The dashboard is a saved question, not a saved answer, which is what lets the same widget show today's numbers tomorrow without anything being recomputed and stored in between.

Layout is data too. widgets carries position_x, position_y, width and height, and dashboards keeps a layout column — so a user dragging a chart is an update, and the arrangement survives being opened on another machine.

Key relationships

workspaces → dashboards → widgets
The tenant, the canvas, and the charts on it. Each widget's position is stored on the row, so the layout is queryable.
workspaces → data_sources → widgets
A connection belongs to the workspace and is reused by many widgets, so credentials are configured once rather than per chart.
workspaces → events
Raw telemetry scoped to the tenant, with user_identifier and session_id as the two grains almost every analytics question uses.
dashboards → reports
A schedule attached to a dashboard, with schedule_cron and recipients, so a report is a dashboard sent on a timer.
events.properties
A structured blob so a new event type needs no migration — the price is that nothing validates what a property contains.

Design decisions

Event properties are schemaless

Every product tracks different things, and a column per property would mean a migration for every new event anyone wants to send. A properties blob accepts all of them. The cost lands on reading: queries have to extract fields that may be absent, misspelled or of the wrong type, and a typo in an event name creates a second event nobody notices until a chart looks wrong.

Widgets save the query, not the result

Caching results in the database would make dashboards instant and immediately stale, and would need invalidation rules for data that changes continuously. Storing query_config keeps one definition that always reflects current data. Freshness is bought with query cost, which is why a production system puts a cache in front rather than in the table.

Connection config lives on data_sources

Putting credentials on each widget would duplicate secrets across every chart that uses the same database and make rotating them a sweep. One row per connection means a credential is changed once, and status with last_synced_at makes a broken connection visible before every widget on the dashboard fails at once.

Reports attach to dashboards, not to widgets

People subscribe to a view, not to a single chart. Keying the schedule to the dashboard means recipients get the whole picture and the report definition does not have to be rebuilt when a widget is added. The limitation is that a single-chart alert cannot be expressed here — that is a different feature with a threshold rather than a cron.

Tables in this template

workspacesWorkspaces
6 cols
dashboardsDashboards
8 cols
widgetsWidgets
10 cols
data_sourcesData Sources
8 cols
eventsEvents
7 cols
reportsReports
8 cols

Frequently asked

Should raw events really live in the same database?
At small volume, yes. Past that, events belongs in a columnar store while the configuration tables stay relational — the schema shows the model, and the split is a deployment decision made when event volume dictates it.
Why is there no users table?
Because this models the analytics product, not its accounts. workspaces carries an owner_email, and events identifies people by user_identifier — a string from the tracked application, not a foreign key, since those people are not users of this system.
How do I count unique visitors?
Distinct user_identifier over a time range, or distinct session_id for visits rather than people. Both columns exist precisely because those two questions have different answers.
Where would a funnel or retention query get its data?
From events alone, by ordering a user_identifier's rows by timestamp and matching event_name sequences. That is why the raw table is kept rather than only aggregates — aggregation decides in advance which questions can be asked.
Can a dashboard mix several data sources?
Yes — data_source_id is on the widget, not the dashboard. One canvas can show a chart from a warehouse next to one from the events table, which is the usual reason a dashboard exists.