The command-line interface for Zeloxa Host: deploy a site from your terminal or from CI, roll it back in seconds, read deployment logs and manage custom domains.
The CLI is a client of the Zeloxa Host API: every command is one or two calls to it, with the same token scopes and error codes. Use the API reference when you would rather script the calls yourself.
npx zeloxa login
npx zeloxa link
npm run build
npx zeloxa deployInstall
Requires Node.js 18.17 or newer (Node 20 or newer recommended).
npm install --global zeloxa # then: zeloxa <command>
npx zeloxa <command> # or run it without installingThe CLI has a single dependency (fflate, for zipping) and no install
scripts.
Quick start
Create a token. In the Zeloxa dashboard go to Hosting → API tokens, create a token with the
host:deployscope (orhost:adminif you also want to manage domains from the terminal), and copy it. It is shown once.Log in and paste the token when asked. It is checked with Zeloxa, then saved on this machine:
zeloxa loginLink your project folder to a site. Pick it from the list:
cd my-site zeloxa linkBuild and deploy. Without a folder argument,
deployuses the first ofdist,outandbuildthat contains files:npm run build zeloxa deployThe deployment is live as soon as the command finishes, and the site's URL is printed.
Changed your mind? Go back to the previous deployment:
zeloxa rollback
Authentication
The CLI uses Zeloxa API tokens (zx_…, 46 characters). There is no browser
login: you create a token in the dashboard with exactly the scopes you want,
which is also what CI uses, so there is one kind of credential and one place
to revoke it.
zeloxa login reads the token from a hidden prompt, from stdin when input is
piped (echo "$ZELOXA_TOKEN" | zeloxa login), or from --token. Prefer the
prompt or stdin: a --token value stays in your shell history.
The token is saved to $XDG_CONFIG_HOME/zeloxa/config.json, or
~/.config/zeloxa/config.json when XDG_CONFIG_HOME is not set. The folder
is created 0700 and the file 0600 (readable only by you), and the file is
written atomically. If the file ever becomes readable by other users, every
command that uses the saved login warns you. On Windows the file relies on
your user profile's permissions.
The saved token is tied to the API address it was saved for. If --api-url
or ZELOXA_API_URL points somewhere else, the CLI refuses to send the saved
token there (API_URL_MISMATCH) rather than leak it to another server.
The CLI never prints a token. Where it identifies one, it shows the first 10
characters (zx_AbC1234…), the same prefix the dashboard lists tokens by.
zeloxa logout deletes the saved login. The token keeps working anywhere
else it is used (CI secrets, other machines) until you revoke it under
Hosting → API tokens.
Commands
Every command accepts the global options. Run
zeloxa help <command> for the details of one command.
| Command | What it does | Scope |
|---|---|---|
zeloxa login | Save an API token on this machine | any |
zeloxa logout | Remove the saved token from this machine | none |
zeloxa whoami | Show the organization, token and scopes in use | any |
zeloxa link | Link this folder to a site (.zeloxa/project.json) | host:read |
zeloxa sites | List the sites in your organization | host:read |
zeloxa open | Print the site's URL | host:read |
zeloxa deploy [dir] | Upload a build folder and make it live | host:deploy |
zeloxa deployments | List recent deployments | host:read |
zeloxa rollback [n] | Serve the previous deployment again, or deployment #n | host:deploy |
zeloxa promote <n> | Serve deployment #n | host:deploy |
zeloxa logs [n] | Print a deployment's log (default: the newest) | host:read |
zeloxa domains | List the site's custom domains | host:read |
zeloxa domains add <hostname> | Add a domain and print the DNS records to create | host:admin |
zeloxa domains verify <hostname> | Check a domain's DNS records and certificate | host:read |
zeloxa domains rm <hostname> | Remove a domain from the site | host:admin |
login
zeloxa login # hidden prompt
echo "$ZELOXA_TOKEN" | zeloxa login # from stdin
zeloxa login --api-url http://localhost:3000 # a local Zeloxa for developmentlogout
zeloxa logoutwhoami
Shows which organization the token belongs to, the token's label and scopes,
where the token came from (--token, ZELOXA_TOKEN or the saved login) and
which API is in use. Useful as the first step of a CI job.
link
zeloxa link # numbered picker
zeloxa link --site my-blog # by slug or id, no promptWrites .zeloxa/project.json:
{
"siteId": "3f2c1b9a-0d4e-4c5b-8a7f-112233445566",
"slug": "my-blog",
"name": "My blog"
}The file holds identifiers only, never a token or an API address, so it is
safe to commit. Commit it if teammates and CI should deploy to the same site;
add .zeloxa/ to .gitignore to keep the link to yourself.
sites
Lists your organization's sites with their slug, URL and last deploy, and marks the one this folder is linked to.
open
Prints the site's URL and nothing else, so it composes:
open "$(zeloxa open)" # macOS
xdg-open "$(zeloxa open)" # Linuxdeploy
zeloxa deploy # first of dist, out, build that has files
zeloxa deploy public # a specific folder
zeloxa deploy . # this folder as it is (must be named explicitly)
zeloxa deploy --dry-run # list what would be uploaded; uploads nothing, needs no tokendeploy checks the token and the site first, then scans the folder, zips it,
and uploads it. On a terminal it shows progress while scanning, packing and
uploading. The deployment goes live as soon as the upload is stored, and the
URL is printed alone on stdout (progress and the summary go to stderr),
so URL=$(zeloxa deploy) works in scripts.
The folder must have index.html (a static site) or _worker.js (a
full-stack app) at the top. If every file sits inside one folder (for example
site/index.html), that folder is treated as the top.
A failed upload is never retried automatically: the server may have stored it even if the reply was lost, and a blind retry would create a second deployment. See Troubleshooting.
deployments
zeloxa deployments # the newest 20
zeloxa deployments -n 50 # up to 100# STATUS SOURCE AGE SIZE
#4 ready cli 2m ago 1.2 MB current
#3 ready github 1d ago 1.2 MB a1b2c3d Fix the footer
#2 error cli 2d ago - Deployment #2 failed: No index.html…
#1 ready upload 5d ago 1.1 MBrollback
zeloxa rollback # the newest ready deployment older than the live one
zeloxa rollback 3 # deployment #3Nothing is rebuilt or re-uploaded: the site switches to a deployment that is
already stored, so it takes effect in seconds. zeloxa promote <n> switches
back.
promote
zeloxa promote 4Only deployments whose status is ready can be served.
logs
zeloxa logs # the newest deployment
zeloxa logs 42026-10-02 10:00:01Z system Received 412.3 KB archive (CLI).
2026-10-02 10:00:01Z system Unpacked 48 files (1.2 MB), runtime static.
2026-10-02 10:00:02Z system Stored 48 files.
2026-10-02 10:00:02Z system Released deployment #4 to https://my-blog.zeloxa.app.Times are UTC. Long logs are fetched page by page and printed as they arrive.
domains
zeloxa domains # list
zeloxa domains add www.example.com # prints the DNS records to create
zeloxa domains verify www.example.com # after creating them
zeloxa domains rm www.example.com # asks first; --yes to skipdomains add prints each DNS record to create at your DNS provider (type,
name, value and why it is needed). Then run domains verify until the domain
shows as verified; DNS changes can take a while to spread. Domains can be
given by hostname (pasted URLs like https://www.example.com/ work) or by id.
domains rm asks for confirmation on a terminal. Without a terminal it
refuses unless you pass --yes, so a script can never remove a live domain
by accident.
Global options
| Option | Meaning |
|---|---|
--token <token> | API token; overrides ZELOXA_TOKEN and the saved login |
--site <id|slug> | Site to act on; overrides ZELOXA_SITE_ID and .zeloxa/project.json |
--api-url <url> | API address (default https://zeloxalabs.com). https:// only, except http://localhost and other loopback addresses |
--cwd <dir> | Run as if started in <dir> (where .zeloxa/ and the build folder are looked for) |
--json | Print one JSON document on stdout instead of text |
--no-color | Plain output |
-h, --help | Show help |
-v, --version | Print the CLI version |
Choosing the site
Commands that act on a site use, in order:
--site <id|slug>ZELOXA_SITE_ID(an id or a slug).zeloxa/project.jsonin the current folder (or--cwd); parent folders are not searched
The token comes from --token, then ZELOXA_TOKEN, then the saved login.
What gets uploaded
Never uploaded, whatever .zeloxaignore says:
node_modules/, .git/ | dependencies and version control |
.env, .env.*, .envrc (any name starting with .env, in any letter case) | environment files can hold secrets, and deployed files are public. The CLI tells you which ones it skipped |
.DS_Store, ._*, __MACOSX/ | macOS metadata, including the AppleDouble files macOS writes on non-Apple drives |
.zeloxa/, .zeloxaignore | the CLI's own files |
| symbolic links inside the folder | not followed, so a link can never pull in a file from outside the folder; reported as a warning |
.ssh, .aws, .gnupg, .docker, .kube, .npmrc, .netrc, .pypirc, id_rsa and other SSH keys, *.pem, *.key, *.p12, *.pfx | credentials, never published; reported like .env files |
| a deploy folder that is itself a symbolic link | refused; pass the real folder instead |
.zeloxaignore
Leave more files out with a .zeloxaignore file, written like a
.gitignore. It is read from the project folder and, if there is one, from
the folder being deployed (in that order, so the second can override the
first). Patterns match paths inside the folder being deployed, which are
also the paths the files are served at.
# Source maps stay private
*.map
# …except this one
!vendor.js.map
# A folder, at any depth
drafts/
# Only at the top of the deployed folder
/staging.html
# Anywhere below assets/raw
assets/raw/**
# Any folder called fixtures, at any depth
**/fixtures/Supported: # comments, blank lines, * (anything but /), ?, [abc],
[a-z] and [!a], ** as a whole path segment (**/x, x/**, a/**/b),
a trailing / for folders only, a leading or middle / to anchor a pattern
to the deployed folder, ! to re-include, and \ to escape. The last
matching line wins, and — as in git — a file cannot be re-included when a
folder above it is excluded.
Check the result without uploading anything:
zeloxa deploy --dry-runLimits
| Files | 5,000 |
| One file | 25 MB |
| Whole build (unzipped) | 100 MB |
| Upload (zipped) | 100 MB |
| Path length | 1,024 characters (longer paths are skipped with a warning) |
The CLI checks these before uploading, so an oversized build fails in seconds with the name of the file at fault.
JSON output
With --json, stdout carries exactly one JSON document and nothing else.
Warnings still go to stderr. Field names follow the Host API (camelCase, ISO
8601 timestamps, explicit nulls).
| Command | stdout |
|---|---|
login | { loggedIn, apiUrl, organization: { id, name }, tokenPrefix, scopes, configPath } |
logout | { loggedOut, configPath, zeloxaTokenSet } |
whoami | { tenant: { id, name }, actor: { type, label }, scopes, via, apiUrl, tokenSource } |
link | { linked: true, site, path } |
sites | { sites: [site] } |
open | { url, site } |
deploy | { deployment, url, site: { id, name, slug }, archive: { files, bytes, compressedBytes } } |
deploy --dry-run | { dryRun: true, dir, runtime, fileCount, totalBytes, archiveBytes, files: [{ path, size }], skipped: { env, symlinks, longPaths } } |
deployments | { deployments: [deployment], currentDeploymentId } |
rollback, promote | { deployment, previousDeploymentId } |
logs | { deployment, lines: [{ seq, stream, line, at }] } |
domains | { domains: [domain], cnameTarget } |
domains add, domains verify | { domain, dnsRecords: [{ type, name, value, note? }], cnameTarget } |
domains rm | { deleted: true, hostname } |
A deployment looks like:
{
"id": "6a0c…",
"siteId": "3f2c…",
"number": 4,
"status": "ready",
"runtime": "static",
"source": "cli",
"fileCount": 48,
"totalBytes": 1258291,
"gitRepo": null,
"gitBranch": null,
"gitCommit": null,
"gitMessage": null,
"errorMessage": null,
"createdAt": "2026-10-02T10:00:00.000Z",
"readyAt": "2026-10-02T10:00:02.000Z",
"current": true
}When a command fails with --json, stdout gets the same error envelope the
API uses (plus a hint when there is one), and the exit code is non-zero:
{
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "This action needs the host:admin scope.",
"hint": "Use a token with the host:admin scope. Create one at https://zeloxalabs.com/dashboard/host/tokens.",
"details": { "required": "host:admin", "granted": ["host:read", "host:deploy"] }
}
}Example: deploy and capture the URL and deployment number in a script:
result=$(zeloxa deploy dist --json)
echo "$result" | jq -r '.url'
echo "$result" | jq -r '.deployment.number'Exit codes and errors
| Code | Meaning |
|---|---|
0 | Success |
1 | The command failed: an API error, a network problem, a build over the limits, a cancelled prompt |
2 | The command line was wrong: unknown command or option, missing argument, no site selected, no build folder found. Running it again unchanged cannot succeed |
Errors are printed to stderr as Error (CODE): message, usually followed by
a line saying what to do next. The code is stable — match on it, not on the
message. Codes from the API (INVALID_TOKEN, INSUFFICIENT_SCOPE,
SITE_NOT_FOUND, DEPLOYMENT_NOT_READY, DOMAIN_TAKEN, PAYLOAD_TOO_LARGE,
RATE_LIMITED, …) are passed through unchanged and are listed in the
API reference; the CLI adds its own for
local problems (NO_SITE, NO_OUTPUT_DIR, NO_ENTRY_POINT,
FILE_TOO_LARGE, TOO_MANY_FILES, API_URL_MISMATCH, NETWORK_ERROR,
TIMEOUT, …).
Environment variables
| Variable | Meaning |
|---|---|
ZELOXA_TOKEN | API token. Overrides the saved login; meant for CI |
ZELOXA_SITE_ID | Site id or slug, for CI without zeloxa link |
ZELOXA_API_URL | API address (default https://zeloxalabs.com) |
XDG_CONFIG_HOME | Where the saved login lives ($XDG_CONFIG_HOME/zeloxa/config.json) |
NO_COLOR | Any non-empty value turns colours off. Colours are also off whenever stdout is not a terminal, and with --json |
ZELOXA_DEBUG | Set to print a stack trace for unexpected errors (Error (UNEXPECTED)) |
Token scopes
Scopes nest: host:admin includes host:deploy, which includes host:read.
| Scope | Commands |
|---|---|
| none | logout, deploy --dry-run, help |
| any token | login, whoami |
host:read | link, sites, open, deployments, logs, domains, domains verify |
host:deploy | deploy, rollback, promote |
host:admin | domains add, domains rm |
For CI, create a token with host:deploy only: it can ship and roll back,
but cannot add or remove domains. Tokens can never create other tokens or
change site passwords, security headers or integrations; those are
dashboard-only.
GitHub Actions
Deploy on every push to main:
- Create a token with the
host:deployscope under Hosting → API tokens. - In your GitHub repository, go to Settings → Secrets and variables →
Actions:
- Secrets tab: add
ZELOXA_TOKENwith the token. - Variables tab: add
ZELOXA_SITE_IDwith your site's id or slug (zeloxa siteslists them).
- Secrets tab: add
- Add this file as
.github/workflows/zeloxa.yml, changingdistto your build folder if it differs:
name: Deploy to Zeloxa
on:
push:
branches: [main]
# Pull-request preview deployments are coming soon. Until then, deploy
# from main only: every Zeloxa deployment goes live as soon as it uploads,
# so a pull_request trigger here would publish unreviewed changes.
# One deployment at a time. A newer push waits for the one in progress (and
# replaces any older run still waiting), so an older build can never finish
# last and overwrite a newer one.
concurrency:
group: zeloxa-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v7
with:
# The job never pushes, so the repository token is not left on disk
# for install scripts and build tools to find.
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
# The token is given to this step only, so dependency install scripts
# and your build never see it.
- name: Deploy
id: deploy
env:
ZELOXA_TOKEN: ${{ secrets.ZELOXA_TOKEN }}
ZELOXA_SITE_ID: ${{ vars.ZELOXA_SITE_ID }}
# The URL is the only thing deploy prints on stdout. Assigning it to a
# variable first keeps a failed deploy failing the step.
run: |
url="$(npx --yes zeloxa@0.1.0 deploy dist)"
echo "url=$url" >> "$GITHUB_OUTPUT"Notes:
- The CLI version is pinned (
zeloxa@0.1.0): the deploy step holds your token, so it should run a version you chose, not whatever was published last. Change the number when you decide to upgrade. environment: productionshows the deployed URL on the run's page. If you use environment protection rules or environment secrets, putZELOXA_TOKENthere instead.- To roll back from a workflow, run
npx --yes zeloxa@0.1.0 rollbackwith the same environment variables.
Troubleshooting
Error (UNAUTHORIZED): Not logged in.
Run zeloxa login, or set ZELOXA_TOKEN.
Error (INVALID_TOKEN): …
The token is malformed, expired or revoked, or the account is suspended.
Create a new one under Hosting → API tokens and run zeloxa login again
(or update the ZELOXA_TOKEN secret). The CLI checks the token's shape
locally first, so a value that is not a Zeloxa token (for example, a GitHub
token pasted into the wrong secret) fails before anything is sent.
Error (INSUFFICIENT_SCOPE): This action needs the host:admin scope.
The token works but cannot do this. Create a token with the scope named in
the message.
Error (API_URL_MISMATCH): …
--api-url or ZELOXA_API_URL points at a different API than the one you
logged in to, so the saved token is not sent there. Unset the variable, run
zeloxa login --api-url <url> for that API, or pass a token for it with
--token.
Error (NO_SITE): No site selected.
Run zeloxa link, pass --site, or set ZELOXA_SITE_ID.
Error (SITE_NOT_FOUND): …
The site does not exist in the token's organization. Check --site or
ZELOXA_SITE_ID, or run zeloxa link again. zeloxa whoami shows which
organization the token belongs to.
Error (NO_OUTPUT_DIR): No build output found …
None of dist, out and build exists with files in it. Build first, or
name the folder: zeloxa deploy public.
Error (NO_ENTRY_POINT): No index.html or _worker.js at the top of …
You are probably deploying the project folder rather than its build output.
Deploy the folder your build writes to.
Error (FILE_TOO_LARGE), Error (TOO_MANY_FILES), Error (PAYLOAD_TOO_LARGE)
The build is over the limits. Leave large or unneeded files out
with .zeloxaignore, then check with zeloxa deploy --dry-run.
Error (INTERNAL_ERROR): Deployment #N was uploaded, but whether it went live could not be confirmed.
Do not deploy again yet. Run zeloxa deployments: if #N is marked
current, it is live and nothing else is needed; if it is ready but not
current, run zeloxa promote N.
Error (NETWORK_ERROR) or Error (TIMEOUT) during deploy
The upload may have reached Zeloxa before the connection failed. Check
zeloxa deployments before deploying again. Behind a corporate proxy, note
that Node.js does not use HTTPS_PROXY by default; recent Node.js versions
can be told to with NODE_USE_ENV_PROXY=1 (see your Node version's docs).
Error (SERVICE_UNAVAILABLE) or Error (HTTP_5xx) during deploy, with no message from Zeloxa
Something between you and Zeloxa answered instead of the API, or the request
died part-way, so the upload may still have been stored and released. Check
zeloxa deployments before deploying again. (An UPSTREAM_ERROR from the API
itself means nothing changed, and deploying again is safe.)
Error (UNEXPECTED_REDIRECT)
The API address answered with a redirect. The CLI does not follow redirects
with your token attached; set --api-url or ZELOXA_API_URL to the address
it redirects to.
Warning: … config.json can be read by other users
Run the chmod 600 command in the warning. zeloxa login sets this for you
on every save.
Something else
Run the command again with ZELOXA_DEBUG=1 for a stack trace of an
unexpected error, and include the output (it never contains your token) when
you contact support.
License
MIT. The full text ships with the package in LICENSE.
