# ADR-027 — Hosted SaaS distribution and the control-plane/runner split

**Status:** Accepted

**Date:** 2026-07-12

## Context

This repo is being repurposed from the `template-vertical` starter into **Veect** — a design-system-native canvas that connects to a code repository, discovers its real components and tokens, lets a designer compose screens constrained to that system, and publishes real TSX as an ordinary pull request. The product specification bundle lives in `.proto/veect-product-docs/` (tech spec rev 1.2.2, design spec, UI gap spec, PRD, strategy docs).

That bundle **locked** a distribution decision on 2026-07-10: standalone desktop app (Electron, bundled Node runtime) as the default, plus a self-host Docker image, with "do not relitigate" markers. Large parts of the tech spec derive from it: the `node` PATH shim, Electron ABI policy, `safeStorage` credential storage, GitHub OAuth *device flow* (a desktop app cannot hold a client secret), per-workspace SQLite, and the "designer needs no dev environment" assumption (A5) satisfied by bundling the runtime.

On 2026-07-12 the founder superseded that decision during the architecture grill session: **Veect v1 is web-based — a hosted cloud SaaS — with a Tauri desktop shell later.** The spec itself declared the editor UI shell-agnostic and Tauri a documented alternative, so the editor carries over; but "who runs the server" changes everything downstream: auth posture, secret storage, isolation, persistence, and milestone acceptance criteria.

The central new problem: Veect executes **untrusted customer repo code by design** — `pnpm install` lifecycle scripts, `next dev`/Vite servers, arbitrary config plugins. On a desktop that risk belonged to the user's own machine (mitigated by a workspace-trust prompt). Hosted, it lands on Veect's infrastructure and neighbors' tenants.

## Decision

**1. Veect v1 is a hosted cloud SaaS.** Users sign in from a browser; no local install is required for the primary path. Assumption A5 is fully restored — stronger than the Electron plan ever made it. A Tauri desktop shell is a later distribution, not v1.

**2. The architecture splits into a control plane and workspace runners, joined by a single runner protocol.**

| Part | What it is | Where it runs |
| --- | --- | --- |
| **Control plane** | Multi-tenant service: auth, tenancy, workspace/project metadata, design-doc persistence, AI proxying, runner orchestration. Template-shaped: Payload + Postgres, tRPC (ADR-013 stands), features in `packages/*`, UI in `apps/web-next` | Veect-operated infrastructure |
| **Cloud runner** | Executes a workspace's repo code: clone, worktrees, dependency installs, discovery scans, preview adapters, git checkpoint/publish. **One isolated container per workspace**: CPU/mem limits, egress allowlist (npm registry, git host), no cross-tenant network | Veect-operated infrastructure |
| **Local runner** (CLI agent) | The same runner contract implemented as a CLI the developer runs against a local checkout; the browser editor reaches it directly at `127.0.0.1` after pairing with the hosted session | User's machine |

The **runner protocol** (plain WS/JSON — deliberately *not* tRPC, because a slim CLI must implement it and the browser must speak it to a local runner) is the generalization of tech spec §9's "common runtime protocol". One protocol, N runner implementations — cloud, local, and later Tauri-embedded.

**3. v1 connection modes: remote repos (cloud runner) + local checkouts (local runner).** The self-host Docker image is **cut from v1** (fast-follow for enterprise); the core stays 12-factor/deployable-anywhere so it is not foreclosed.

**4. Identity is provider-agnostic.** Email/password accounts via the existing `auth` feature; repo access is a per-workspace credential (PAT or OAuth). The device-flow screens are retired. A GitHub App is a compatible later upgrade.

**5. Control-plane persistence is Payload + Postgres.** Users, workspaces, projects, designs, revisions, and AI runs are Payload collections behind the template's repository pattern. The spec's per-workspace SQLite (§11) is **deleted from v1**: runners hold only ephemeral working state and are rebuildable from the control plane + git. Canonical design state = Postgres (working) + `design.veect.json` on the branch (shared, reviewed) with file-wins reconciliation.

