Architecture
PlotPress is three things glued by a stable contract:
- A library of dashboard workspaces in git. Each dashboard folder under
dashboards/is self-contained: its own connections, its own users, its own pages. - A Go backend that loads those workspaces, holds metadata in SQLite, executes queries against allow-listed data sources, and enforces per-dashboard authorization.
- An Astro frontend that renders the markdown into pages and runs Observable Plot in islands that fetch data from the backend.
High-level diagram
Section titled “High-level diagram”
flowchart TB
subgraph Repo[Git repository]
direction TB
P[plotpress.yaml]
D1[dashboards/sales/]
D1c[connections.yaml]
D1u[users.yaml]
D1d[dashboard.yaml + *.md]
D1 --> D1c
D1 --> D1u
D1 --> D1d
end
subgraph Server[Go backend]
L[Loader<br/>parse workspaces]
A[Auth<br/>OIDC / Email OTP]
Q[Query executor<br/>per-driver]
M[(SQLite<br/>users, sessions, audit)]
end
subgraph Browser[Astro frontend]
Page[Markdown page]
I[Plot island]
end
Repo --> L
L --> Server
A --> Server
Server --> M
Page -->|GET dashboard| Server
I -->|POST /api/query| Q
Q -->|driver| DS[(Data sources)]
Request flow for a chart
Section titled “Request flow for a chart”- User loads
/dashboards/sales/in the browser. - Astro serves a static HTML page rendered from
dashboards/sales/index.md. - A Plot island in that page calls
POST /api/querywith{ dashboard: "sales", view: "monthly_revenue", params: { year: 2026 } }. - The Go backend:
- Resolves the user’s session (OIDC cookie or Email OTP).
- Reads the dashboard’s
users.yamland computes membership. - Reads
connections.yamland finds the connection bound tomonthly_revenue’s default-or-overridden source. - Verifies that at least one of the user’s resolved roles is in that connection’s
allowed_users. - Runs
SELECT * FROM monthly_revenue WHERE year = $1(parameter binding) against the connection’s driver. - Returns a typed JSON payload (column names, types, rows).
- The island hands the payload to Observable Plot and renders the chart.
The browser never sees a connection string and never names a table the dashboard’s connections.yaml doesn’t reference.
Why per-dashboard, not per-workspace
Section titled “Why per-dashboard, not per-workspace”Earlier designs put a single connections.yaml and users.yaml at the workspace root. Per-dashboard scoping is stricter:
- A dashboard cannot read a data source it doesn’t list. There is no inheritance from “the workspace can read X” → “this dashboard can read X.”
- A user permitted on one dashboard isn’t implicitly permitted on another. Permissions are declared where they apply.
- A new dashboard is
cp -r dashboards/_template dashboards/newand edit three files. Nothing in the rest of the tree changes.
What lives where
Section titled “What lives where”| Concern | Frontend (Astro) | Backend (Go) |
|---|---|---|
| Markdown rendering | ✅ build-time | — |
| Plot rendering | ✅ in browser | — |
| Workspace loading | — | ✅ at boot + on SIGHUP |
| Query execution | — | ✅ |
| Auth / sessions | passes cookie | ✅ source of truth |
| User ↔ connection binding | — | ✅, per-dashboard |
| Audit log | — | ✅ in SQLite |
Hot reload
Section titled “Hot reload”In development the backend watches each dashboard folder and reloads on change. In production a SIGHUP (or POST /admin/reload) reparses the workspace tree without dropping in-flight queries.