← Design · c134

plan.md

c134 plan: Control Plane Web App (draft, before kick-off)

Gate: Tyler says Kamal is free to start the data model, and rules where the code lives; Mark's word. Phase 0 (the inventory of both apps) can run before the gate: kicked off 2026-11-09 with Phase 0 in its own session. Reader: Mark; Tyler and Kamal for the contract and the port plan; the executor who mocks the shell, auth and install flows. Session: 1b21a3ce. Rob: SaaS hardening doesn't block self-hosted adoption. Mark Peterson owns the local install experience and docs.

Why

The dashboard leaves qcontrol and becomes the control plane's UI: the thing you connect endpoints to, offered as SaaS and as a Docker image a company runs itself. Mark's prompt says app.qpoint.io already models it: the org and context frame, auth, the Pulse-backed data patterns, the wizards, alerting. Tyler's plan keeps Pulse (Go) and puts a new data model in it; Firebase goes because it can't be self-hosted. The pages are the qdash UI we have been building since c92. Design's part is the seam: which shell wins, what the API contract is, how people sign in without Firebase, how the thing installs, and the plan for the port.

What exists

  • app.qpoint.io (../../../app.qpoint.io): Nuxt 4 SPA, Pinia stores (auth, flash, pulse, query-string, top-nav), layouts default / global / org / session / dev, auth.global.ts and context.global.ts, Firebase 11 (email, Google, GitHub, Microsoft, SSO), SAML routes (server/routes/sso/saml/), the Pulse plugin ($pulse.fetch), useList / useEntity over Firestore, org-scoped composables, alerting rules and integrations with adapters, wizards (InstallQtap, DeployQproxy, ConnectApp), a dev/ showcase. Last commit 2026-10-27. Memory note (2026-10-02): the deployed site was considered stale and unused; it is the model, not the base.
  • qdash UI (../../../qcontrol/crates/qdash/ui, feat/new-pages, :3030): the pages (overview, inventory/*, activity, live, organization/*, policies, findings, compliance, framework, integrations, cost, alerts, campaigns, dlp, identity, performance, posture, security, reports, settings, account), the shell with the global filter and the when: pill, composables/api/transport.ts (http(), local()), types/api/* written from the backend handlers (R13), lib/*-rollups.ts with vitest, LAYER-CANDIDATES.md, c108's honest states, c113's baked worlds for the demo.
  • The qdash backend (crates/qdash/src): 38 /api/* routes over SQLite, plus /api/events/stream. The seed (scripts/qdash-seed) generates the Meridian world.
  • Pulse: not in this tree. Tyler: handcrafted filtering and views, "the JavaScript filter" on the backend side; Kamal strips the data model and puts the new one in.
  • Backend asks already recorded: from / to on every page and history beyond 90 days (c128), snapshots, sensor version and health, seat prices (c105), staged state that should be served (bible R25 list).

The shape

┌ control plane (one Docker image) ─────────────────────────────────────┐
│  UI (Nuxt, static)  ──▶  Go app (Pulse, new data model, API first)    │
│                              │                │                       │
│                          Postgres         ClickHouse                  │
│                        (state, orgs,     (events, audit,              │
│                         policies)         activity)                   │
└───────────────────────────────────────────────────────────────────────┘
        ▲ connect (OAuth-style, from the native app)      ▲ SSO / OIDC / local accounts
  endpoints (qcontrol + native app)                   people (operators, viewers)

Two deployments, one build: self-hosted (docker compose up, env for the two databases and secrets) and SaaS (we run the same image per customer).

Design's deliverables

  1. inventory.md — both apps mapped: pages, shells, auth and context flows, data access, wizards; what moves, what is replaced, what is dropped.
  2. The shell decision — one mock of the qdash pages inside the chosen frame (D1), with sign-in, org switch and the global filter together.
  3. The API contract — types/api/* and the 38 routes plus the backend asks, written as the contract Kamal implements; the window (from / to) on every list and rollup; auth and org scoping on every call. A document, reviewed with Tyler and Kamal.
  4. Identity and access without Firebase — sign-in, sign-up or invite, SSO (SAML exists; OIDC added), local accounts for self-hosted, roles (operator, viewer; c128's readers), mocked in the lab. Plus two things ruled on 2026-11-09: the standardised credential an endpoint's native app uses to connect to a plane at a base URL (c131 D13), and mechanic-access grants (c136): time-boxed, categorised access a partner gives Qpoint, held as plane objects.
  5. The install experience — first run: docker compose up, the first admin, connect the first endpoint (the native app's Connect is the other half), the demo world as an option. On c125's wizard kit. With Mark Peterson's install docs.
  6. The port plan — repo, build, how the qdash UI moves out of qcontrol, how dist/ and the Docker image are produced, CI, the cut-over, and what happens to demo.qpoint.io (c113).

Rules

  • R1. API first. The UI reads the API only; no database access from the front end, no Firestore-style live subscriptions unless the API offers a stream.
  • R2. The bible holds. R13 types, R15 primitives, R23 honesty, R25 staged → served swaps, R31 honest states. The port is a transport swap, not a rewrite.
  • R3. One build, two deployments. Nothing in the UI knows whether it is SaaS or self-hosted except by configuration.
  • R4. Self-hosted is first-class. Every flow works with no Qpoint service reachable (updates and telemetry opt-in).
  • R5. Data trust is visible. The app says where data lives and who runs it (Mark's "greater data trust").

Decisions (defaults)

#QuestionDefault
D1Which shellqdash's shell (the global filter, the when: pill, the nav ruled through c92–c128) inside app.qpoint.io's auth and org frame (sign-in, org, context, roles).
D2IdentityOIDC (works with most IdPs and with self-hosted Keycloak-type servers) plus local accounts for self-hosted; the existing SAML routes kept. No Firebase.
D3Multi-tenancyone org per self-hosted install; SaaS runs one image per customer at first (Tyler: "we run that on behalf of our customers"). Multi-org in one instance later.
D4Where the code livesa new control-plane repo holding the Go app and the UI, the UI moved from crates/qdash/ui (Tyler rules). app.qpoint.io stays as the reference until the new app replaces it.
D5The democ113's baked worlds become the SaaS demo and the self-hosted "try it" world.
D6Scope of design's cyclethe six deliverables above; the port itself is engineering's with design in review, recorded here.
D7Alerting and integrationsapp.qpoint.io's alerting rules, adapters and integrations are re-homed into qdash's Alerts and Integrations pages, not rebuilt.
D8Endpoint connection, grants, messagesthe plane exposes a connect endpoint (base URL plus credential exchange), holds mechanic-access grants (c136 D1) and the operator's message inbox (c137 D1). All three are in the API contract (Phase 2).

Phases

0. Inventory (can run now)

inventory.md: both apps mapped, the API gap list, the backend asks consolidated.

1. The shell

One lab mock (/pages/phase4/mock/... or a new lab section): sign-in → org → the qdash home with the global filter. Review stop.

2. The contract

The API document from types/api/*, the routes, the asks, auth and scoping, the window. Reviewed with Tyler and Kamal. This is the hand-off that lets Kamal start.

The six endpoint-only shapes the native app needs (sensor state, resources, effective config, log tail, config file, plane connection) are the document's first new entries: ruled into qdash routes typed in TypeScript and generated into Rust like the other 38 (c131 D17, 2026-11-11), so there is one mechanism for all 44 shapes.

3. Identity and install

Sign-in, invite, SSO, local accounts; first run and connect-the-first-endpoint, on the wizard kit. Mocked in the lab, reviewed one at a time. Install docs with Mark Peterson.

4. The port plan (G4)

Repo, build, Docker, CI, cut-over, demo. Accepted by Tyler and Kamal.

5. The port, with engineering

The UI moves; transports swap to the new API; design reviews each page against the lab; the record here.

Clarifying questions

  1. Where does the code live (D4), and when is Kamal free?
  2. Which of Pulse's patterns survive (the filter, the views)? Is Pulse's source available to read?
  3. Identity: OIDC + local accounts acceptable for self-hosted, or does Tyler have a plan?
  4. One org per install (D3), or multi-org from the start?
  5. Does app.qpoint.io's alerting move (D7), or does qdash's Alerts page replace it?
  6. What happens to demo.qpoint.io and the baked worlds (D5)?

Patterns to consider

  • Generate the API types from the Go handlers (or from a shared schema), and generate the TypeScript and Rust clients from them, so the native app and the web app share one contract (c131 R3).
  • The Connect flow as the join: the native app signs an endpoint in; the web app shows it arrive. Design both halves as one story.
  • The install as a tour (c125): the first-run wizard and the operations journey are the same path.
  • A self-hosted "health" page: the two databases, the version, the last update check; honest states for an operator who runs it themselves.
  • Messages are a resource (c137): the operator's inbox and send are plane objects in the same contract, not a side channel.
  • The pipeline map (brand/pipeline.md) gets its stage 7 rewritten: production install = the control plane image and the native app.

Qpoint Brand Style Guide