← Projects
DeckIn developmentLive demo ↗
deck / project specs

Architecture

How the deck binary serves the static panel and the local API, and runs commands without a shell.

Overview

Deck is a single Go executable, deck. It has two modes that share the same code:

  • CLI: deck <command> [flags], for the terminal and for AI agents.
  • Panel: deck ui, which starts an HTTP server on 127.0.0.1:4004 and opens the browser.

The panel screens are written in Next.js (React 19, Tailwind 4 and @thecodecodex/shell), but no Node server runs: Next is only used at build time to generate a static export (HTML, CSS and JS). That export is embedded in the binary with go:embed. At runtime, Go itself serves the pages and answers the API.

Runtime (user machine)

Build (CI or developer machine)

npm run build

copied to

go:embed + go build

exec, arguments as an array

src/ (Next.js)

static export

internal/hubserver/web/

deck binary

deck ui

/ → embedded pages

/api/* → Go handlers

deck command · gh · az · node

Runtime (user machine)

Build (CI or developer machine)

npm run build

copied to

go:embed + go build

exec, arguments as an array

src/ (Next.js)

static export

internal/hubserver/web/

deck binary

deck ui

/ → embedded pages

/api/* → Go handlers

deck command · gh · az · node

Components

PieceWhereResponsibility
CLIcmd/deck, internal/commands/registry.goSingle command catalog: menu, --help, --schema and dispatch all come from it
Modulesinternal/gitworkflow, internal/azure, internal/database, internal/mocks, internal/setup…The logic of each domain
Configurationinternal/configReads and validates deck.yaml
Panel serverinternal/hubserverEmbedded pages, security middleware and the /api/* routes
Screenssrc/app, src/componentsStatic export; they only talk to same-origin /api/*
Public demosrc/demo, npm run build:demoThe same export with fictitious responses instead of the API
Updatesinternal/selfupdatedeck update, channels, rollback and signature verification

The path of a click

Moving an Azure Boards card from the panel:

deck boards-move-cardHandler /api/boardsMiddleware (deck ui)Page (browser)deck boards-move-cardHandler /api/boardsMiddleware (deck ui)Page (browser)UserClicks "Code Review"POST /api/boards {action, id, state}Local host? same origin? session cookie? JSON?forwardsis id a number? is state a board phase?exec deck boards-move-card --id 1234 --state "Code Review"output{success, output}Card movedUser
deck boards-move-cardHandler /api/boardsMiddleware (deck ui)Page (browser)deck boards-move-cardHandler /api/boardsMiddleware (deck ui)Page (browser)UserClicks "Code Review"POST /api/boards {action, id, state}Local host? same origin? session cookie? JSON?forwardsis id a number? is state a board phase?exec deck boards-move-card --id 1234 --state "Code Review"output{success, output}Card movedUser

Key points of this path:

  • The client sends values, never flags. The body carries id and state; the server decides that they become --id 1234 --state "Code Review". See Security.
  • Each action is a child process of the binary itself. The handler does not reimplement business rules: it calls the same command you would run in the terminal. Panel and CLI never diverge.
  • No shell. The process is created with arguments as an array; ;, $(...) or | in a field are just text.

The local API

All routes live in internal/hubserver, one per domain:

RouteFileWhat it does
/api/configconfig.goNon-secret part of deck.yaml for the screens
/api/gitgit.goMulti-repo dashboard and batch actions
/api/boardsboards.goCard, features and the branch/PR workflow
/api/githubgithub.goPRs, my PRs, pipelines and workflow dispatch
/api/cloud-logs, /api/cloud-logs/authlogs.go, azure.goLog search and Azure CLI device-code login
/api/run-tests, /api/run-historytestes.go, historico.goPlaywright: list, run, abort, history
/api/coberturacobertura.goReport status, running tests with coverage and serving the HTML report
/api/launcher, /api/mocksservicos.goService ports, compose, mocks
/api/database/*database.goQuery catalog, execution and profiles (no passwords)
/api/setup-envsetup.goDiagnostics and installation steps
/api/cli-releaserelease.goInstalled version, releases and binary downloads
/api/skillsskills.goThe team’s agent skills

Every route has a plain JSON contract. Input errors come back as HTTP 400 with a readable message; a missing external binary (for example, gh not installed) comes back as 503.

Process execution

Every process goes through executar.go:

RuleWhy
Arguments as an array, never a shellNothing the user types is interpreted as syntax
stdin closedA command that asks a question fails immediately instead of hanging
Own process groupTimeout and “Abort” kill the whole tree (including Playwright’s Chromium, the git called by gh…)
Per-action timeoutFrom 8 seconds (gh queries) to 60 minutes (E2E suite)
Output capped at 10 MBA huge log does not bring the server down

Long runs, such as the E2E suite, do not depend on the browser tab: closing the tab does not orphan Playwright, and the “Abort” button is how you stop it. Only one E2E run happens at a time.

Local state

deck ui has no database. The little state there is lives in local files, outside git:

DataWhere
E2E run history and the copied error-context.md files<root>/.data/run-history.json and .data/run-history-artifacts/
Queries created or edited in the panel<root>/.data/queries-customizadas.json (the project catalog is never rewritten)
Team skills<root>/.claude/skills/<id>/
Secrets (passwords, tokens, PAT)Secrets directory, outside the repository (deck secrets path)

The CLI and the panel read and write the same files: deck tests history shows runs started from the panel.

Development mode

To work on the screens with hot reload, npm run dev starts two things:

  • next dev on port 4005 (loopback only), serving the pages with hot reload;
  • deck ui --dev http://127.0.0.1:4005 on port 4004, which proxies the pages to Next and answers the API in Go.

You open http://localhost:4004 as usual. --dev only accepts an http loopback target with a port.