Operating a Climier remote server
Run an authenticated Climier remote server as the source of truth for a project.
This runbook documents one setup path for a Remote v1 server. The server is the
source of truth for a linked project; a checkout stores only its project ID and
server origin. The path uses the single packaged climier command and does not
depend on deployment scripts outside the package.
One setup path: generate, check, start, connect
Run server init on the host that will own the private configuration. It writes
server.json, server.env, and, by default, a systemd unit. The configuration
contains no password; server.env contains a freshly generated 32-byte
base64url secret. All three files are written with mode 0600 and the data and
state directories are created with mode 0700 unless path creation is
explicitly deferred.
server init is idempotent over matching artifacts: an existing server.json,
env file, and unit that already match the requested options are left untouched,
and a plain re-run never rotates an existing password. A new secret is generated
only when the env file is absent or when --rotate-password is passed; an
existing, structurally valid env file is adopted verbatim, including a value the
operator chose by hand. --force recreates only the artifacts that conflict
with the requested options and still adopts the existing password; it can only
mint a new secret when the env file is structurally invalid and therefore has no
value to adopt. Intended rotation is always --rotate-password.
Start with a dry run when choosing paths. It reports the files and actions but writes neither artifacts nor directories:
climier server init --root /srv/climier \
--host localhost --port 43127 \
--data-root /srv/climier/data \
--state-home /srv/climier/state \
--ui-root /srv/climier/ui/dist \
--dry-runGenerate the artifacts with the same options, omitting --dry-run:
climier server init --root /srv/climier \
--host localhost --port 43127 \
--data-root /srv/climier/data \
--state-home /srv/climier/state \
--ui-root /srv/climier/ui/distThe default --unit systemd creates
/srv/climier/climier-server.service. server init never installs, enables,
or starts it. The generated unit resolves the installed climier executable
without using an npm shim; standalone installs use the current binary and npm
installs use their Bun runtime plus the packaged CLI entrypoint. Use
--service-user <user> only when
the service identity is an explicit operator choice. init reports the
required ownership commands but does not run chown, chmod, sudo, or other
privileged operations.
The generated unit hardens the service with ProtectSystem=strict,
ProtectHome=true, and ReadWritePaths limited to dataRoot and stateHome.
Choose the root and the climier installation outside /home and /root; a
root hidden by ProtectHome leaves the service unable to read its own
configuration, env file, or executable. A unit created before the single-binary
runtime still points at the retired launcher and must be regenerated so its
ExecStart uses climier server run.
Use --unit none when a container or a foreground process supplies its own
process supervisor. This still generates server.json and server.env, but no
unit file. Use --allow-missing-paths when generating the artifacts before
dataRoot or stateHome is mounted or created by the runtime; it prevents
init from creating those directories. The later preflight must still be able
to resolve writable paths before the server starts.
Preflight before binding
Run preflight after generating the files. It uses the same checks and does
not start the server, acquire the service lock, or create the runtime
directories. From the setup root, climier server doctor uses server.json and
server.env by default. Otherwise pass --config and --env-file (and put the
global project flag before the command):
climier --project /srv/climier server doctor
# Optional: add --probe-bind; add --strict to make bind/UI warnings fail.The env file is parsed as strict KEY=VALUE data; it is not executed as a
shell script. The checks cover configuration shape and permissions, storage
paths, the secret, the optional UI root, and the bounded body-size setting.
--probe-bind is opt-in because it briefly opens the configured address;
without --strict, port and missing-UI findings are warnings. Neither check
proves that systemd is delivering the EnvironmentFile, and doctor does not
detect drift between the generated unit and a unit already installed in
systemd. Inspect the installed unit separately when changing it.
Start the service or run in the foreground
For the generated systemd unit, apply any pending ownership commands from
server init, then install the unit file using the path reported by files.unit
and start it:
sudo install -m 600 /srv/climier/climier-server.service \
/etc/systemd/system/climier-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now climier-server.serviceFor --unit none, a foreground launch must provide the generated secret to the
process environment without putting it in argv or logs. The generated env file
contains only simple assignments and is private:
set -a
. /srv/climier/server.env
set +a
climier server run /srv/climier/server.jsonserver run binds, prints a single JSON line
{"ok":true,"host":"<host>","port":<port>} on stdout, and stays in the
foreground; SIGINT and SIGTERM close the listener. Treat that line, or the
unit's journal, as the startup confirmation.
Do not print, copy into command arguments, or commit the generated server secret.
--print-secret is intentionally the sole exception for controlled recovery or
handoff; it is unsafe and should not be used in routine setup. To rotate it,
stop the service, run climier server init --root /srv/climier --rotate-password,
run preflight again, and start the service. Rotation
rewrites only the secret, invalidates existing sessions, and requires every
client to log in again; it does not require --force.
Connect a client checkout
After the service is reachable, configure a checkout. link and login are
client-side operations; login reads the password interactively from a TTY.
Read the secret through a protected administrative channel rather than putting
it in shell history, environment variables, output, or logs:
climier link https://climier.example.test
climier login --server https://climier.example.test
climier initinit here provisions the server-side project. It does not upload an existing
local DAG. Use the explicit transfer commands documented below when a complete
snapshot transfer is required. server init, server doctor, and server setup
are local host operations and remain available when the checkout is linked to a
remote backend.
Serving the web UI
The server serves the built Solid UI at the origin root (non-/v1 GET/HEAD)
with SPA fallback, immutable caching for /assets/*, and no-cache for
index.html. /v1/* keeps its JSON contract and never returns index.html.
The root defaults to <package>/ui/dist; set --ui-root during server init
to use an absolute path elsewhere. If the build is missing, / answers 503
with a short hint while the API keeps working. Build the bundle with the
package's documented UI commands before starting the service:
cd ui
bun install --frozen-lockfile
bun run buildThe SPA authenticates with the same POST /v1/auth/login password; it lists
projects with GET /v1/projects and reads one project with
/v1/projects/:id/ui/snapshot, /ui/nodes/:nodeId, /ui/activity, and
/ui/events SSE stream (revision notifications; the client revalidates the
snapshot with ETag).
TLS and network boundary
HTTPS is the default and recommended transport for every remote origin. Climier does not enforce a listener address or transport topology: the operator chooses whether to bind loopback, an interface, a wildcard, or another host accepted by the operating system, and is responsible for TLS, a private overlay, firewall, and proxy policy. For TLS on the same host, a trusted reverse proxy can forward to a loopback upstream:
client -- HTTPS --> trusted TLS proxy -- loopback HTTP --> climier serverIf remote clients connect directly over HTTP, the login password and bearer are
sent without transport encryption. Successful login, link, and remote
init commands add a warnings field with kind insecure-remote-http,
severity warning, and the configured origin when HTTP is non-loopback. Use
--no-warnings before or after a command to suppress that non-blocking field.
The destination still comes from .climier.json, and bearer sessions remain
indexed by origin. For large snapshots, the private service environment can
also contain CLIMIER_SERVER_MAX_BODY_BYTES=16777216 (16 MiB); choose the
smallest cap that fits the transfer.
Link, authenticate, and provision a checkout
link creates .climier.json and a project ID if the checkout has no metadata;
otherwise it preserves the existing project_id. Then log in and run init
while linked to provision that project on the remote server:
climier --project /srv/climier/checkouts/alpha link https://climier.example.test
climier --project /srv/climier/checkouts/alpha login
climier --project /srv/climier/checkouts/alpha initlogin reads the password from an interactive TTY without echo, calls
POST /v1/auth/login with X-Climier-Protocol-Version: 1, and stores only the
bearer for that origin in the local profile (~/.climier/remote-sessions.json
by default). The profile directory
is 0700 and its file is 0600 on POSIX. The password and bearer must not be
placed in argv, environment variables, stdin, .climier.json, command output,
or logs. For a remote HTTP origin, successful login reports the transport
warning described above; pass --no-warnings when that field is not wanted.
The project-specific destination remains in .climier.json and sessions are
indexed by origin in the local profile. logout removes the local copy; it does
not revoke the server hash.
Linking an existing local checkout preserves its project ID but does not upload
or merge its local DAG. Remote init provisions the server-side project; it is
not a migration. Transfers are separate, explicit snapshot operations described
below. Changing an origin requires climier link <new-origin> --replace=true;
it keeps the project ID but does not copy data between servers.
If a checkout is cloned, preserve its .climier.json project ID and run
login on the new machine. The canonical remote metadata contains only the
backend type and complete URL. A checkout carrying a retired protocol marker
fails with REMOTE_CONFIG_OUTDATED before authentication or local state I/O;
run link <configured-origin> to clean it, using --replace=true only when
changing the URL.
Only authenticated remote init provisions an absent project directory. Reads,
normal operations, batch requests, push, and pull never create storage
implicitly. Remote init --force, snapshots, restore, and plugins are
unsupported.
Manual local / remote transfers
push and pull are EXPERIMENTAL / UNSAFE manual transfers of a complete
DAG snapshot for the same project_id. They require a linked Remote v1 backend
and a valid bearer from login. link only selects the backend: it does not
transfer the local DAG. To publish local work for the first time, initialize it
locally, then link, log in, provision the remote with init, and push:
# The checkout already has its local DAG and project ID.
climier link https://climier.example.test
climier login
climier init
climier push --as aliceinit creates a pristine remote project; it does not import local state. A
normal first push is accepted only by that pristine destination. push never
provisions an absent project. A non-pristine destination without a confirmed
transfer baseline fails instead of being overwritten.
For offline work, pull the latest snapshot before selecting local state. Remove
the backend object from .climier.json while preserving project_id; this
versioned metadata change makes normal commands use local state. When online
again, link the origin, log in, and push:
climier login
climier pull --as alice
# Remove `backend` from .climier.json; keep project_id.
# Work against the local DAG while offline.
climier link https://climier.example.test
climier login
climier push --as aliceThe non-secret transfer baseline is stored outside the checkout at
$CLIMIER_HOME/remote-transfer-baselines/ (default ~/.climier), scoped by
origin and project ID. It is not copied by git or by link. Preserve the same
CLIMIER_HOME to retain the baseline. Without a baseline, pull accepts only an
absent or pristine local destination; push accepts only a pristine initialized
remote. With a baseline, CAS compares the destination revision to the last
confirmed transfer. Conflicts do not mutate either DAG; choose an explicit side
with push --force or pull --force only after review.
Both --force forms replace the entire destination DAG, including claims,
in_progress, plugin data, and its log. The winning snapshot retains its own
log and adds a transfer.push or transfer.pull event with the replaced
revision. That event does not preserve or recover the discarded destination
log. Back up both sides before force; the feature is experimental and unsafe.
There is no merge, automatic fallback, journal, or retry. A network timeout may
be ambiguous: the remote may have committed even though the local baseline was
not advanced. Inspect the destination or pull before choosing whether to retry
or use force.
Backup, rotation, and recovery
Back up stateHome, remote-auth.json, and dataRoot together using a
consistent filesystem or service-level snapshot. Protect the backup as both a
secret and the original DAG data. Stop the server before restoring. Never use
init --force, delete a project state, or restore a ledger without its matching
state file as a recovery shortcut.
To rotate the password, stop the service and run
climier server init --root /srv/climier --rotate-password. Run the preflight,
then start the service against the same stateHome. The generated secret stays
in the private server.env; it is never printed by default. The server durably
writes the new verifier and an empty session list before listening. Existing
bearers fail closed; each client must run login again. If startup or
persistence fails, do not report a successful rotation or reopen the proxy.
If remote-auth.json is corrupt, stop the service and take a backup first.
Move only that file aside, then restart with the intended password. The server
creates fresh auth and leaves project DAGs, ledgers, and dataRoot untouched;
all clients must log in again. Never delete tasks.json, the ledger, or the
whole state directory for auth recovery.
The service lock at stateHome/.server.lock covers the entire process lifetime.
A second process using the same stateHome fails with SERVER_ALREADY_RUNNING
before listening. A normal shutdown releases it. If a crash leaves it behind,
verify that the recorded PID and service are gone, back up stateHome, remove
only that lock, and restart. Never auto-delete a lock while another process may
be active.
Verification and failure boundaries
Before accepting a deployment, rerun the preflight checks described above and
confirm that the generated paths are writable by the service identity. For a
linked client, verify login, a read-only command, and one authenticated
write against a disposable project before serving production data.
A failed request must not mutate a local sentinel or silently retry against
local state. Check the structured error.code; common boundaries are
AUTH_INVALID, REMOTE_CONFIG_OUTDATED, REMOTE_REQUEST_FAILED,
REMOTE_UNSUPPORTED_OPERATION, and SERVER_ALREADY_RUNNING.

