Skip to content

CLI

The public zelavis command starts and operates the long-running Platform OS. Its endpoint-backed command implementation is zelavis/cli, importable on its own for tooling that wants the commands without the binary.

Install the Platform on this machine to get the complete command, including serve:

Terminal window
npm create zelavis@latest -- --yes
zelavis --help

All installed release formats use a private Node. In user mode, start the Platform yourself:

Terminal window
zelavis serve

zelavis serve --enrollment-port 8444 [--enrollment-address <ip-or-host>]... also serves a narrow HTTPS listener (self-signed, pinned) through which worker machines enroll; see Node Enrollment. zelavis nodes platform prints its URL and the fingerprint to pin.

Plugin-owned operations use the plural namespace consistently:

Terminal window
zelavis plugins --help
zelavis plugins example health get --url http://localhost:3000/zelavis --json
zelavis plugins example --help --url http://localhost:3000/zelavis

Commands are discovered from the running installation’s operation declarations. The example above calls the same handler as GET /zelavis/api/v1/plugins/example/health and client.plugins.example.health.get(). For writes, use --file input.json; path/query arguments use --param name=value and --query name=value. Supply --token when the endpoint requires bearer authentication. Output and errors are JSON, and failures exit nonzero.

See Plugin API for declaring an operation and the current migration limits. Only endpoints with operation declarations appear in this command tree.

  • serve starts the Platform runtime and dashboard.
  • setup runs the animated, interactive first-install wizard and creates the first Platform owner through the bootstrap API.
  • bootstrap creates the first Platform owner account non-interactively.
  • bootstrap status reports whether an owner still has to be created.
  • agent runs the separately supervised Agent that executes Project processes. With --operations-root <dir> --platform-authority <file> it also runs installed host operations the Platform requests over its local socket with Ed25519-signed, request-bound authority. --operation-cgroup delegated (Linux, cgroup v2, systemd Delegate=yes) contains each operation in its own cgroup, optionally limited by --operation-pids-max and --operation-memory-max; it refuses to start rather than fall back when the host cannot provide that. --require-root-owned-operations is for packaged installs. zelavis serve runs Projects through an Agent when ZELAVIS_AGENT_ENDPOINT names its directory.
  • terminal status|list|install|open|attach|close [session-id] is the owner’s shell on the server (client.terminal, GET|POST|DELETE /zelavis/api/v1/runtime/terminal*, permission server.terminal.use in system scope; Server → Terminal in the dashboard). open starts a session and attaches to it, attach picks one up again, and Ctrl-] detaches while the session keeps running. Sessions run inside tmux as the account the Platform runs as (never root) with a small fixed environment, belong to the principal that opened them, and are bounded in number, input size and idle time. install adds tmux through the audited zelavis.packages-install operation (it also needs server.packages.install). Opening and closing are written to the Platform log; typed input and printed output are never recorded. nodeAdapter({ terminal: false }) removes it.
  • logs [--ref REFERENCE] [--level LEVEL] [--source NAME] [--limit N] [--after SEQUENCE] [--follow] reads the Platform’s recent log (client.logs.read, GET /zelavis/api/v1/runtime/logs, permission server.logs.read in system scope). An unexpected request failure answers with a generic message and a reference; --ref prints the cause recorded under that reference. The log is bounded and kept in memory (it is emptied by a restart, and the same error lines are written to the service journal), and secrets are redacted when an entry is written.
  • update status|check|apply [--wait] shows the running version and the newest one on its channel, looks again, or asks the installation to update itself and, with --wait, follows it to the end. It calls the same routes as the dashboard (client.updates). sudo zelavis update --run [--instance NAME] is the other half, run by the zelavis-update systemd unit and never by hand; zelavis update --run --user is the same for a user-mode install, started by the Platform as the same user. It replaces the installed release, so it has no HTTP route and a system update refuses to run unless it is root. See Updating without downtime.
  • worker join --platform-url URL --node-id ID --enrollment-token T makes this machine a Node of a Platform: it generates the Agent’s key and certificate locally, authenticates the Platform (https only; --platform-ca-file or --platform-fingerprint for a private or self-signed one), enrolls, and writes the Agent configuration. Host-local; the enrollment it makes is client.nodes.enroll. worker refresh-trust re-reads the Platform’s public keys over the same pinned connection (a running Agent does this daily by itself). See Node Enrollment.
  • cloud status|connect|disconnect|nodes|request|release|scaling manages cloud capacity (client.cloud): one provider connection and the machines created through it. The provider token is read from ZELAVIS_CLOUD_TOKEN, never from an option, so it stays out of shell history. See Node Enrollment.
  • nodes list|platform|audit|enroll-token|enroll|remove manages the machines that join this Platform (client.nodes). enroll-token <node-id> issues a single-use credential and shows it once; enroll <node-id> --enrollment-token T --cert-file F --agent-url URL is what the joining machine runs, and --trust-out FILE saves the Platform’s trust keys for its Agent. See Node Enrollment.
  • host-operations catalog|submit|get|audit requests host operations through the Platform (client.hostOperations.*, /zelavis/api/v1/runtime/host-operations). submit <operation> --project <id> --arg name=value needs the permission the operation’s installed manifest names for that Project (or system scope). An operation that declares a JSON result returns it in get. audit [--project <id>] [--limit N] lists issuance records (never argument values) with the audit permission. Submissions are rate limited per caller (HTTP 429 with Retry-After). Available when the Platform runs with an Agent.
  • projects list|get|create|rename|start|stop|restart|upgrade|logs|remove manages Projects, and projects recipes lists Project recipes. Each command calls the same route as the JavaScript client (client.projects.*) and /zelavis/api/v1/runtime/projects. With --json the output is the SDK result and failures are JSON with the HTTP status; a recipe whose required isolation the server cannot provide fails with code: "project.isolation.unsatisfied". The backend is chosen by server policy; there is no backend option. projects versions [id] lists qualified installed native App engines. projects switch-version <id> --engine-version <exact-version> selects one with its matching App recipe. create <name> and upgrade <id> also accept --engine-version; their default is the latest qualified installed engine. create also takes --method ID and --software-version V for a recipe that offers install choices (projects recipes shows them). Running qualified Apps switch through the shared handover at their existing address. Source installations and unsupported drivers report why independent selection is unavailable. projects setup <id> [--reveal] shows the values an installed application’s own setup asks for (a database address and name, a generated password), from GET|POST /zelavis/api/v1/runtime/projects/:projectId/setup[/reveal] and client.projects.setup|revealSetup. A secret carries no value until --reveal, which needs project.setup.reveal and is recorded in the audit trail.
  • auth service-accounts list|create|rotate|revoke manages revocable Platform machine identities through client.auth.admin.*. Creation and rotation print a token once. Repeated --permission options add Platform permissions; --project <id> adds the standard scoped Project operator grants.
  • system-store namespaces|records <namespace> inspects the Platform’s own backend tables through client.runtime.systemStore and /zelavis/api/v1/runtime/system-store/namespaces. Use --limit (1–200), --after with the returned next key, and --json for paginated records. Inspection is read-only, redacts secrets, and requires the system-scoped server.database.inspect permission. Supply --token for authentication.
  • services list lists service registry entries.
  • services register registers an ESM service specifier.
  • services install activates a registered service.
  • services disable returns a service to the available state.
  • uninstall --all --dry-run shows every native packaged-install artifact that complete removal owns. uninstall --all --confirm DELETE-ALL-ZELAVIS-DATA then removes the Platform, Agent, releases, package records, Zelavis data and local configuration. It is a host-local adapter operation, not an HTTP endpoint. A current installer receipt covers create/package installs; plain npm/source copies without one use their originating lifecycle. The inventory includes installer/data locks and the Platform ownership record.

