02 · START

Install

XERJ ships as a single static binary. No JVM, no installer, no package manager yet — download the tarball, verify the checksum, move the binary into place, write a 20-line config, start it as a systemd unit.

System requirements

Install with one command

The installer detects your OS and CPU, downloads the matching release from GitHub Releases, and verifies its SHA-256.

Linux / macOS (x86_64 and arm64):

$ curl -fsSL https://xerj.org/get | sh

Windows (x64 and ARM64, PowerShell):

> powershell -ExecutionPolicy Bypass -c "irm https://xerj.org/get.ps1 | iex"

Both installers verify fail-closed: each computes the archive's own SHA-256 and compares it against the published digest, and if the checksum cannot be downloaded, or no SHA-256 tool (sha256sum, shasum, openssl, busybox…) can be found, the installer stops rather than installing an unverified binary.

On Linux the one-liner installs the static musl build by default — it runs on any distro including Alpine and has no glibc-version floor. Ask for the glibc build explicitly if you need it:

$ curl -fsSL https://xerj.org/get | XERJ_LIBC=gnu sh

Other environment overrides: XERJ_VERSION (install a specific tag), XERJ_INSTALL_DIR (default ~/.local/bin), XERJ_REPO.

Manual download

Every release publishes eight targets, each with a .sha256 alongside: x86_64/aarch64 × unknown-linux-musl (static, any distro), unknown-linux-gnu, apple-darwin, and pc-windows-msvc (.zip).

Set target to your platform’s triple (the eight published targets are listed above), then paste the whole block. It runs in a subshell, so a refusal ends the block and not your shell:

(
  set -eu

  ver=$(curl -fsSL https://api.github.com/repos/xerj-org/xerj/releases/latest \
    | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
  target=x86_64-unknown-linux-musl              # set this to YOUR platform's triple
  stage="xerj-${ver}-${target}"
  asset="${stage}.tar.gz"
  base="https://github.com/xerj-org/xerj/releases/download/v${ver}"

  curl -fsSLO "$base/$asset"
  curl -fsSLO "$base/$asset.sha256"

  want=$( { sha256sum "$asset" 2>/dev/null || shasum -a 256 "$asset"; } | cut -d ' ' -f 1 )
  printf %s "$want" | LC_ALL=C grep -qE '^[0-9a-f]{64}$' \
    || { echo "no working SHA-256 tool — refusing to install an unverified $asset" >&2; exit 1; }

  tr -d '\r' < "$asset.sha256" \
    | LC_ALL=C grep -qxF -e "$want  $asset" -e "$want *$asset" \
    || { echo "CHECKSUM MISMATCH for $asset — do not install it" >&2; exit 1; }

  tar xzf "$asset"
  sudo install -m 0755 "$stage/xerj" /usr/local/bin/xerj
  xerj --version
)

The digest is computed here and the .sha256 is searched for that exact line, rather than letting sha256sum -c read a filename out of a file that arrived from the same place as the archive. sha256sum -c skips # comment lines silently and exits 0 when at least one line verified and no listed line failed, so a .sha256 carrying a comment naming the archive plus a valid digest for some other file passes while the archive is never hashed at all. (It does exit 1 on a listed line that fails; the attack does not need one.) Both patterns accept GNU text mode and -b binary mode, and tr -d '\r' accepts a checksum file written on Windows.

Three details carry the weight. The digest is length- and hex-checked before it is used. Without that, a host where neither hashing tool runs leaves want empty, the two patterns collapse to " $asset" and " *$asset", and a .sha256 containing that literal line satisfies the check with nothing hashed. Extraction and install are inside the guard, not after it: an earlier version of this page printed the mismatch and then ran sudo install on the next line, which installs whatever an older extraction left behind. A refusal exits non-zero, so a Dockerfile RUN, a CI step, or an agent sees the failure instead of a silent success.

Deploying to a host with no runtime internet access? Follow the air-gapped deployment recipe for release checksums, optional local neural model files, and the loopback/auth defaults.

macOS Gatekeeper: the release binaries are not code-signed or notarized. A tarball fetched with curl (both the one-liner and the commands above) is not quarantined and runs as-is. A tarball downloaded through a browser is: macOS attaches com.apple.quarantine and the first launch is blocked with "cannot be opened because the developer cannot be verified". Clear it once, then run normally:

$ xattr -d com.apple.quarantine ./xerj

Write the config

The minimal config is three sections. Everything else uses defaults — see the config reference for the full key list.

# /etc/xerj/xerj.toml
[server]
rest_port    = 8080
# Default is "127.0.0.1" (loopback only). Exposing the node while TLS is off
# needs the second line too, or startup refuses.
bind_address = "0.0.0.0"
allow_insecure_network_bind = true
data_dir     = "/var/lib/xerj"

[auth]
enabled = true

With auth.enabled = true, the first start prints an auto-generated admin API key and writes it to <data_dir>/admin.key — every native-API request (including health) must send it, and so must the CLI clients that talk to this node (see Index your first folder below). In-process TLS is not wired yet (rustls integration is on the roadmap) — terminate TLS at a reverse proxy (nginx/caddy) in front of ports 8080/9200 for now.

Run as a systemd unit

Create the service user first — StateDirectory then gives it /var/lib/xerj automatically:

$ sudo useradd -r -s /usr/sbin/nologin xerj
# /etc/systemd/system/xerj.service
[Unit]
Description=XERJ storage engine
Wants=network-online.target
After=network-online.target

[Service]
User=xerj
Group=xerj
StateDirectory=xerj
ExecStart=/usr/local/bin/xerj --config /etc/xerj/xerj.toml
Restart=on-failure
LimitNOFILE=65536
ProtectSystem=strict
ReadWritePaths=/var/lib/xerj

[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now xerj
$ sudo systemctl status xerj
$ curl -s -H "Authorization: ApiKey $(sudo cat /var/lib/xerj/admin.key)" \
    http://localhost:8080/v1/health

Index your first folder

The node above has auth on, so xerj autoindex needs the same key. It does not read /etc/xerj/xerj.toml — the config file belongs to the server, not to its clients — so hand the key over explicitly, with --api-key or the XERJ_API_KEY environment variable. Under the unit above the key file is owned by the xerj user and mode 0600, hence the sudo:

$ export XERJ_API_KEY="$(sudo cat /var/lib/xerj/admin.key)"
$ xerj autoindex ~/my-project

Run it without a key and every request is refused: the run ends in HTTP 401 Unauthorized. autoindex is a client of the ES-compatible port — http://localhost:9200 by default; pass --url for anything else. Full flag list on the CLI reference.

Docker

No published image yet — build it locally from engine/Dockerfile (the image defaults to --insecure --data-dir /data, right for CI and local dev):

$ docker build -t xerj:local engine/
$ docker run -d --name xerj \
    -p 8080:8080 -p 9200:9200 \
    -v xerj-data:/data \
    xerj:local

Source · engine/README.md