← Projects
DeckIn developmentLive demo ↗
deck / project specs

Architecture decisions

Why Deck is local-first, what was discarded (remote relay, Node server) and the trade-offs of each choice.

How to read this

Each decision follows the same format: context, what was decided, alternatives considered and consequences. The date is when the decision was made.

1. Always local execution (2026-10)

Context. The initial idea was to trigger actions from anywhere (phone, another computer, a cloud AI agent) without having to open the panel. Execution, however, has to happen on the machine: that is where the repositories, git, the authenticated CLIs (az, gh, docker), Playwright and the secrets live.

Alternative considered: a cloud relay. A Cloudflare Worker with one Durable Object per machine, D1 for jobs and auditing, R2 for logs, and an agent on the machine connected through an outbound WebSocket. Clients: deck --remote, an MCP server for AI agents and the hosted panel.

Adopted: everything local

Local browser

deck ui (127.0.0.1)

Execution

Discarded: remote relay

Phone / cloud agent

Relay (Worker + DO + D1/R2)

Agent on the machine

Adopted: everything local

Local browser

deck ui (127.0.0.1)

Execution

Discarded: remote relay

Phone / cloud agent

Relay (Worker + DO + D1/R2)

Agent on the machine

The security and cost analysis of that design found problems that changed the project:

ProblemWhy it mattered
The relay becomes a single point of compromiseWhoever controlled the cloud account could issue commands to every machine
Client data leaving the machineSQL, logs and PRs would go through D1, R2 and an LLM’s context, with LGPD and contractual implications
The AI agent would confirm its own destructive actionA confirm: true parameter offers no protection when the sender is the model, which is subject to prompt injection
The allowlist only saw top-level commandsAllowing db would allow db script; the registry had no types to validate arguments
Recurring costsPaid Workers, Durable Object duration, R2 for reports and LLM tokens with large catalogs
Company policyA daemon offering remote control of a work machine tends to clash with corporate security rules

Decision. Everything local. The panel is served by the binary itself on 127.0.0.1; no data leaves the machine; there is no hosted backend.

Consequences. No access from a phone or another machine. If that becomes necessary again, the path is a private network (Tailscale, for example) to deck ui, never a public relay.

2. One binary, no Node server (2026-10)

Context. The panel was a separate Next.js server that called the deck binary through execFile. That meant two processes, two languages maintaining the same configuration and the same validation, and a Node runtime on the user’s machine.

Alternative considered: a hosted “shell” site. Publish the screens on a domain and have the browser talk to deck on localhost. Discarded because the published JavaScript would end up commanding the machine of whoever opened it (a compromised provider account would become an attack vector), it would require CORS, and it would run into the local network access restrictions browsers have been adopting.

Decision. The screens become a static export embedded in the binary with go:embed; deck ui serves the pages and answers the API in Go. The public demo still exists, but only with fictitious data and with a CSP that prevents it from talking to localhost.

Consequences. A single artifact to distribute and update; configuration and validation live only in Go; Node becomes a build tool. The 22 Next routes were rewritten in Go.

3. The client never sends flags (2026-10)

Context. Validating arguments after receiving them (blocking ;, $(...)) does not address the real risk when there is no shell: the danger is the extra argument, such as --force, --delete-source or --sql -.

Decision. Each panel action is declared on the server with typed parameters; the server builds the argv. Extra fields in the request body are ignored.

Consequences. Every new action requires a declaration and two tests (argv and rejected input). It is more code per action, in exchange for no arbitrary flag ever reaching a process.

4. No job queue or live output (2026-10)

Context. The plan included jobs with output over Server-Sent Events.

Decision. Do not build it. No screen needs streaming output: queries are fast and the E2E suite responds at the end, with the “Abort” button available during the run.

Consequences. Less code and less state on the server. If a screen ever needs a live log, jobs will be built along with it.

5. No confirmation token for destructive actions (2026-10)

Context. In the remote design, destructive actions would require a confirmation token issued by the server.

Decision. Do not use it in the local panel. Whoever could call the action could also request the token; the real protection is the middleware (host, same origin, session cookie, JSON-only POST). The screens still ask the user for confirmation before destructive actions.

6. Free-form SQL stays in the panel (2026-10)

Context. The remote plan restricted the panel to catalog queries.

Decision. Keep the SQL editor, in a read-only transaction by default, with writes only after confirmation and never on production profiles. Locally, the panel has the same reach as the terminal, and the editable catalog already allowed arbitrary SQL.

7. The database password does not go to the browser (2026-10)

Context. The “Connect” screen received the resolved passwords to build connection strings.

Decision. The profiles route returns only the secret’s name and whether it is set; snippets use the <senha> placeholder.

Consequences. Copying a ready connection string requires replacing the placeholder with the value. One extra step, in exchange for the password not sitting in page memory, browser extensions or clipboard history.

8. Releases signed with Ed25519 (2026-10)

Context. deck update checked the SHA-256 against the checksums.txt of the same release: integrity, not authorship. With the panel embedded in the binary, compromising a release would also compromise the interface.

Decision. CI signs checksums.txt with a private key stored as a repository secret; the public key is embedded in the binary; a release without a valid signature is rejected. The Ed25519 implementation from Go’s standard library was used, with no new dependencies (minisign and cosign were considered).

Consequences. The private key must be kept carefully. Rotating it requires publishing a version with the new public key and a manual update for anyone on older versions.