Dashboard Configuration & Studio Builder
What this is
Dashboards used to be React code. Each one — payroll overview, EDI volume, transportation — was a hand-written route file with its own charts, layout and fetch calls. Changing one meant a pull request and a deploy.
Now a dashboard is a row of JSON in the DASHBOARD_CONFIGS table, and there's
a visual editor for that JSON: the Studio Builder. You pick widgets, point
them at data, drag them around, save.
Two halves, worth keeping straight:
| Half | What it is | Where |
|---|---|---|
| The config | JSON describing tabs, data sources, filters and widgets | DASHBOARD_CONFIGS.config |
| The Studio Builder | The admin UI that writes that JSON for you | /app/settings/dashboard_configs |
The Builder's canvas is the real renderer, so what you see while editing is what users get.
Why we built it
Three problems, all really the same one:
- Every customer wants a different dashboard. One route per customer doesn't scale.
- Small changes cost a deploy. "Move the KPI row above the chart" shouldn't take a release.
- Only developers could touch them. Now any admin can build one.
How a dashboard gets picked
When someone opens /app/dashboards, the backend answers "which dashboard does
this person get?":
That filtering happens on the server, before the JSON is sent. The browser never receives a widget the user isn't allowed to see — nothing hidden, nothing to un-hide with devtools.
Six rules that explain the design
versionis always2. v1 configs are rejected.- One active config per
(company, app mode). To show different users different things, gate tabs with permissions inside that one config — don't make a second config. - One API call per tab. Name the data source
"main"and have every widget on the tab read from it. Your endpoint returns everything the tab needs in one go. - No JavaScript in configs. Formatters come from a fixed registry (
$fn). A config is data, not code — that's what makes it safe to hand to non-developers. - Every save creates a new row. The old row is deactivated, not overwritten. Revision history and "Restore" come free.
- Validation runs before every save. A config that fails can't be written; the
API returns
422.
Where the code lives
api/
app/models/dashboard_config.rb the record + resolve_for
app/controllers/dashboard_configs_controller.rb CRUD, revisions, validate, catalog
app/services/dashboard_configs/
validator.rb shape checks — errors block save, warnings don't
resolver.rb inheritance + isActive + permission filtering
catalog.rb the picker lists the Builder populates from
dashboard_configs/
widget_catalog.json single source of truth for widget types, $fn, kinds
web/app/
types/dashboardConfig.ts TS mirror of the schema
components/dashboard/DashboardRenderer/ the renderer
components/dashboard/builder/ the Studio Builder
routes/app.dashboards._index/ the page end users see
routes/app.settings.dashboard_configs.*/ the Builder routes
There are no dashboard config files in the repo to edit. Every dashboard is a row
in DASHBOARD_CONFIGS, and the Studio Builder is the way you create, change and
version them. If you want a config as a file — to review it, or to copy it to
another environment — export it from the Builder's JSON tools.
Glossary
| Term | Meaning |
|---|---|
| App mode | Which product: payroll, edi, or ai_platform. |
| Global default | The config with company_id IS NULL. The fallback for every company on that app mode. |
| Tab | One page of the dashboard, with its own data sources, filters and widgets. |
| Data source | A named HTTP call the tab makes. Widgets read from it by name. |
| Widget | One card on the grid, chart, KPI, table, map. |
| Binding | A marker like {"$ref": "main.kpis.total"}, swapped for real data at render time. |
| Revision | An older, inactive row for the same (company, app mode). |
| Resolver | The service that picks the config, expands inheritance, and strips what you can't see. |
| Studio | The three-pane editor: widget list, live canvas, inspector. |
Where to go next
- JSON schema and components breakdown: how it's shaped and how it works.
- Default dashboards: what ships out of the box.
- Migrating an existing dashboard: legacy React page → config.
- Creating one from scratch: SQL → backend → Studio.