zelavis install --from-release /absolute/release uses a staged release, while zelavis install --from package --version <exact-version> acquires its matching verified prebuilt archive. Both use the same plan; --dry-run inspects it, --user selects user mode and --json selects structured output. Existing receipts identify the source, entry, mode, selected instance, port and Edge ownership. Foreign layouts, live data owners and occupied or reserved instance ports are refused. --force only permits command replacement; stop user processes before maintenance. A matching owned systemd Platform can be stopped and restarted for repair/upgrade.

zelavis doctor [--user|--system] [--json] inspects PATH, receipt/current/private Node, data owner, service state, ports and Agent cgroup/delegation. Use sudo for root-readable system receipts. It writes no files, takes no locks and downloads nothing; errors return exit status 1. Install, doctor and complete removal are host-local commands with no HTTP/dashboard route. Use --instance <name> on install, serve, doctor and complete removal for a named Linux/systemd instance. A new install needs an explicit --port (1024–65535). User mode supports default only. The selected current/receipt/runtime descriptor live under /opt/zelavis/instances/<name>, with data/config/account/unit names scoped to the instance. Releases and the management command remain shared. Only default may own host Edge; secondary instances run with Edge off. uninstall --instance <name> --all removes all of that instance’s data and retains shared state needed by other receipts. The last instance removes shared releases, templates and package/APT state. Removing default releases its host Edge record/kernel lock. Use --url for endpoint-backed commands on a secondary Platform; --instance never changes a remote target.

