Skip to main content

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:

HalfWhat it isWhere
The configJSON describing tabs, data sources, filters and widgetsDASHBOARD_CONFIGS.config
The Studio BuilderThe 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:

  1. Every customer wants a different dashboard. One route per customer doesn't scale.
  2. Small changes cost a deploy. "Move the KPI row above the chart" shouldn't take a release.
  3. 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

  • version is always 2. 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
Configs live in the database, nowhere else

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

TermMeaning
App modeWhich product: payroll, edi, or ai_platform.
Global defaultThe config with company_id IS NULL. The fallback for every company on that app mode.
TabOne page of the dashboard, with its own data sources, filters and widgets.
Data sourceA named HTTP call the tab makes. Widgets read from it by name.
WidgetOne card on the grid, chart, KPI, table, map.
BindingA marker like {"$ref": "main.kpis.total"}, swapped for real data at render time.
RevisionAn older, inactive row for the same (company, app mode).
ResolverThe service that picks the config, expands inheritance, and strips what you can't see.
StudioThe three-pane editor: widget list, live canvas, inspector.

Where to go next