Skip to content

Architecture

frogg is a client-server system. A Node daemon runs agents and owns all state. Clients connect to one or more daemons to watch and control those agents. Closing a client does not stop agents.

Electron desktop Expo app CLI (frogg)
(app-only shell) web / iOS / Android
| | |
| local socket/pipe, | WebSocket, direct | WebSocket, direct
| SSH, direct, relay | or via relay | or SSH
+-----------+-----------+----------+-----------+
| |
v v
+------------------------------------------+
| Daemon (@frogg/server, Node) |
| HTTP: /ws /api/* /mcp/agents web UI |
| agents, projects, workspaces, worktrees |
| terminals, schedules |
+---------------------+--------------------+
|
provider processes (Claude, Codex, ACP agents, ...)
|
$FROGG_HOME on disk

Daemon (packages/server). Spawns and supervises agent processes, streams their timelines, manages projects, workspaces, worktrees, terminals and schedules, and enforces authorization. Agent state stays authoritative on the daemon across client disconnects. The daemon is installed separately (install script, daemon bundle, Docker or frogg CLI) and listens on 0.0.0.0:9999 by default. FROGG_LISTEN, daemon.listen in config.json, or --listen override it. The listen target can also be a Unix socket or a Windows named pipe.

Expo app (apps/ui, package @frogg/app). One React Native codebase for web, iOS and Android. Its web export is also the desktop renderer and the optional web UI the daemon can serve.

Desktop (apps/desktop-electron). An Electron shell around the Expo web export (shipped as app-dist). The renderer runs sandboxed with context isolation; the preload exposes window.froggDesktop. The main process owns native features: windows, notifications, updates, SSH transports and remote daemon deployment. The desktop is app-only. It never bundles, starts or stops a daemon, and the packaged smoke test asserts that.

CLI (apps/cli, package @frogg/cli, binary frogg). A command-line client over the same WebSocket protocol, plus daemon lifecycle and service commands.

All clients speak one WebSocket protocol. Schemas live in packages/protocol/src/messages.ts.

  • Direct. A WebSocket upgrade on the daemon HTTP server at /ws, over TCP, or a local socket or pipe from the desktop main process.
  • SSH. The desktop main process and the CLI (--host ssh://user@host) run the local OpenSSH client as ssh -T -W 127.0.0.1:<daemonPort> <host> and carry the WebSocket over its stdio. The remote daemon must already be running. The tunnel never installs or starts it.
  • Relay. An optional relay bridges clients and a daemon that cannot accept inbound connections. Payloads are end-to-end encrypted with NaCl box (tweetnacl), so the relay only routes ciphertext. The official brand configures no relay endpoint. You must run your own; packages/relay includes a Cloudflare Workers adapter.

Protocol rules for adding or changing messages are in Coding standards.

Path Package Role
apps/ui @frogg/app Expo client for web, iOS, Android and the desktop renderer
apps/desktop-electron @frogg/desktop-electron Production Electron app-only shell
apps/cli @frogg/cli frogg command-line client and daemon launcher
apps/desktop @frogg/desktop Inactive Tauri shell. Reference source only
apps/daemon-rs none (Cargo crate) Inactive Rust daemon. Reference source only

apps/desktop and apps/daemon-rs are excluded from releases. Do not build features on them. apps/desktop is still an npm workspace, so root typecheck and build include it.

Path Package Role
packages/protocol @frogg/protocol Zod wire schemas, message types, generated validators, SSH tunnel args
packages/client @frogg/client Daemon WebSocket client (DaemonClient) and the FroggClient SDK facade
packages/server @frogg/server The daemon
packages/relay @frogg/relay Relay transport and E2E encryption
packages/highlight @frogg/highlight Syntax highlighting
packages/branding @frogg/branding Build-time product identity
packages/expo-two-way-audio @frogg/expo-two-way-audio Native module for two-way audio streaming

Other top-level directories: brands/ (brand manifests, brands/frogg is official), deploy/ (Docker, Nix, install scripts), skills/ (agent skills), and scripts/dev|ci|release|mobile.

@frogg/protocol is the source of truth for everything on the wire. It depends on nothing in the server. Server, app, CLI and @frogg/client all import it from its built dist/, which is why protocol changes need npm run build:client before downstream typechecks see them.

@frogg/client exports DaemonClient through @frogg/client/internal/daemon-client and the higher-level FroggClient from the package root.

Product identity is data, not code. brands/<id>/brand.json defines the name, CLI name, daemon port, env prefix, URL scheme, home directory, artifact prefixes and service URLs. npm run brand:prepare resolves the brand selected by FROGG_BRAND_DIR (default brands/frogg) into .generated/branding/. That output is never committed. Runtime code reads it through brand from @frogg/branding. See Fork and rebrand for the full model.

The daemon stores state as files under $FROGG_HOME. It resolves FROGG_HOME (the brand’s <ENVPREFIX>_HOME), then falls back to ~/.frogg. Dev scripts use .dev/frogg-home in the checkout.

Path Contents
config.json Persisted daemon configuration
agents/ Agent records as JSON
agent-timelines/ Agent timeline data
projects/ projects.json and workspaces.json
worktrees/ Worktrees the daemon creates
uploads/ Uploaded files
daemon.log Daemon log (rotated)
runtime/ Runtime files such as managed-process state

This table lists the main entries only. See Configuration for config.json.

The daemon serves an MCP endpoint at /mcp/agents on its HTTP server. Agents it launches get this endpoint injected so they can use frogg tools: workspaces, agents, permissions, schedules and heartbeats. The tools come from a transport-neutral catalog in packages/server/src/server/agent/tools/; MCP is one adapter over it. The route authenticates its own requests rather than using the daemon bearer check.