The CLI is the intended layer for operator-facing runtime workflows such as installing, activating, disabling, or removing services. Those workflows should use official Zelavis runtime APIs and adapter-provided capabilities instead of application code passing service toggles into new Zelavis(...).

Adapter-specific behavior should stay behind adapter boundaries. The CLI can ship first-party commands for supported adapters, but concrete host mechanics such as local file caches or future Deno runtime setup should live in adapter modules that the CLI orchestrates.

Complete host removal is the intentional exception to endpoint-backed command parity. It destroys the running Platform, its Agent, its authority material and all Project data, so it must be initiated locally on the machine. The runtime-neutral ZelavisInstallationUninstaller contract is exported by zelavis/runtime; the Node host implementation is createNodeInstallationUninstaller from zelavis/adapters/node.

Framework bootstrap commands are intentionally absent. Zelavis is installed as a long-running Platform OS; framework bindings are adapters and provider capabilities are plugins rather than hosts for the Platform itself. The bootstrap command below is a different thing: it claims the first owner of an already-running Platform, not a project scaffold.

A freshly installed Platform has no accounts. Until one exists there is nobody to sign in as, so claiming the first owner is a one-time operation guarded by a token the operator sets on the server:

Terminal window
ZELAVIS_BOOTSTRAP_TOKEN="$(openssl rand -hex 32)" zelavis serve

The endpoint stays closed unless that variable is set, so a Platform started without it cannot have an owner claimed by whoever reaches it first. With the Platform running, use the guided setup from the same machine:

Terminal window
zelavis setup

The setup wizard checks the installation, guides the owner identity and security choices, and never prints the resulting owner session. It is the recommended interactive flow. The equivalent scriptable command is:

Terminal window
zelavis bootstrap --email owner@example.com --display-name "Owner"

The command prompts for the password with the terminal echo turned off. There is deliberately no --password option: anything passed as an argument is readable in shell history and in the system process list, which is not an acceptable place for the credential that owns the Platform. Passing it is refused rather than ignored. For unattended installs, pipe it instead:

Terminal window
printf '%s' "$OWNER_PASSWORD" | zelavis bootstrap --email owner@example.com --password-stdin

To check where an installation stands before doing anything:

Terminal window
zelavis bootstrap status

That reports whether an owner exists, whether the bootstrap token is configured, and which credential providers are available. Password sign-in is part of Zelavis, so nothing has to be installed first. One provider covers both identifier kinds: an identifier containing @ is treated as an email address, anything else as a username.

The Authorization Code flow is part of Zelavis: it holds the state, nonce, and PKCE verifier, which are the parts that are dangerous to get wrong and the same for every provider. What differs per provider — endpoints and claim mapping — is a definition, and Google, GitHub, and a generic OIDC builder ship in the box.

The credentials your installation was issued are yours to supply:

Terminal window
curl -X PUT https://example.com/zelavis/api/v1/auth/oauth/connections/github -H 'content-type: application/json' -d '{
"clientId": "...",
"clientSecret": "...",
"redirectUri": "https://example.com/zelavis/api/v1/auth/oauth/github/callback"
}'

The client secret is write-only: no endpoint returns it, and omitting it on a later write keeps the stored one. A redirect URI must use https outside localhost, because the authorization code arrives on that URL and a code is enough to complete a sign-in.

An installation that must come up already configured can set ZELAVIS_AUTH_<PROVIDER>_CLIENT_ID, _CLIENT_SECRET, _REDIRECT_URI, and _SCOPES instead. Anything stored through the API wins.

Any OpenID Connect provider is added by pasting its issuer URL. Zelavis reads the endpoints from the issuer’s own /.well-known/openid-configuration, so Google, Microsoft, Okta, Auth0, Keycloak, or your company’s SSO needs no plugin and no release when an endpoint moves:

Terminal window
curl -X PUT https://example.com/zelavis/api/v1/auth/oauth/connections/acme \
-H 'content-type: application/json' \
-d '{
"issuer": "https://id.example.com",
"clientId": "...",
"clientSecret": "...",
"redirectUri": "https://example.com/zelavis/api/v1/auth/oauth/acme/callback"
}'

