HostSSH Platform — architecture & decision record

Why and how we build our own PaaS (Railpack, no Nix), the 100%-ownable stack, and the phased roadmap.

HostSSH Platform — build our own (ADR)

Decision

Build our own deployment platform (a Coolify-equivalent we fully own) rather than depend on Coolify. Chosen path: C — build our own, phased, on proven OSS primitives. Internal-first, productize later. Driven by the owner + AI agents. Keep Coolify running the live fleet until the new platform reaches proven parity — prove on the .223 Playground box → one non-critical app → expand → migrate.

Why own it: one system (deploy + backup + recovery + image-limits + MCP + god mode share one model, UI, auth), AI-native at the core, full control of roadmap + brand, and our backup/clone/relocate moat lives at the orchestration layer instead of being scraped from a third-party API.

Note: MCP did not require rebuilding — we already drive Coolify's API and can wrap it as MCP. We own the platform for vertical integration + control + product ownership, not MCP alone.

Ownership — "100% ours / repackable" (hard requirement)

Everything must be permissive or weak-copyleft we can still ship proprietary + rebranded.

Allowed: MIT, ISC, Apache-2.0, BSD-2/3, 0BSD, Unlicense, CC0, BlueOak, Python-2.0, MPL-2.0 (file-level — only edits to its files need sharing), LGPL-3.0 (dynamic lib), CC-BY-4.0 (attribution, usually build-time data). Banned: GPL / AGPL (strong copyleft), SSPL / BUSL-BSL / Elastic (source-available), any -NC (non-commercial), -SA for code.

Build engine = Railpack (MIT — verified railwayapp/railpack). Chosen over Nixpacks because it does not use Nix (Mise for runtimes + apt for system packages on BuildKit) → faster, simpler, no Nix. Owner directive: "Nix is no good."

