The control panel for everything you self-host at home.
The harness for your servers.
Point it at the things you run — a gateway, a media server, a bot, a database — and it starts them, watches them, restarts what dies, and shows you one page of what is going on. Or BYOU, for a specialized UI that fits you exactly.
🚀 Quick start · 🤖 Agents & API · ✨ Features · 🧩 Servers · 🛠 CLI · 🔔 Notifications · 🎨 BYOU
The stock panel, the NOC-console that ships alongside it (for TUI and shortcuts wizards), and UI directions you could build yourself — BYOU.
You run a handful of services at home. The usual choices are extremes — 🧟 tmux sessions you
forget about, 📜 a hand-written systemd unit per service (times six), or 🐳 a whole docker/k8s
stack??? - too extreme! — plus 😩 monitoring, rebooting and changing the host machine, yuck!
🙂✨ home-hosted enhances on top: a panel/supervisor that starts them, watches them, restarts what
dies, and puts the whole stack on one page, with deep backup support — whether a server is a plain
command or a docker compose stack.
┌──────────────────────────────────────┐
your browser ──▶│ home-hosted · 127.0.0.1:3999 │
│ your UI + JSON API + SSE logs │
└───────────────┬──────────────────────┘
│ supervises
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ gateway │ │ files │ │ bot │
│ :4000 │ │ :4010 │ │ ... │
└────┬────┘ └─────────┘ └─────────┘
│ compose up -d
▼
┌────────────┬────────────┬────────────┐
│ gateway │ postgres │ redis │ restarts ↻
└────────────┴────────────┴────────────┘
health ✓ (the published port is the probe)
| ❌ "Is it still running?" | Health, CPU/mem, uptime and live logs per server — no ps, no curl, no hope |
| ❌ Silent deaths | Restarted automatically, and the panel or Telegram notifies |
| ❌ Fragile reboots | autostart brings the stack back; one down stops it all cleanly |
| ❌ "Move it to the new box" | One archive: config and data, restored on a blank host |
| ✅ home-hosted | Declare it once, watch it forever, one command to stop it all |
It ships with nothing: no blessed paths, no opinion about what you run — a server is a command, some arguments, and the environment you give it:
{ "id": "gateway", "command": "node", "args": ["server.js"], "port": 4000, "autostart": true }Manage them from the panel, home-hosted start|stop <id> from a shell, home-hosted status --json,
or GET /api/state; GET /healthz is the same status line for your own monitor, no session needed.
npx home-hosted # start it — detached, it stays running
npx home-hosted status # where is it, is it healthy
# pnpm instead of npx: `pnpm dlx home-hosted …`That is it. The panel is on http://127.0.0.1:3999 and keeps running after the terminal closes. Needs Node 24 or newer. Stop it whenever you like:
npx home-hosted down # stops the panel *and* everything it startedNote
The first boot writes a default password (hh) so the panel is never unprotected. Change
it under Settings → Authentication — binding beyond 127.0.0.1 stays refused until you do.
📦 Install it instead of npx-ing it
npm install -g home-hosted # or: pnpm add -g home-hosted
home-hosted up # `hh up` does the sameEverything it owns — config, secrets, logs, TLS, backups — lives in $HHOSTED_HOME, default
~/.home-hosted. Delete that and nothing of yours is left behind.
An installed home-hosted answers to hh too — hh up, hh status, hh down. Only the
installed form gets it; npx stays npx home-hosted ….
📁 Keep a whole setup in a project you can take anywhere
Commit the project and it is the setup. init scaffolds it:
npx home-hosted init # or: pnpm dlx home-hosted init
# project directory, package name, package manager, install, git — every step has a default
# `--yes` takes them all, for an agent or a CI jobIt writes a manifest whose scripts all pass --home ./state:
node_modules/
state/* # secrets, logs, TLS keys and archives stay local…
!state/servers.config.json # …but the server definitions are committed
data/ # per-server data directories, declared through dataEnvsOne clone, pnpm install --frozen-lockfile, pnpm run up — the setup is up on any machine with Node.
Worked example, with per-server data inside the project:
hhosted-ai-pack.
Call them as pnpm run up — pnpm up is pnpm's own update, not your script.
[!TIP] Give a project its own panel port (
control.port, e.g.4399) — the default3999is what the global instance and every other project also want.
🧰 Run it under systemd or Docker (no daemon needed)
home-hosted up --foreground # stays in the foreground, logs to stderr[Unit]
Description=home-hosted
[Service]
ExecStart=/usr/local/bin/home-hosted up --foreground
Environment=HHOSTED_HOME=/srv/home-hosted
Restart=always
[Install]
WantedBy=multi-user.targetEverything the panel does, a script or an agent can do: the API takes a long-lived token in place of the browser cookie. One command for a credential, then plain HTTP to start, stop, inspect, restart and read logs.
home-hosted set-token --generate
# hh_9uA2… (printed once; only its hash is kept, mode 0600)
curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/omniroute/restart
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=omniroute' # SSE
home-hosted status --json # machine-readable: pid, url, health, pathsA token has the same authority as a signed-in browser and outlives restarts; set-token --clear
revokes it instantly.
🌐 The endpoints worth knowing
GET /api/state |
the full snapshot: config, live status, host vitals |
GET /api/events |
SSE: the live state, plus logs (?logs=0, ?serverId=…) |
GET /api/servers/:id/stream |
SSE: one server's state and logs |
POST /api/servers/:id/{start,stop,restart} |
lifecycle |
POST /api/servers/:id/free-port |
ask whatever holds that server's port to stop |
PATCH /api/servers/:id, PATCH /api/settings |
edit configuration |
GET /api/logs, /api/backups |
history and archives |
PUT/DELETE /api/notifications/token, POST /api/notifications/{test,detect-chats} |
the bot credential, a test send |
GET /healthz |
no session needed — the one an external monitor wants (its per-server detail needs a credential) |
GET /api/metrics |
Prometheus text (needs a token or session, like every /api route) |
GET /openapi/spec.json describes all of it, /openapi/ui is the browsable version, and every error
comes back as one envelope ({ message, code, detail }) with a stable code a tool can branch on.
🧑💻 Pointing an agent at it
Give the agent four things and it can run your home server without guessing:
- the token (
home-hosted set-token --generate), http://127.0.0.1:3999/openapi/spec.json— the API it may call,home-hosted status --json— where things are,- SERVERS.md — how an entry is declared when it needs a new server.
For a UI rather than the API, UI_CREATION.md is the whole contract, and the panel can be told what to be: "Help me build a UI for home-hosted: nostalgic game theme, including …".
| 🚦 Lifecycle | Start, stop, restart from the panel or the API; autostart entries come up with it. |
| 📝 Hand edits welcome | Change servers.config.json in an editor, a git checkout or a config tool: the panel notices within seconds, no restart. A file it cannot read is reported in the panel, and the running servers are left alone. |
| ♻️ Auto-restart | Exponential backoff on crash, with the counter reset once a process stays up. |
| 🩺 Health that acts | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, check ports before starting — and follow or replace a program that restarts itself. |
| 🔗 Ordered startup | dependsOn waits for a dependency to be healthy — not merely spawned — and stops in reverse. |
| 📜 Logs | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
| 📈 Resources | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
| 🌡️ Host vitals | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
| 🤖 Token API | Scripts and agents drive it with Authorization: Bearer — no browser, no session. ↑ |
| 🔔 Notifications | Telegram on crash, unhealthy, forced restart, recovery and host thresholds — setup here. |
| 💾 Backups | One click for config, secrets, TLS and your declared data directories — plain .zip, or AES-256 with a password, restored per path. |
| 🎨 BYOU — Bring Your Own UI | Upload a static build, ui-update to follow its releases, ui-revert to go back. UI_CREATION.md |
| 🔐 Security | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
| 🧩 No special treatment | A server is command + args + env + cwd; nothing is built in for any particular app. |
| 🖥 Cross-platform | Linux, macOS and Windows: /proc, ps or Win32_Process, process groups or taskkill /T, no shell dependencies. |
An entry is a few lines. Add one with ➕ Add server, or write it into servers.config.json:
{
"id": "myapp",
"command": "node",
"args": ["server.js"],
"port": 8080,
"autostart": true,
"dataEnvs": { "DATA_DIR": "{projectDir}/data/myapp" }
}dataEnvs declares a data directory once: it is exported to the process and picked up by Backups.
Every field, every placeholder, the port-conflict policies (including adopting a server that
restarts itself), and how hand-edits are validated: SERVERS.md.
| command | |
|---|---|
home-hosted up |
start the panel detached, and keep it alive in the background |
home-hosted down |
stop it cleanly — supervised processes included, persistent entries left running |
home-hosted restart |
down, then up |
home-hosted status |
pid, URL, health, uptime, state and log paths (--json for scripts) |
home-hosted start <id> |
start one server — and anything it dependsOn |
home-hosted stop <id> |
stop one server, nothing else |
home-hosted set-password |
set the panel password without opening a browser |
home-hosted set-token |
set the API token scripts and agents use (--generate, --clear) |
home-hosted migrate |
bring servers.config.json up to this release's schema (--dry-run, --yes) |
home-hosted init |
scaffold a project that keeps state/ and its data in the repo |
home-hosted ui-switch |
install a UI from a release asset, a zip file or a URL (interactive) |
home-hosted ui-update |
bring an installed UI up to date, or pick a release (--old, --check) |
home-hosted ui-revert |
go back to the stock panel UI after uploading your own |
⚙️ Flags
home-hosted <command> --help prints what that command takes.
up, restart -c/--config -p/--port --host --open --no-autostart --foreground --print-config
down (no flags)
status --json
start, stop <id> (the server's id in servers.config.json; both need the panel up)
init --dir --name --pm --no-install -y/--yes
set-password --clear
set-token --generate --clear
migrate --config --dry-run -y/--yes
ui-switch --repo --tag --asset --file --list --token -y/--yes
ui-update --check --tag --asset --old --repo --token -y/--yes
ui-revert (no flags)
every command --home <dir> --project <dir> (or $HHOSTED_HOME, $HHOSTED_PROJECT)
env vars HHOSTED_PASSWORD, HHOSTED_MIGRATE=allow, HHOSTED_TOKEN, GITHUB_TOKEN or GH_TOKEN
up and restart share the same flags: restart is down, then up with exactly what it was given.
🧭 Upgrading, and why the panel sometimes refuses to start
servers.config.json records what wrote it: meta.writtenBy (the release) and meta.schema (the
config shape). That buys two guarantees:
- A newer home-hosted always reads an older config — every existing key keeps its meaning.
- Keys a newer release added are ignored, not fatal. The panel names them in its log, leaves them in the file, and never resets the settings around them.
What it will not do is run a config it cannot read. A wrong value, a duplicate id, an unreadable file
or a config whose schema is newer than the running release stops up with the exact problem, rather
than starting with defaults that quietly differ from your file. Fix the file, or install the release
that wrote it.
When a release changes the shape itself, home-hosted migrate applies the steps it ships:
home-hosted migrate --dry-run # print the steps, write nothing
home-hosted migrate # ask, then write — keeps servers.config.json.bakUnattended, consent comes from --yes or HHOSTED_MIGRATE=allow; without it the command stops.
Everything binds 127.0.0.1 until you say otherwise.
- A password is required to expose the panel. Replace the default, then bind to
lan— in the UI, in the config, or with--host lan. The same guard applies in all three places. - Sessions live in memory only; the cookie is
HttpOnlyandSameSite=Strict, and the login route locks out repeated failures per IP. - API tokens for scripts and agents:
home-hosted set-token --generateprints one once, and a request proves itself withAuthorization: Bearer …— the same access as a signed-in browser, stored as a SHA-256 hash, revoked withset-token --clear. - Port conflicts are named —
port 4010 is already in use (pid 4242)— and can be resolved from a confirmation popover on that banner or card. The process is looked up again at that moment, never taken from the message, and anything the panel supervises is refused, not killed. A server that restarts itself can be followed, or replaced with a supervised copy. - Secrets never enter the config: the password hash, the API token hash and the Telegram bot
token live in
$HHOSTED_HOME/.control-secrets.jsonwith mode0600; the TLS pair in.tls/. - Behind a proxy turn on
trustProxyand letcookieSecure: autoaddSecureon https, or upload a PEM pair and let home-hosted terminate TLS itself.
Telegram, when something happens while you are not looking: a server that gave up restarting, a failing health check, a forced restart, a recovery, or a host threshold (disk, memory, swap, load, temperature). Opt-in, rate-limited per server and reason, and the bot token stays in the secrets file. Two minutes of setup: NOTIFICATIONS.md.
Settings → Backups archives the config, secrets, TLS pair and every data directory your entries
declare — an ordinary .zip, or WinZip AES-256 with a password, restored per path. Known build output
and dependency directories (node_modules, dist, .next, framework caches) are skipped per entry;
backupIgnoreGenerated: false captures them anyway.
🚚 One archive is a whole setup
Start a blank home-hosted anywhere — another machine, another user, a fresh container — upload the
archive and restore. Definitions come back, data lands where this machine's config says, and
autostart entries come up immediately.
It works because an archive carries its own servers.config.json and paths are matched by the
declaration (omniroute:DATA_DIR), not by an absolute path from the source machine. A restore never
writes where no config declares.
The panel is a static site: $HHOSTED_HOME/.ui overrides the packaged one, and Settings →
Interface takes a zip. No restart, no fork — and home-hosted ui-revert brings back the stock
panel if yours breaks.
Two ship in this repo: uis/stock, and uis/noc-console for TUI and shortcuts wizards; a release
attaches both as home-hosted-ui-<name>.zip. Yours can be anything that compiles to static files —
the server never cares what built it.
Install a UI from the CLI: home-hosted ui-switch — with no flags it fetches the official asset
built for this release. An official UI keeps itself paired with the panel: upgrade the panel and
the next up re-installs the matching asset. Someone else's UI declares its own repo/asset in
ui.json, and home-hosted ui-update offers its newer releases to pick from — --old for older ones.
🤖 Or have an agent build the UI you actually want
The whole contract fits in one file, so a coding agent can do this. Point it at this repo and be specific:
Help me build a UI for
home-hosted: nostalgic game theme, including … features.
UI_CREATION.md has the endpoints, the SSE frames, the auth rules and a checklist.
Is it a systemd replacement?
No — it is a friendlier layer for the handful of things you run yourself. Keep systemd for the
panel itself (--foreground) and for system services; use home-hosted for the rest.
What happens to my servers when the panel stops?
home-hosted down stops them — that is the point of the command. SIGTERM/SIGINT are handled
the same way: every supervised process tree is stopped before the panel exits.
An entry marked persistent is the exception, and down names it instead of stopping it: it runs
under its own nanny process, keeps logging, and is reattached by the next panel
(SERVERS.md).
Nothing starts and the port is busy
A supervised server whose port is taken is reported rather than started over — the panel names the holder and offers to free it, and a program that restarts itself can be followed or reclaimed instead (SERVERS.md). The control port itself is checked before the listener is opened.
Where is my state?
$HHOSTED_HOME, default ~/.home-hosted:
servers.config.json your servers, plus meta: which release and schema wrote it
servers.config.schema.json regenerated on every start, for editor autocomplete
.control-secrets.json password hash + API token hash + Telegram token (mode 0600)
.logs/ rotated per-server logs + history
.tls/ an uploaded PEM pair
.backups/ zip archives
run.json the running panel (pid, url, token, mode 0600)
home-hosted status prints the paths.
Which ports does it use?
Just the control panel, 3999 by default. Supervised servers use the ports you give them.
Windows support, really?
Yes. Process trees are sampled from Win32_Process, termination uses taskkill /T, and the shipped
examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the OS does not
expose them to an unprivileged process, and adopting a self-restarted process is Linux/macOS only.
src/ control plane: config, supervisor, API, providers, services
src/cli.ts the command line; one file per command under src/cli/
src/index.ts the control plane itself, used by `up --foreground`
uis/ UIs: `stock` (shipped in the package) and alternatives — any framework, static output
bin/ the published entry point
docs/ topic docs, UI examples and the README's media
scripts/ builds, typechecks, media capture, release helpers
test/ the vitest suite
pnpm dev runs the panel with tsx watch plus the stock UI's dev server (state goes to
.dev-state/); pnpm build produces dist/ and uis/stock/dist/; pnpm quickcheck is lint plus
types; pnpm test is vitest; pnpm run media regenerates the GIF above.
📚 Which doc do I need?
| if you want to… | read |
|---|---|
| declare a server: every field, placeholders, port conflicts | SERVERS.md |
| get Telegram alerts working end to end | NOTIFICATIONS.md |
| build a UI against the API | UI_CREATION.md |
| change the internals: architecture and the rules | AGENTS.md |
| poke the live API on your own panel | /openapi/ui |
🔗 Interesting resources
- dsh-home-hosted — home-hosted servers management with boot autostart from DeepSeek Harness
+ PR to add yours
MIT
