Docs / Serverless Objects / Running as a service
Run Serverless Objects as a service
Run a node with sobjects up, put TLS ingress in front with static certificates or ACME, open the dashboard and install a systemd or launchd unit.
A node is sobjects up reading a sobjects.toml. With [ingress] or [admin] configured, the supervisor (TLS ingress and loopback admin API) starts first, then the engine.
Start and stop a node
sobjects up # foreground
sobjects up --detach # background
sobjects status
sobjects logs # add --supervisor for the ingress and admin log
sobjects down # waits for the engine to drain and release its lease
The config path defaults to $SOBJECTS_CONFIG, else ./sobjects.toml. Pass --config PATH to use another file.
TLS ingress
With an [ingress] table, up starts a TLS-terminating reverse proxy in front of the engine. It:
- routes by exact,
*.wildcardor*hostname and answers421for anything else; - tunnels WebSocket upgrades;
- replaces client-supplied
X-Forwarded-*andCF-Connecting-IPheaders with the real peer address; - optionally redirects HTTP to HTTPS with
308; - serves
/.well-known/sobjects/health, which checks the upstreams at most once a second; - writes JSON access logs without query strings.
Remote clients of the health endpoint get only {"status":"ok"} or "degraded" (with 503). Loopback clients also see each upstream's address and state.
Plaintext ingress is allowed only on loopback. When [celld].listen is loopback, up starts celld with --trust-forwarded-headers, so a Worker's request.url has the client's https:// scheme and host, as on Cloudflare.
Static certificates
[ingress]
listen = "0.0.0.0:443"
redirect_http = "0.0.0.0:80"
[ingress.tls]
cert = "/etc/serverless-objects/tls/fullchain.pem"
key = "/etc/serverless-objects/tls/privkey.pem"
[[ingress.routes]]
hostname = "agents.example.com"
Relative paths resolve from the config file's directory. SIGHUP reloads static certificates. A route can carry its own certificate, chosen by SNI: tls = { cert = "...", key = "..." }. Wildcard hostnames need static certificates.
ACME HTTP-01
[ingress]
listen = "0.0.0.0:443"
redirect_http = "0.0.0.0:80"
[ingress.acme]
email = "ops@example.com"
agree_to_terms = true
directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
[[ingress.routes]]
hostname = "agents.example.com"
ACME needs:
| Requirement | Why |
|---|---|
email and agree_to_terms = true |
Records your agreement to the CA's subscriber terms. |
redirect_http on port 80 |
Serves the HTTP-01 challenge. Public DNS and inbound port 80 must reach it. |
| Exact hostnames | HTTP-01 doesn't issue wildcard certificates. |
The default directory is Let's Encrypt production, so test with the staging directory_url first. Keys and certificates are stored under the state directory with mode 0600 files in 0700 directories. Valid certificates are reused at startup and renewed 30 days before expiry. A failed renewal is retried every six hours while the current certificate is still valid. Issuance has been tested against Pebble; public Let's Encrypt issuance and renewal have not been exercised yet.
Dashboard and admin API
An [admin] table makes the supervisor serve a dashboard at / and a JSON API:
[admin]
listen = "127.0.0.1:8790"
The dashboard shows node health and load, the objects in each namespace, per-object storage with a key-value browser and read-only SQL console, and eviction. The API listens on loopback only and refuses non-loopback Host headers. Mutations require the x-sobjects-request: 1 header. [admin] is available with [celld] only.
Admin token
Every local process can reach loopback, including the apps you deploy, so the API also requires an admin token. The supervisor creates it on first start as admin-token in the state directory (a 256-bit random value, mode 0600 in a 0700 directory). Only the dashboard's static files, GET /api/v1/health and GET /metrics work without it.
Run sobjects dashboard as the same user and with the same SOBJECTS_STATE_DIR as sobjects up:
sobjects dashboard
# http://127.0.0.1:8790/#token=...
The token travels in the URL fragment, which browsers don't send to the server. Treat the URL like a password. To rotate the token, stop the supervisor, delete admin-token and start it again.
From a script, send the token as a bearer token:
curl -H "Authorization: Bearer $(cat "${SOBJECTS_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/serverless-objects}/admin-token")" \
http://127.0.0.1:8790/api/v1/node
Endpoints
| Endpoint | Returns |
|---|---|
GET /api/v1/health |
celld listener and operator API health |
GET /api/v1/node |
Resident and owned objects, memory, CPU, WebSockets, deployment version, drain state |
GET /api/v1/objects?class=&limit=&after= |
Objects, marked resident or not |
GET /api/v1/objects/<Class:id> |
SQL tables with row counts, pending alarm, WebSocket count and tags |
GET /api/v1/objects/<Class:id>/kv?prefix=&startAfter=&limit= |
Key-value entries (Date, Map, Set, bytes and bigint as tagged JSON) |
POST /api/v1/objects/<Class:id>/sql |
Read-only SELECT, WITH, EXPLAIN or PRAGMA, up to 500 rows |
GET /api/v1/objects/<Class:id>/export |
JSON download of every KV entry, app table and pending alarm (10,000 each) |
POST /api/v1/objects/<Class:id>/evict |
Evicts a resident object |
GET /api/v1/backups?class=&limit=&after= |
Superseded LTX epochs eligible for GC (not a full history) |
GET /metrics |
Prometheus text: engine gauges and ingress counters |
celld 0.6 has no storage API, so sobjects deploy wraps the app's Durable Object classes in a small inspection hook. Requests carrying x-sobjects-inspect with this host's token are answered by the hook, and everything else reaches the app unchanged. Only the token's SHA-256 goes into the bundle, and the ingress strips the header from client requests. Pass --no-inspector to deploy without it.
Service units
sobjects service print writes a systemd unit (Linux) or launchd plist (macOS) to stdout. --kind overrides the choice. sobjects service install writes it to disk, refuses to overwrite without --force, and prints the enable commands. It doesn't start anything.
| Kind | Path | Enable with |
|---|---|---|
| systemd, user | ~/.config/systemd/user/serverless-objects.service |
systemctl --user daemon-reload, systemctl --user enable --now serverless-objects.service, and loginctl enable-linger "$USER" to keep it running after logout |
systemd, --system --user NAME |
/etc/systemd/system/serverless-objects.service |
sudo systemctl daemon-reload, sudo systemctl enable --now serverless-objects.service |
| launchd | ~/Library/LaunchAgents/dev.serverless-objects.plist |
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.serverless-objects.plist |
sobjects service print --config /etc/serverless-objects/sobjects.toml
sobjects service install --config /etc/serverless-objects/sobjects.toml --system --user sobjects
The unit runs sobjects up in the foreground and restarts it on failure. systemd units read credentials from an optional sobjects.env beside the config. System units get CAP_NET_BIND_SERVICE so the ingress can bind ports 80 and 443. Review the generated unit before you enable it.
Native engine nodes
A [native] table in place of [celld] makes up run the native engine. --detach, status, logs, down and service install work the same way, and [ingress] routes to [native].listen by default. [admin] and the dashboard aren't available with [native] yet, and sobjects deploy is for celld: restart the node to run new code. See native engine and benchmarks.
Security checklist
- Keep celld's listener, its operator listener and the admin API on loopback. Only the ingress should face the network, and it requires TLS to do so.
- The admin token guards the admin API against other local processes. It is not a reason to expose the API.
- Keep object-store credentials in the environment, the credential chain or
sobjects.env, never insobjects.toml. - Use a bucket dedicated to Serverless Objects.