Skip to content

Architecture

PlotPress is three things glued by a stable contract:

  1. A library of dashboard workspaces in git. Each dashboard folder under dashboards/ is self-contained: its own connections, its own users, its own pages.
  2. A Go backend that loads those workspaces, holds metadata in SQLite, executes queries against allow-listed data sources, and enforces per-dashboard authorization.
  3. An Astro frontend that renders the markdown into pages and runs Observable Plot in islands that fetch data from the backend.
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)]
  1. User loads /dashboards/sales/ in the browser.
  2. Astro serves a static HTML page rendered from dashboards/sales/index.md.
  3. A Plot island in that page calls POST /api/query with { dashboard: "sales", view: "monthly_revenue", params: { year: 2026 } }.
  4. The Go backend:
    • Resolves the user’s session (OIDC cookie or Email OTP).
    • Reads the dashboard’s users.yaml and computes membership.
    • Reads connections.yaml and finds the connection bound to monthly_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).
  5. 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.

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/new and edit three files. Nothing in the rest of the tree changes.
ConcernFrontend (Astro)Backend (Go)
Markdown rendering✅ build-time
Plot rendering✅ in browser
Workspace loading✅ at boot + on SIGHUP
Query execution
Auth / sessionspasses cookie✅ source of truth
User ↔ connection binding✅, per-dashboard
Audit log✅ in SQLite

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.