The document’s own issuer must match the URL requested and every endpoint must be https — one of them supplies the keys that decide whether an ID token is genuine.

A plugin is only needed for a provider that is not OIDC at all, where the profile endpoint and claim mapping are real code. Ship one declaring "capabilities": ["zelavis/identity:oauth"] whose service exports oauthProviders. A definition with an issuer must also give a jwksUrl: an ID token nobody can verify is attacker-supplied JSON, so one is refused at definition time.

A plugin that extends another declares a capability owned by it — zelavis/identity:oauth, not the domain namespace provider:auth. It is an ordinary plugin in every other way; the only difference is who it points at.

That relationship is what lets an extension be listed beside the plugin it extends rather than in a general catalogue:

Terminal window
zelavis extensions
zelavis extensions --for zelavis/identity

The same over the API, which is what a plugin’s own settings page asks for:

GET /zelavis/api/v1/runtime/extensions?owner=zelavis/identity

Each entry in runtime/services carries extends, so a general listing can leave extensions out. Installing one whose owner is not present is refused: the plugin it points at is what discovers it, so on its own it would look installed and do nothing.

Runtime service management is available through the services command group:

Terminal window
zelavis services list
zelavis services register --specifier https://example.com/service.mjs
zelavis services register --specifier https://example.com/service.mjs --install
zelavis services install @zelavis/ecommerce
zelavis services disable @zelavis/ecommerce

The commands talk to http://localhost:3000/zelavis by default. Use --url when the runtime is mounted elsewhere:

Terminal window
zelavis services list --url http://localhost:8787/zelavis

Services can be dropped into a folder on the server instead of composed in code. The Platform scans <data directory>/services at boot:

/var/lib/zelavis/services/
acme-analytics/package.json
@acme/theme/package.json

Each package needs a package.json with a zelavis block naming its kind, type: "module", and an exports entry. Scoped packages nest one level deeper, exactly as they do in node_modules. Restart the Platform to pick up changes.

Code in this folder runs with Platform authority, which is inherent to a folder on your own server — treat adding a package there as seriously as installing one. Discovery still refuses anything it cannot verify: an exports entry that resolves outside its own package directory, a missing entry file, an invalid manifest, an invalid capability, or a package claiming a reserved core service name. Each is skipped with a logged reason, and a package that throws on import is skipped too, so one bad package cannot stop the Platform from booting.

A capability says which contract a service satisfies. Alongside the Platform’s own namespaces (api:routes, provider:auth), a capability can be owned by the package that defines it:

{
"zelavis": {
"kind": "plugin",
"capabilities": [
"zelavis/identity:credentials"
],
"namespace": "example"
}
}

This is how a provider says which plugin it extends. provider:payments states what interface a plugin implements but not whose contract it is, so two commerce plugins scanning for it would each pick up the other’s providers.

There is no parent/child relationship behind this. Discovery is a flat scan, naming an owner asks to be considered by it and grants nothing, and the owning plugin still validates every provider against its own registration contract.

The distribution seeds the services it ships into the services folder the first time it starts, the way a CMS lays down its bundled plugins. They are ordinary packages from that point on: list them, replace them, or delete them. A deleted one stays deleted rather than reappearing on the next restart.

Packages in the folder can import zelavis — the running Platform is linked into <data directory>/services/node_modules so Node’s resolution finds it, and the link always points at the Platform that loaded the package rather than another copy on the machine.

A static frontend is built once and served from wherever it is installed. Two manifest fields make that work:

{
"zelavis": {
"kind": "frontend",
"frontend": {
"runtime": "static",
"bundle": "build/client",
"mode": "spa",
"assetBase": "/assets/",
"basePathGlobal": "__ZELAVIS_BASE_PATH__"
}
}
}

assetBase names the prefix the bundle’s own references were built against, so the Platform can rewrite them to the real mount — including unquoted CSS references such as url(/assets/font.woff2).

assetBase moves references that appear in the markup. It cannot tell a client-side router where it lives, because that is a value the bundle reads at boot rather than a path to rewrite. basePathGlobal names a global the Platform defines on the served page, holding the mount path; the bundle reads it and configures its own router. The Platform knows nothing about what reads it, so this works for any framework.

Without basePathGlobal a bundle must be built for a fixed mount, which is what kept a frontend from being installed anywhere but the path it was built for.