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, *.wildcard or * hostname and answers 421 for anything else;
  • tunnels WebSocket upgrades;
  • replaces client-supplied X-Forwarded-* and CF-Connecting-IP headers 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 in sobjects.toml.
  • Use a bucket dedicated to Serverless Objects.