**6. The telemetry and compliance stances flip with the distribution.** "No telemetry v1" was a desktop-privacy posture; operating a multi-tenant service reverses it — the OTel/Sentry stack (ADR-014/017), `core-audit`, `core-consent`, `core-dsr`, and `core-analytics` are **kept and wired** (an EU-entity SaaS with user accounts has GDPR obligations on day one, and the activation/NSM metrics require product analytics). Demo content features (`blog`, `media`, `navigation`, `marketing-pages`) and `apps/web-tanstack` are deleted; `apps/cms` stays as the Payload admin; the landing page becomes a static route in `web-next`.

**7. AI compose is Veect-metered with a BYO escape hatch.** Server-side Anthropic key with per-tenant quotas (Haiku 4.5 default, Sonnet opt-up) *and* an optional per-workspace BYO key override, both from day one. The §14 "what was sent" disclosure is a hard requirement since prompts transit Veect infrastructure.

## Alternatives considered

- **CLI + Docker local-first (`npx veect` + self-host image).** The closest web-based reading of the original spec: repos, secrets, and execution stay on the user's machine. Rejected: reintroduces the install/dev-environment friction for designers that the founder explicitly wants gone; the developer-champion has to run/maintain a server for the team.
- **Docker-only self-host.** Simplest release engineering, strongest data-residency story. Rejected: heaviest first-run, clumsy local-folder support, no zero-install designer path.
- **Electron per spec.** Rejected by the founder's web-first direction; Tauri (not Electron) is the eventual desktop shell, and the runner protocol keeps that path open.
- **Shared-host execution with OS-level separation (cloud side).** Cheapest isolation. Rejected: indefensible to the engineering/security veto persona for untrusted-code execution; container-per-workspace is the industry floor (Codespaces/StackBlitz class), hardening to gVisor/microVMs later without protocol changes.

## Consequences

- **Positive:** zero-install activation for designers (the <20 min time-to-value target gets its biggest lever); the template's machinery survives largely intact (Payload repositories, tRPC, generators, conformance, dev-seed, work pipeline); `InMemoryEventBus`/`PayloadJobsEventBus` and `IJobQueue` are all genuinely useful again; one runner codebase serves cloud, CLI, and future Tauri.
- **Negative / accepted:** repo code and env values now transit and execute on Veect infrastructure — the enterprise trust question sharpens ("your code leaves your machines") and needs a security FAQ before design-partner outreach; per-workspace compute (installs, dev servers) is a real COGS line requiring hibernation/pooling; the local runner is a second runner implementation to maintain from v1; publishes author as the workspace's stored credential (multi-seat honesty must be surfaced in the publish UI).
- **Retired spec surface (do not implement):** Electron shell, bundled-runtime PATH shim, ABI policy, `safeStorage`, auto-update/adapter-drain, deep links, GitHub device flow, per-workspace SQLite, self-host Docker (v1), SSH-via-system-agent for cloud runners (returns with the local runner, where an agent exists).
- **Milestones M0–M7 must be re-derived**: the M0 acceptance ("boots with no system Node") is meaningless hosted; its replacement is the walking-skeleton PRD (sign-in → connect fixture repo → cloud runner → one real component in the editor).
- The `.proto` bundle remains the product-behavior authority **except** where this ADR and the grill decisions supersede it; the authority table in `.proto/veect-product-docs/README.md` §2 is amended by this ADR.

## Related

- ADR-013 (tRPC conventions — stands), ADR-014/017 (instrumentation — exporter layer now stays), ADR-015 (events/jobs — both impls in use), ADR-016 (realtime — control-plane pushes), ADR-025 (EU compliance baseline — applies for real now)
- ADR-028 (iframe canvas), ADR-029 (DesignDoc v1 + editor rebuild)
- `.proto/veect-product-docs/veect-technical-spec.md` rev 1.2.2 (superseded on distribution, §3/§6/§11 partially retired)
- Glossary: "Veect product domain" section in `docs/glossary.md`