Current scan (control-plane/web): all permissive except — lightningcss MPL-2.0 (Tailwind's CSS engine; fine), @img/sharp-libvips LGPL-3.0 (Next image optimizer; optional/droppable for zero copyleft), caniuse-lite CC-BY-4.0 (build-time data), and our own private package. No GPL/AGPL/SSPL.

How ownership is guaranteed (not trusted):

  1. CI license gatelicense-checker --onlyAllow (Node) + go-licenses (agent) fail the build on any banned license. Added to CI; verified passing.
  2. Vendor + pin critical OSS (Railpack, BuildKit frontends) and mirror to our own registry so an upstream relicense/disappearance can't pull the rug.
  3. Swappable builder interface — Railpack sits behind our own Builder abstraction, so no single dependency owns us (we could swap it or fork it).
  4. SBOM + NOTICE — generated per release; bundles the (light) attribution obligations while our own code stays proprietary.

Our own code (control plane, agent, .hsi engine, MCP, god mode, orchestration) is ours outright.

Architecture (all permissive)

Control Plane (Next.js + god mode)  ──drives──▶  Agent (Go, on each box)
        │  REST / MCP                                   │ orchestrates:
        ▼                                               ├─ Build:  Railpack  → OCI image (Mise+apt+BuildKit, no Nix)
  Postgres (state)                                      ├─ Run:    Docker / Podman (OCI containers, workers)
                                                        ├─ Route:  Traefik / Caddy (auto Let's Encrypt TLS)
                                                        └─ Protect: restic / .hsi (backup · recovery · image-limits)
  • Build — Railpack turns a repo into a standard OCI image; zero-config language detection.
  • Run — Docker/Podman runs images as apps + on-demand workers.
  • Route + TLS — Traefik/Caddy with automatic certs; no exposed ports beyond the proxy.
  • Protect — the existing restic engine + .hsi for backup/recovery; image-limits/retention configurable from the Control Panel.
  • Control plane + god mode — already built (24 routes). Agent — Go, Phase-1/2 done (CLI, license activation + ed25519 verify, telemetry heartbeat, engine wrappers).
  • Data model: Project → App → Service/Worker → Deploy → Image.

Product surface (what the Control Panel exposes)

  • Hosted / managed solutions — we run it for you (host-first), plus licensable for any VPS.
  • One-click installs — a template/marketplace catalog; deploy a stack in a click.
  • MCP worker spin-up — MCP tools so AI agents provision on-demand workers / instances / VPS.
  • Novice + advanced modes — progressive disclosure: sane defaults for novices, full config (build, env, scaling, networking, retention) for advanced users.
  • Everything configurable in the Control Panel, robust on the backend, all under super-admin god mode.

Phases

  • Phase 0 (done): decision, ownership policy + CI license gate, builder abstraction, this ADR.
  • Phase 1 (✅ proven on metal — 2026-06-22): the build→run→route pipeline on .223railpack build → OCI → docker run → Traefik route, driven by the real compiled agent. One sample app deployed end-to-end (see "Phase 1 — proven on metal" below). Real public ACME TLS is the one remaining mile, gated on owning host 80/443 (already proven on .207).
  • Phase 2: databases, env/secrets, log streaming, one-click templates, novice/advanced UI, MCP deploy + worker tools. Plus: build the /v1/license/activate control-plane endpoint (the agent side exists; the server route does not yet) and wire the live dashboard's simulated deploys to the real agent.
  • Phase 3: backup/recovery/image-limits integration + restore-drills, multi-server, hosted provisioning + billing.
  • Phase 4: migrate the 14 fleet apps off Coolify at proven parity; decommission the dependency.

Phase 1 — proven on metal (.223, 2026-06-22)

We ran our own platform end-to-end on the Playground box with the real compiled agent — no Coolify in the path. The box was not idle (it runs the staging Coolify, whose Traefik owns 80/443), so the proof was additive and reversible: Coolify and its 80/443 were left untouched throughout.

What ran:

  • Agent: cross-compiled CGO_ENABLED=0 GOOS=linux GOARCH=amd64 (static ELF, 7.1 MB), shipped to /usr/local/bin/hostssh on .223. Self-issued dogfood license in /etc/hostssh/agent.toml (state=active → 14-day grace path; the cryptographic /v1/license/activate handshake is a Phase-2 gap).
  • Build: railpack 0.30.0 (x86_64-musl) + a --privileged moby/buildkit container. hostssh deploy shelled out to railpack build --name hostssh/hsdemo:latest <src> → BuildKit → docker loadhostssh/hsdemo:latest (508 MB) in ~41 s.
  • Run: agent docker run on a dedicated hostssh network → container Up, served the proof body over the network (http://hsdemo:3000).
  • Route: our own hostssh-proxy (Traefik v3.6) on the hostssh network, published on alternate host ports 8081→80 / 8444→443 (to avoid Coolify's 80/443). hostssh deploy --domain demo.hostssh.com --port 3000 emitted the Traefik labels (Host(...), entrypoints=websecure, tls.certresolver=le, server.port=3000); curl -k --resolve demo.hostssh.com:8444:127.0.0.1 https://demo.hostssh.com:8444/ returned the app. An unmatched Host correctly returned 404.

The one unproven mile: real public Let's Encrypt issuance — the le resolver can't complete ACME on alt ports, so the route served Traefik's default self-signed cert (curl -k). This is purely a function of owning public 80/443 + DNS, already proven in production on .207 (corereflex.com, usermails.com issue valid LE certs via these identical labels).

To make .223 the live branded instance (Phase 1.5 cutover — needs explicit go + a Hostinger snapshot): stop Coolify's proxy on .223, rebind hostssh-proxy to 80/443, point a demo domain's DNS at .223, and the same le resolver issues a real cert. Then apply HostSSH branding and wire the live dashboard to this agent.

HostPack — our own branded build engine

Railpack is MIT (verified railwayapp/railpack — corrects an earlier "Apache-2.0"). It already sits behind our swappable Builder interface, so we brand the build stage HostPack and evolve it in phases: (1) brand + wrap — Railpack engine, HostSSH UX/labels — now; (2) vendor + fork to Girard-Media/hostpack (rename binary, NOTICE file) when we want full roadmap control; (3) original language providers only if warranted. HostPack also owns the base-image policy: slim/distroless defaults

  • a multi-stage build→runtime split to shrink deployed images (today's hsdemo proof was 508 MB on Railpack's default base → target ~120–150 MB), tied to the existing keep-all/retention policy and the fleet disk-bloat lesson. Full design lands with the HostPack design pass.

Implementation status — spec vs. proven (keep docs honest)

  • Proven on metal: build→run→route — Phase 1 on .223, 2026-06-22 (see above).
  • Built, not yet wired end-to-end: the Go agent (deploy/db/logs/drill/retention/license-gate/heartbeat, stdlib-only) and the Next.js god console exist, but the control-plane /v1/* endpoints the agent already calls do not exist yet (/v1/license/activate, /v1/telemetry/heartbeat, /v1/fleet/*, /v1/jobs/{id}/stream), and dashboard deploys are still simulated (lib/platform/store.ts).
  • Spec, NOT yet implemented (security spine — verified fail-open by the design red-team): the agent currently uses a plain http.Client (no mTLS/pinning), Activate() stores the returned token without verifying it, and install.sh fetches an unsigned binary. agent-protocol describes the target (mTLS-pinned, ed25519-verified, signed releases) — treat it as the build order, not current reality.

Mandatory security build-order — gate every later feature on these: (1) sign the binary + verify it in the installer against an embedded key; (2) enforce mTLS + control-plane cert pinning, reject non-TLS; (3) make Activate() verify the token before storing; (4) grace-clock refresh requires a signed, fingerprint-bound response (a bare HTTP 200 must never refresh grace → else revocation is defeated); (5) append-only, off-box, hash-chained audit; (6) a license-allowlist CI gate. Browser-root-SSH ships LAST, behind all of the above + a third-party pen-test. See hardening and web-ssh.

Reuse vs build

  • Reuse (own via permissive license): Railpack, BuildKit, Mise, Docker/Podman, Traefik/Caddy, restic.
  • Build (our IP): the orchestration/control plane, the agent, .hsi format, MCP surface, god mode, templates/marketplace, the novice/advanced UX.