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):
- CI license gate —
license-checker --onlyAllow(Node) +go-licenses(agent) fail the build on any banned license. Added to CI; verified passing. - Vendor + pin critical OSS (Railpack, BuildKit frontends) and mirror to our own registry so an upstream relicense/disappearance can't pull the rug.
- Swappable builder interface — Railpack sits behind our own
Builderabstraction, so no single dependency owns us (we could swap it or fork it). - 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 +
.hsifor 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
.223—railpack 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/activatecontrol-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/hostsshon.223. Self-issued dogfood license in/etc/hostssh/agent.toml(state=active→ 14-day grace path; the cryptographic/v1/license/activatehandshake is a Phase-2 gap). - Build:
railpack 0.30.0(x86_64-musl) + a--privileged moby/buildkitcontainer.hostssh deployshelled out torailpack build --name hostssh/hsdemo:latest <src>→ BuildKit →docker load→hostssh/hsdemo:latest(508 MB) in ~41 s. - Run: agent
docker runon a dedicatedhostsshnetwork → containerUp, served the proof body over the network (http://hsdemo:3000). - Route: our own
hostssh-proxy(Traefik v3.6) on thehostsshnetwork, published on alternate host ports8081→80/8444→443(to avoid Coolify's 80/443).hostssh deploy --domain demo.hostssh.com --port 3000emitted 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 returned404.
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
hsdemoproof 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, andinstall.shfetches 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,
.hsiformat, MCP surface, god mode, templates/marketplace, the novice/advanced UX.