Developer docs

Zeloxa CLI

Deploy a site from your terminal or CI with the zeloxa command: log in with an API token, link a folder, deploy, roll back, read logs and manage domains.

On this page

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 deploy

Install

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 installing

The CLI has a single dependency (fflate, for zipping) and no install scripts.

Quick start

  1. Create a token. In the Zeloxa dashboard go to Hosting → API tokens, create a token with the host:deploy scope (or host:admin if you also want to manage domains from the terminal), and copy it. It is shown once.

  2. Log in and paste the token when asked. It is checked with Zeloxa, then saved on this machine:

    zeloxa login
  3. Link your project folder to a site. Pick it from the list:

    cd my-site
    zeloxa link
  4. Build and deploy. Without a folder argument, deploy uses the first of dist, out and build that contains files:

    npm run build
    zeloxa deploy

    The deployment is live as soon as the command finishes, and the site's URL is printed.

  5. 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.

CommandWhat it doesScope
zeloxa loginSave an API token on this machineany
zeloxa logoutRemove the saved token from this machinenone
zeloxa whoamiShow the organization, token and scopes in useany
zeloxa linkLink this folder to a site (.zeloxa/project.json)host:read
zeloxa sitesList the sites in your organizationhost:read
zeloxa openPrint the site's URLhost:read
zeloxa deploy [dir]Upload a build folder and make it livehost:deploy
zeloxa deploymentsList recent deploymentshost:read
zeloxa rollback [n]Serve the previous deployment again, or deployment #nhost:deploy
zeloxa promote <n>Serve deployment #nhost:deploy
zeloxa logs [n]Print a deployment's log (default: the newest)host:read
zeloxa domainsList the site's custom domainshost:read
zeloxa domains add <hostname>Add a domain and print the DNS records to createhost:admin
zeloxa domains verify <hostname>Check a domain's DNS records and certificatehost:read
zeloxa domains rm <hostname>Remove a domain from the sitehost:admin

login

zeloxa login                           # hidden prompt
echo "$ZELOXA_TOKEN" | zeloxa login    # from stdin
zeloxa login --api-url http://localhost:3000   # a local Zeloxa for development

logout

zeloxa logout

whoami

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.

zeloxa link                  # numbered picker
zeloxa link --site my-blog   # by slug or id, no prompt

Writes .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)"    # Linux

deploy

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 token

deploy 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 MB

rollback

zeloxa rollback        # the newest ready deployment older than the live one
zeloxa rollback 3      # deployment #3

Nothing 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 4

Only deployments whose status is ready can be served.

logs

zeloxa logs        # the newest deployment
zeloxa logs 4
2026-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 skip

domains 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

OptionMeaning
--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)
--jsonPrint one JSON document on stdout instead of text
--no-colorPlain output
-h, --helpShow help
-v, --versionPrint the CLI version

Choosing the site

Commands that act on a site use, in order:

  1. --site <id|slug>
  2. ZELOXA_SITE_ID (an id or a slug)
  3. .zeloxa/project.json in 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/, .zeloxaignorethe CLI's own files
symbolic links inside the foldernot 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, *.pfxcredentials, never published; reported like .env files
a deploy folder that is itself a symbolic linkrefused; 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-run

Limits

Files5,000
One file25 MB
Whole build (unzipped)100 MB
Upload (zipped)100 MB
Path length1,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).

Commandstdout
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

CodeMeaning
0Success
1The command failed: an API error, a network problem, a build over the limits, a cancelled prompt
2The 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

VariableMeaning
ZELOXA_TOKENAPI token. Overrides the saved login; meant for CI
ZELOXA_SITE_IDSite id or slug, for CI without zeloxa link
ZELOXA_API_URLAPI address (default https://zeloxalabs.com)
XDG_CONFIG_HOMEWhere the saved login lives ($XDG_CONFIG_HOME/zeloxa/config.json)
NO_COLORAny non-empty value turns colours off. Colours are also off whenever stdout is not a terminal, and with --json
ZELOXA_DEBUGSet to print a stack trace for unexpected errors (Error (UNEXPECTED))

Token scopes

Scopes nest: host:admin includes host:deploy, which includes host:read.

ScopeCommands
nonelogout, deploy --dry-run, help
any tokenlogin, whoami
host:readlink, sites, open, deployments, logs, domains, domains verify
host:deploydeploy, rollback, promote
host:admindomains 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:

  1. Create a token with the host:deploy scope under Hosting → API tokens.
  2. In your GitHub repository, go to Settings → Secrets and variables → Actions:
    • Secrets tab: add ZELOXA_TOKEN with the token.
    • Variables tab: add ZELOXA_SITE_ID with your site's id or slug (zeloxa sites lists them).
  3. Add this file as .github/workflows/zeloxa.yml, changing dist to 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: production shows the deployed URL on the run's page. If you use environment protection rules or environment secrets, put ZELOXA_TOKEN there instead.
  • To roll back from a workflow, run npx --yes zeloxa@0.1.0 rollback with 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.

Ready to try it?

Create an API token in the dashboard. You choose what it can do, and it is shown once.

Get an API token

Something unclear or missing? Tell us.