Server
One Linux box. Caddy is the only public listener: it terminates
TLS for every queue domain and reverse-proxies to a small, dependency-free Node API bound to
127.0.0.1:8790. The API stores definitions, keeps every task as a file, records each task's
lifecycle, and samples the machine's health. Every path below is fixed in the code; the only per-install
input is the master token.
/srv/
taskhoster/ queue + user definitions, mirrored up from your machine
taskhoster-items/ every task, one file per state directory
taskhoster-logs/ Caddy's access log, and the durable task-event history
taskhoster-server-stats/ server-health day-files (CPU, memory, disc, network)
/etc/
taskhoster/ the master token and the self-signed TLS cert
caddy/Caddyfile the Caddy config the installer writes
systemd/system/taskhoster-api.service the API service unit
/opt/
taskhoster/ the installed server code, one dir per version + a "current" symlink
Contents
| Name | Type | Description |
|---|---|---|
/srv/taskhoster/ | directory | Queue and user definitions, one file each, as the local package pushes them. |
/srv/taskhoster-items/ | directory | Every task as a file, under a pending / processing / done state directory. Detailed on the Store page. |
/srv/taskhoster-logs/ | directory | Caddy's access log, the durable task-event log, and any archived payloads. |
/srv/taskhoster-server-stats/ | directory | Server-health samples, one dated file per metric. |
/etc/taskhoster/ | directory | The master token and the self-signed bare-IP certificate. |
/opt/taskhoster/ | directory | The installed server code, versioned, with a current symlink. |
Caddy out front
Caddy is the whole public surface. It terminates TLS for every queue domain with on-demand
certificates, gated by a local call to /internal/allow so it only issues for a domain that
actually has queue definitions. Requests are routed to the API by Host header, and the
WebSocket upgrades pass through transparently. Bare-IP requests (the
management and tools surface) are reverse-proxied too, with /internal/* blocked from the
public side. A no-SNI or bare-IP handshake is answered with a self-signed certificate for
th.invalid, so a probe never gets a name-bearing cert.
The API
A single dependency-free Node process, taskhoster-api, bound to
127.0.0.1:8790. It carries three surfaces: the public producer and
worker endpoints (routed by Host, gated by each queue's access rules), and
the token-gated management + tools API under /api/v2 (reached by bare IP).
A periodic sweep runs inside the same process, so there is no separate worker service. With a valid
master token the HTTP status line carries the outcome; without one, every management route returns an
identical 404 wall.
| Status | Meaning |
|---|---|
200 | ok (data, or a result body) |
400 | bad request (an invalid path or definition) |
426 | a request on a different API version's path — upgrade the local package |
500 | a write failed on the server |
404 | the wall: no/!bad token, or an unknown route (indistinguishable) |
/srv/taskhoster/
The definitions the local package mirrors up, one folder per queue host, each with the two fixed definition folders. Nothing else is written here; the items live elsewhere.
/srv/taskhoster/
task.example.com/ a queue host (a domain)
taskhoster-queues/
resize.json the "resize" queue definition
thumbnail.json
taskhoster-users/
alice.json a user: { "taskhoster-name", "taskhoster-token" }
/srv/taskhoster-items/
Every task, one JSON file, under a directory that is its status. The claim, the sweep and the full item object are on the Store page.
/srv/taskhoster-items/
task.example.com/
resize/
pending/ 1f3c…9a.json waiting to be claimed
processing/ 7b21…04.json leased to a worker
done/ c8d0…e2.json finished or failed (terminal)
/srv/taskhoster-logs/
Two independent logs live here, both read-only through the token-gated
tools API: Caddy's access log (the
Web Logs source), and the durable task-event log the API appends to on
every lifecycle transition (the Task Logs source), out of band of a
queue's short retention. When a queue sets taskhoster-archivePayloads, the payload bodies
are kept alongside, separate from the queue's own item storage.
/srv/taskhoster-logs/
access.log Caddy's JSON access log (rolled by size)
tasks/
tasks-2026-07-26.log one JSON line per task event; a file per UTC day
tasks-payloads/
task.example.com/resize/1f3c…9a/
item.json result.json archived bodies (only when archivePayloads is on)
/srv/taskhoster-server-stats/
The API samples the machine's health every 15 seconds and appends one JSON line per metric to a dated day-file. It is the Server Logs source, and the same recorder every hoster uses. Older files are pruned; the local browser mirrors them and keeps its own history.
/srv/taskhoster-server-stats/
cpu-2026-07-26.log per-core busy %
memory-2026-07-26.log total / used / available bytes
disk-2026-07-26.log diskio-2026-07-26.log netio-2026-07-26.log
/etc/taskhoster/
The one secret and the fallback cert. The token is the whole authorisation story for the management and tools API; the cert answers bare-IP and no-SNI TLS handshakes.
/etc/taskhoster/
token the master token (64 hex chars, mode 600)
tls.crt tls.key the self-signed th.invalid certificate
sudo cat /etc/taskhoster/token./opt/taskhoster/
The installed code, one directory per version with a current symlink the service runs. An
upgrade lays a new version beside the old and flips the link, so a rollback is a symlink change. The
bundle is just the server halves of src/.
/opt/taskhoster/
0.1.0/ the server code for this version
server/ api.js, store.js, ws.js, Caddyfile
shared/ schema.js, paths.js, version.js, the recorders
common/ server/logs-stats.js (the shared logs + stats routes)
current → 0.1.0
current is what taskhoster-api runs.Security
There is one secret: the master token in /etc/taskhoster/token, read once at startup. The
management and tools API compares it in constant time, and anything without it — a bad token, or an
unknown route — gets an identical 404 Not Found, so an unauthenticated probe cannot tell
the API is there at all. The service runs as an unprivileged taskhoster user with
NoNewPrivileges. Producer and worker access is separate: each queue's
access rules decide who may post, read and work, by user token or public.