Architecture

A deliberate split: the CLI handles intent, the agent handles reality.

Relay is easier to operate because the system boundary is plain. The Node CLI computes local state, applies sync filters, and speaks to the server. The Go agent owns workspaces, lane policy, edge access, diagnostics, container builds, logs, databases, and process control.

Deploy path
1. relay loads .relayignore + cache
2. relay computes manifest
3. agent answers diff plan
4. changed files upload
5. lane policy applies defaults
6. runtime.Build() images
7. container swaps + lane URL prints

relay CLI

Computes the local file manifest, honors .relayignore, and reuses saved hashes when files have not changed.

Uploads changed files or bundles to the current sync session.

Targets a lane and sends the deploy request to the agent.

Runs relay doctor for transport, version, and setup diagnostics before you hit the dashboard.

Streams logs and prints deploy status to the terminal.

relayd agent

Stores app state and deploy history in SQLite.

Exposes /api/doctor so the CLI and dashboard can guide DNS, TLS, Docker, socket, data-dir, and webhook setup with live checks.

Materializes workspaces under the data directory.

Selects buildpacks, writes Dockerfiles, and routes builds through a pluggable runtime backend.

Applies lane defaults for production, staging, dev, and preview, then enforces edge access before traffic reaches the app.

Performs container swaps and keeps one previous image for rollback. Docker is the default backend; Station is available as a per-app experimental alternative.

Storage

Boring storage is a feature.

The data model is intentionally local and inspectable. If something breaks, you should be able to answer basic questions with files and a SQLite shell.

relay.db

Deploy history, app state, lane policy state, secrets, analytics rows, and project service records. Control-plane and analytics paths now use separate SQLite handles against the same file.

logs/

One log file per deploy, readable by humans and streamable to the CLI.

workspaces/

Per-app repo snapshots, staging paths, and sync state between deploys.

plugins/

Server-installed buildpack plugin definitions kept under the data directory.

Workspace shape
data/
  relay.db
  token.txt
  logs/
    <deployId>.log
  workspaces/
    <app>__<env>__<branch>/
      repo/
      staging/
  plugins/
    buildpacks/
Runtime engine

All container operations route through a single interface.

ContainerRuntime abstracts build, run, stop, network, log, and image operations. Docker is the default backend. The engine is stored per-app in app state so Station can still be selected deliberately for workloads that need the experimental native runtime.

ContainerRuntime interface

Covers RunDetached, Remove, IsRunning, ContainerIP, PublishedPort, Exec, NetworkConnect, EnsureNetwork, RemoveNetwork, RemoveVolume, Build, RemoveImage, ListImages, and LogStream.

Docker (default)

DockerRuntime implements all interface methods via the Docker CLI. DOCKER_BUILDKIT=1 is set for builds. This is the backend relayd starts with.

Station (experimental)

StationRuntime is the experimental native container backend. Set engine to "station" in app config. Supports exec, companion services with host-alias routing, and managed named volumes. Docker and Station can coexist on the same server.

Lane policy hooks

Runtime choice is only one part of the deploy contract. Relay also applies lane defaults for routing, traffic mode, retention, and edge access before the rollout starts.

Engine selection
# Set per-app via /api/apps/config
{ "engine": "docker" }   // default
{ "engine": "station" }  // experimental

# lane policy is evaluated separately
# access_policy, traffic_mode, and routing still come from app state + lane defaults
Boundary

Relay is not pretending to be a generic build cloud.

That constraint is what keeps the architecture legible. It is a deploy agent for workloads you own, not a universal remote execution fabric.

See the CLI guide