Architecture

Instance API architecture

The TurboPanel API lives in the turbopanel/turbopanel repository at ~/instance. It is a Hono application with dual runtimes: Cloudflare Workers (src/workers.ts + wrangler) and Deno (src/deno.ts + Unix socket).

Versioned surfaces

Four versioned surfaces each have REST + WebSocket namespaces (where applicable). Prefixes live in src/surfaces.ts. GET /api/health is the single unversioned probe.

SurfaceRESTWebSocketAudience
Client/api/client/v1/*/ws/client/v1End-user UI
Developer/api/developer/v1/*/ws/developer/v1Dev console (__DEV__ only)
Install/api/install/v1/*Self-hosted install wizard (Deno only)
Daemon/api/daemon/v1/*/ws/daemon/v1Remote daemons

Unversioned docs:

Folder layout (instance/src/)

AreaFilesRole
App shellapp.ts, surfaces.tscreateApp() mounts routers, health, OpenAPI, Scalar
Clientclient-routes.ts, auth/*Sign-in, org servers
Installinstall-routes.ts, auth/install-state.tsSelf-hosted install wizard (/api/install/v1/*, Deno only)
Developerdeveloper-routes-core.ts, developer-routes.tsFleet, database (Deno-only extras in developer-routes.ts)
Daemondaemon-api-routes.ts, daemon-hub.tsDaemon registration, CA fetch, WS hub
Deno-onlydeno.ts, system-routes.ts, dev-sync.ts, tunnel-routes.tsUpgrade, reset-dev, dev-sync push
Databasedb.ts, db/schema.tsDrizzle + postgres.js (prepare: true on Workers/Hyperdrive for cached parameterized reads; prepare: false on Deno/direct Postgres)
Workers entryworkers.tsWrangler main; registers developer-routes-core only

Workers bundle rule: Deno-only modules must not be imported from app.ts or workers.ts — register Deno routes only from deno.ts.

Routing flow

In self-hosted production, Caddy terminates TLS and proxies /api/* and /ws/* to the Deno Unix socket. In local dev, Caddy proxies to the Deno Unix socket (default) or wrangler (Workers mode).

Authentication

Custom auth built on the Web Crypto API (no nodejs_compat):

Client auth routes under /api/client/v1/auth/*. Install wizard routes live under /api/install/v1/*. See the instance repo AGENTS.md for the full route table.

Secret envelopes

Persisted secrets (variables, TLS private keys, principal passwords) use a single root of trust (TURBOPANEL_SECRET / TURBOPANEL_SECRETS) and the universal at-rest format tpsecret. At delivery the instance re-seals to recipient-bound tpdaemon(serverId, keyId) via resealSecretForDaemon; daemons decrypt only those envelopes. The client surface encrypts only (optional show-once generate). There are no per-server at-rest keys — a credential is server-agnostic in Postgres and can be delivered to any authorized daemon. Key rotation uses lazy re-seal-on-write plus superadmin POST /api/admin/v1/secrets/reencrypt. Operational detail: Security → Secret management.

Database

Single PostgreSQL 18 database per environment. Schema groups:

GroupTables (examples)
Identityuser, account, session, verification, …
Organizationsorganization, member, team, invitation, …
Configsetting
Runtimeserver

Schema changes are versioned in instance/migrations/ and applied via pnpm migrate (Workers deploy and co-located dev converge). Deno dev can still use drizzle-kit push (dev/scripts/sync.sh) for quick iteration without committing SQL. See instance/src/lib/db/AGENTS.md.

Compose documents & server placement

project.options.compose / environment.options.compose store a versioned ComposeDocument (version: 1, data, presentation) so YAML comments, blank lines, and section order survive editor round-trips. Deploy strips presentation to runtime YAML and merges the environment overlay onto the project base. The compose library lives at instance/src/lib/compose/.

x-turbopanel vendor extension

TurboPanel reserves a top-level x-turbopanel key. This is a standard Docker Compose x-* vendor extension, so docker compose itself ignores it — the key round-trips untouched through composeDocumentToRuntimeYaml.

FieldPurpose
placement.server_idEnvironment → whole-server pin (UUID)
viewCompose UI tab preference: editor or visual (per project base and per environment overlay)

Environment pinning (today)

placement.server_id is read from the environment overlay compose and pins that environment to one whole server. Different environments in the same project may pin to different servers. The value must be a server UUID in the same organization (validated; unknown or stale ids fail cleanly). Project base compose does not own placement — stale project-level pins are ignored (hard cut) and stripped before merge into runtime YAML.

view is written when the compose Editor or Visual tab is saved. It is hidden from the YAML textarea (like placement) and restored on save; project and environment each keep their own preference.

Deploy resolution

POST /api/client/v1/environments/:id/deploy accepts an optional request body serverId. Resolution order:

Environment pinned?Body serverIdResult
YesomittedDeploy to the pinned server
Yesmatches pinDeploy to the pinned server
Yesdiffers from pin400 server_placement_mismatch
NoprovidedDeploy to the requested server
Noomitted400 (a target is required)

Minimal example

Place this on the environment compose overlay (not the project base):

x-turbopanel:
  placement:
    server_id: 01989d42-9adb-7e65-bc2e-f38792c53691
services:
  ...

Future: per-service placement

Per-service placement (different services on different servers, Docker Swarm–style deploy compose) is a planned extension of this same x-turbopanel mechanism — the multi-server compose-placement seam noted in the instance/daemon architecture. It is not implemented yet.

Daemon hub

Daemons connect to /ws/daemon/v1 and send a hello with hostname, optional persisted serverId, and machineId. The instance resolves a canonical server row, dedupes reconnects, and routes commands over the hub (src/daemon-hub.ts).

Co-located daemons on the same host dial the Unix socket directly (no Caddy hop) and collapse to a single local slot.

Adding new routes

  1. Pick the surface (client, developer, install, daemon).
  2. Add handlers in the matching *-routes.ts file (or auth/http.ts for client auth).
  3. Update src/openapi.ts and ui/src/lib/instance-api.ts when the UI needs the endpoint.
  4. If the route is Deno-only (filesystem, systemctl), register it from deno.ts — never from workers.ts.
Edit on GitHub

Last updated on

On this page