Design Patterns, Tradeoffs & The V1→V2 Story
What patterns recur, what they cost, what's clever, and what to look out for.
This doc is the editorial layer over the previous four. Read after 01-architecture.md.
1. Recurring Patterns
1.1 Provider injection (host-service)
packages/host-service/src/app.ts's createApp({ config, providers }) is a clean example of dependency inversion. The host-service doesn't know:
- where the DB file is (config)
- how to authenticate (
HostAuthProvider) - how to talk to the cloud API (
ApiAuthProvider) - how to get git tokens (
GitCredentialProvider) - how to resolve LLM model creds (
ModelProviderRuntimeResolver)
Same code runs on a laptop with a KeychainCredentialProvider and on a Linux container with a FileBasedCredentialProvider. The boundaries doc (HOST_SERVICE_BOUNDARIES.md) is itself an artifact of this pattern — a contract written down so future work doesn't accidentally re-couple.
1.2 tRPC everywhere
Same protocol over IPC, HTTP, and WebSocket. The renderer can do electronTrpc.foo.bar.useQuery() and localHostService.foo.bar.useQuery() with identical ergonomics. End-to-end types from a single source of truth (packages/trpc/).
The cost: subscriptions over IPC must be observables (per apps/desktop/AGENTS.md) — async generators don't work; this is a recurring footgun for new contributors.
1.3 MCP and tRPC share business logic
defineTool(server, {
name: "automations_create",
inputSchema: { … },
handler: async (input, ctx) => caller.automation.create(input),
});The MCP handler is a thin shim over a tRPC caller. One implementation, two surfaces (developer-facing tRPC API + agent-facing MCP). Deepens the language of the system rather than maintaining parallel logic.
1.4 Manifest-based process adoption
~/.superset/host/{org}/manifest.json is the rendezvous file. Electron quits, host-service stays alive (release mode), Electron restarts and adopts. PTY daemon does the same trick. The manifest is small ({ pid, endpoint, authToken, startedAt, organizationId }) and the discovery logic re-validates each one before adopting — defensive against dead pids.
1.5 PATH rewriting as instrumentation
Rather than fork the agent CLIs to add hooks, Superset prepends ~/.superset/bin/ to PATH and drops shim scripts there. Combined with rewriting the agent's own hook config files (~/.claude/settings.json, ~/.codex/...), it gets observation without modification. Idempotent rewrite blocks are marker-fenced so manual edits to the same files don't break either side.
1.6 Discriminated unions + exhaustiveness
Patterns like ResolvedRef (in the v2 git-ref work) define template-literal fullRef types and force switch(kind) blocks to handle every case via a never default. The repo includes a lint script (scripts/check-git-ref-strings.sh) that bans .startsWith("origin/") outside refs.ts so contributors cannot regress to string-typed refs.
1.7 Event-sourced log + pure reducer
The v2 chat plan (plans/v2-chat-greenfield-architecture.md) commits to a single event log per session with monotonic seq, a pure applyEvent(state, event) reducer on the client, gap detection on receive, and replay endpoints. This is the standard Redux/CQRS playbook applied to a chat surface — the win is multi-device convergence (every device replays the same log to identical state).
1.8 Authority by ownership
Recurring decision: who is the source of truth?
| Question | Authority |
|---|---|
| "Does this branch have a workspace?" | Cloud DB (host-stale caches lose) |
| "What's the fork-point for diffs?" | Branch's tracked upstream (not hardcoded origin/main) |
| "Who owns chat events?" | Event log (one writer per session) |
| "When two refresh-token attempts collide?" | Postgres advisory lock (single-flight) |
| "What's a workspace's port range?" | Local SQLite port_base |
| "Is the host online?" | Heartbeat-driven v2_hosts.online |
Writing these down (rather than hoping consistency emerges) is the lesson.
1.9 Cloud-synced collections as primary state
The pendingWorkspaces Electric collection is read in the renderer as if it were a local table — but writes propagate through the cloud and back. This unwinds the "is the workspace ready yet?" question across processes and devices.
1.10 Per-agent dialect templates
contextPromptTemplate.system / .user per agent (Claude → XML, Codex → markdown). Same LaunchSource[] composition; different surface form. Adding Gemini/Pi/etc. = adding a template + a preset, not branching if (agent === "claude") everywhere.
1.11 Bytes through IPC, encode at boundaries
Attachments travel as Uint8Array between renderer/main/host-service; the AI SDK's Anthropic provider does the base64 conversion exactly once at the API boundary. Avoids both performance hits (re-encoding) and bugs (string truncation).
1.12 Heredoc with random delimiter for prompt-as-shell-arg
buildPromptCommandString (packages/shared/src/agent-prompt-launch.ts:26-68). The defensive coding standard for moving arbitrary text into a child process's argv when the binary's flag schema is out of your control.
2. Notable Tradeoffs
2.1 Worktrees over containers
Pro: native git, cheap, plays well with local IDE/dev servers, no Docker dependency.
Con: no security isolation between worktrees (they share ~), no resource quotas, no portability to remote machines (which is why the v2 host-service is being designed to run remotely separately).
The container/VM path is left to the user via setup scripts.
2.2 Bun everywhere except node-pty
Pro: speed, single runtime, single PM (bun).
Con: the PTY daemon runs Node, complicating the build (compile:app checks the bundle, validate:native-runtime checks runtime modules, etc.). If anything else needs node-pty's tty.ReadStream, it must live in the daemon too.
2.3 Two databases
Cloud Postgres + local SQLite, with Electric SQL bridging only the queue table. Most data is single-sided:
- Tasks → cloud-only (synced to Linear/GitHub but not to local SQLite)
- Workspaces (operational state) → local SQLite
- Cloud
v2_workspacesrow exists only as a "this exists somewhere" registry
Pro: less sync surface area, simpler invariants. Con: mental model overhead — readers must know which table is authoritative for which question.
2.4 V1 + V2 in parallel
Two of every entity (projects/v2_projects, workspaces/v2_workspaces, device_presence/v2_hosts). New code goes to v2; old code stays.
Pro: zero cutover risk; ship v2 incrementally.
Con: doubled vocabulary, doubled tests, deferred consolidation cost. Per plans/, the team is shipping V2 of every flow first, then will sunset V1 — a pragmatic but expensive strategy.
2.5 Polling vs events
V1 chat polls getDisplayState() and listMessages() independently at 4 fps from two harness sources, racing on the client. V2 replaces it with an event log. The pivot exposes the cost of "easy" polling — subtle race bugs that are invisible until multiple devices observe the same session.
2.6 Better Auth instead of Clerk
Self-hosted auth = more control + lower cost + custom plugins (organization, apiKey, stripe, expo); fewer hosted batteries. Trusted-origin lists, refresh-token discipline, OAuth flows — all in tree.
2.7 Mastracode + Vercel AI SDK
Mastra owns the agent loop (system prompt, tool-use, hooks, approvals). Vercel AI SDK is used at the edges (small-model one-shots, UI streaming hooks). Two LLM frameworks to maintain, but each is best at its layer; combining them avoids forcing one to do the other's job.
3. The V1 → V2 Transition (Strategic Narrative)
| Theme | V1 (Electron-monolithic) | V2 (distributed, event-driven) |
|---|---|---|
| Process model | Electron main + terminal-host daemon | Electron + standalone host-service + supervised PTY daemon |
| Workspace creation | Tightly coupled to renderer | pendingWorkspaces Electric collection + dedicated page |
| Branch model | Hardcoded origin/main |
BaseBranchSource carried through chain |
| Diffs | Latest-vs-latest (creeping numbers) | Fork-point (merge-base) — stable |
| Chat | 4 fps polling, two sources, races on client | Event-sourced log, monotonic seq, pure reducer, gap replay |
| Devices | Single-device assumption | v2_hosts (machines) × v2_users_hosts (access) |
| Notifications | Played in Electron main | Client-side, enabling remote hosts |
| Agent launch | Flat-string prompt | Composed LaunchSource[] → LaunchSpec with cache hints |
| Workspace state | Defensive auto-delete on local stale cache | Cloud-authoritative; local cache is hint-only |
Active work-in-progress (from plans/):
20260501-linear-team-entity.md— Linear OAuth refresh-token rotation (urgent: Linear migrated to 24h tokens 2026-04-01) + per-team key/sequence model.20260502-bun-dev-server-cleanup.md— orphan process leaks in dev (sh -c exec,detached: !isDev).20260430-pane-store-registry-pr3.md— pane state registry refactor.20260430-v2-delete-workspace-audit.md— workspace deletion correctness.20260427-posthog-v1-v2-dashboard.md— analytics for the v1/v2 split.20260425-host-attachments-pr2.md— file attachment lifecycle on host-service.20260422-v2-notification-hooks-client-side.md— notification handling moves to renderer.20260422-v2-remote-ports.md— port allocation when host is remote.20260417-automations.md— automation dispatch flow.
Done (in plans/done/):
20260417-v2-project-create-import-impl.md— fork/checkout/adopt branches.v2-workspace-context-composition.md— structured agent launch spec.
4. Conventions Worth Adopting
The repo's AGENTS.md and per-app AGENTS.md files codify some specific rules; the underlying philosophy is more general. A few worth borrowing:
- Plans go in
plans/(cross-cutting) orapps/<app>/plans/(app-scoped); shipped plans move toplans/done/. Never*_PLAN.mdat app root. - Architecture/reference docs go in
<app>/docs/, notsrc/. Code is for code; docs sit beside it. - One source of truth for shared resources — slash commands live in
.agents/commands/;.claude/commandsand.cursor/commandsare symlinks. MCP servers configured once in.mcp.json; clones are symlinks. Codex uses.codex/config.toml. OpenCode mirrors the set inopencode.json. - Biome at root, not per-package. Speed first.
ghovergitfor PR/issue ops. Saves a layer of translation.- No fork tarball overrides for upstream packages unless explicitly requested. Prefer published
mastracode/@mastra/*. - Type safety over
any. Discriminated unions, exhaustiveness checks. - Co-locate tests/utils/hooks/components. Folder-per-component (
MyComp/MyComp.tsx + MyComp.test.tsx + index.ts); promote tocomponents/only when used 2+ places. - Drizzle migrations are auto-generated. Never edit
packages/db/drizzle/*.sqlormeta/_journal.json. - Use
ask_user(Superset MCP) for interactive Qs in agent runs, not plain text.
5. Things That Are Unusual / Clever
- The five-process architecture with manifest-based adoption is unusual for an Electron app. Most desktop apps couple their lifecycle to Electron's; Superset deliberately decouples agent runtime from app uptime.
- Selling MCP tools to its own internal harness. Superset's chat runtime calls Superset's own cloud MCP server via Mastra's
MCPClient. This means the agent inside Superset has the same tool surface as a third-party agent connecting from outside. Deduplication of intent. - PATH-rewriting + idempotent hook injection lets it instrument any agent vendor's CLI without forks. The pattern is older than Superset (think shell wrapping) but its application here is end-to-end.
- Setting
actor=appon Linear OAuth. Issues authored by the Superset app rather than the human. Subtle but right — preserves audit clarity when an agent files an issue. - Three workspace creation intents (fork/checkout/adopt) → one UX. Most products would have three buttons; Superset hides the dispatch in one modal with one input that classifies intent.
- Per-agent prompt dialects via Mustache templates rather than per-agent code branches.
disconnectedAt+disconnectReasonon integration connections, surfacing token-expired states to the UI rather than letting the next API call 401.secretstable per org × project, encrypted, never read into the main process — passed straight to the worktree env via host-service.
6. Pitfalls / Lessons Already Learned
- Auto-deleting "ghost" cloud rows when the host can't find them on disk caused data loss. Fix: workspace state is authoritative in the cloud; the adopt intent specifically reclaims orphans.
- Hardcoded
origin/mainbroke fork-repo contributors (whose upstream is their fork, not the project). Fix: branch-tracked upstream + lint check. - Latest-vs-latest diffs cause file counts to creep as collaborators merge. Fix: fork-point (
Flavor 2) for stable comparisons, plus separateFlavor 3(commit list). - Polling chat from two sources raced on the client, producing impossible UI states. Fix: single event log.
- Linear OAuth tokens went short-lived without warning; the refresh pipeline didn't exist. Fix in flight:
refreshTokencolumn + advisory-lock single-flight + 401 retry + UI signal. - Orphan dev processes leaked during
bun run devbecause of howelectron-vitespawns. Fix:sh -c exec,detached: !isDevguards. - Skill-preload was forked from upstream Mastra to ship early. Removed once upstream native
search_skills/load_skilllanded — better than maintaining the fork. - Anthropic OAuth via Claude Code's flow required special headers — the small-model resolver covers this so other code doesn't need to.
7. Open Questions / Where The Engineering Bets Lie
- Cloud chat backend (P5a vs P5b in
v2-chat-greenfield-architecture.md). Postgres + LISTEN/NOTIFY + advisory-lock leases vs Cloudflare Durable Objects with hibernation. P5b is cheaper at idle but locks chat-without-cloud out of offline. Decision deferred until P0–P3 stabilize. - V1 sunset. Parallel-universes coexistence is durable but expensive; deciding when to delete v1 is a deferred-cost question.
- Multi-device convergence at scale. The v2 chat architecture is designed for it but unvetted on real workloads.
- Remote-host UX. Host-service can run remotely; the renderer can talk to it directly; but credential boundaries when "your laptop's host-service" = "a Lambda" need care (
host-service-chat-architecture.mdhas open notes on this). - Performance at large org scale: branch picker pagination, chat event rates, pane layout updates with hundreds of tabs.
8. The Editorial Take
Superset is one of the most architecturally disciplined developer-tool codebases I've seen at this stage of a startup. Several things stand out:
- Boundaries are written down.
HOST_SERVICE_BOUNDARIES.mddoesn't just describe the contract — it names the things that cannot be in host-service (Electron paths, keychain, default DB locations). That's the work. - Plans are first-class.
plans/is a living directory. Every major refactor has a plan checked in; the plan mentions trade-offs explicitly (Postgres vs DO, V1 vs V2 sunset). - Auto-generated artifacts have firewalls around them. The
packages/db/drizzle/rule, thescripts/check-git-ref-strings.shlint, the no-fork rule for Mastra — each is a small bit of social/automated friction against regression. - The product itself dogfoods at the level of the architecture. Slash commands shared across Claude/Codex/Cursor via symlinks; MCP servers configured once; agent presets factored as data not code; AGENTS.md files at every level.
The principal risks are the ones flagged above: V1 sunset cost, multi-device chat untested at scale, and the (already-mitigated) trap of believing local cached state about something the cloud actually owns.
If you're picking up this codebase for the first time, the order I'd recommend is:
- README.md — 5 min
- AGENTS.md (root) — 10 min
apps/desktop/HOST_SERVICE_ARCHITECTURE.mdandHOST_SERVICE_BOUNDARIES.md— 20 minapps/desktop/V2_WORKSPACE_CREATION.md— 15 minpackages/shared/src/agent-prompt-template.ts— 10 min (small, dense)apps/api/MCP_TOOLS.md— 10 minplans/v2-chat-greenfield-architecture.md— 30 min if you want the spice