Skip to content

Admin CLI reference

The spanlight command is the operator’s tool for a self-hosted Spanlight. It creates users, resets credentials, runs migrations, takes backups and runs maintenance jobs by hand. It is not the Python SDK; for that see the Python SDK reference.

The command is installed in the backend image that the api and worker services run. With Docker Compose, run it from the repository root:

Terminal window
docker compose -f deploy/compose.yaml --env-file deploy/.env exec api spanlight --help

The examples on this page show only the spanlight ... part. Put that prefix before them. From a source checkout, run cd backend && uv run spanlight ....

The CLI reads the same settings as the API: environment variables, and a .env file in the working directory. Most commands use DATABASE_URL. Each command names any other setting it needs.

Every command takes --help. Two kinds of failure exit with a non-zero status:

  • A bad option or value prints a usage error and exits with status 2.
  • A runtime failure prints Error: ... to standard error and exits with status 1.
Command What it does
create-user Creates a user, optionally in an organization.
reset-password Sets a new password and signs the user out everywhere.
reset-2fa Turns two-factor authentication off for a user.
sync-prices Inserts the bundled model price snapshot.
prices diff Compares the bundled price snapshot with LiteLLM’s public price file.
reseal-credentials Re-seals provider credentials and billing source admin keys under the first encryption key.
license-status Shows the edition and what the enterprise license key grants.
migrate Applies database migrations.
ensure-app-role Creates the database role the API and worker connect as.
rollups backfill Recomputes hourly metric rollups for a time range.
recompute-usage-rollups Flags usage rollups and retotals the traces of a recent period.
alerts evaluate Runs one alert evaluation pass now.
demo seed-evals Seeds the demo workspace with an experiment.
backup now Takes a database backup now.
backup list Lists stored backups.
restore Restores a backup into an empty database.
spanlight create-user --email TEXT --name TEXT [--org TEXT] [--role owner|admin|member|viewer]
Option Required Meaning
--email yes The user’s email address.
--name yes Display name.
--org no Slug of an existing organization to add the user to.
--role no The role in --org: owner, admin, member or viewer. The default is member.

The command asks for a password twice, hidden. The password must have at least 10 characters. It never takes the password as an option, so it stays out of the process list and the shell history.

What it changes. It inserts the user. With --org it also adds a membership with the given role and writes an audit-log entry for the organization, marked as made through the CLI. Without --org the user belongs to no organization.

Output. Created user <user id> <email>.

Errors. An unknown --org slug and an address that already has an account are refused, and nothing is created.

Terminal window
spanlight create-user --email ada@example.com --name "Ada Lovelace" --org acme --role member
spanlight reset-password --email TEXT

Sets a new password for the user with that address and revokes all their sessions, so they are signed out everywhere. It asks for the new password twice, hidden, with the same minimum of 10 characters. For every organization the user belongs to, it writes an audit-log entry marked as made through the CLI.

Output. Password reset for <email>; all sessions revoked. An address with no account is refused.

Terminal window
spanlight reset-password --email ada@example.com
spanlight reset-2fa --email TEXT

Turns two-factor authentication off for a user who lost both the authenticator app and the recovery codes. It deletes the secret and the recovery codes, as if the user had turned it off. The password and the sessions are left as they are; reset-password is the command that signs the user out everywhere. For every organization the user belongs to, it writes an audit-log entry.

Output. Two-factor authentication turned off for <email>; recovery codes deleted. If the user does not have it turned on, it prints that and changes nothing. An address with no account is refused.

Terminal window
spanlight reset-2fa --email ada@example.com
spanlight migrate

Applies the database migrations up to the latest one (alembic upgrade head) on the database in DATABASE_URL. Run it as the database owner, because migrations create and change tables. It takes no options and prints Database is at the latest migration.

The Compose stack runs it for you in the migrate service on every up, followed by ensure-app-role. See Upgrade Spanlight.

spanlight ensure-app-role [--role TEXT]
Option Default Meaning
--role spanlight_app The role that the API and worker connect as.

Creates or updates that database role and grants it data access. The role is not a superuser and cannot bypass row-level security, which is what keeps one project’s data from another’s.

Run it as the database owner, after migrate and after a restore. The password is read from APP_DB_PASSWORD, which must be set and have at least 16 characters, so it never appears in the process list or the shell history. It prints Role <name> is ready (no superuser, no RLS bypass).

Terminal window
APP_DB_PASSWORD="$(openssl rand -hex 24)" spanlight ensure-app-role --role spanlight_app
spanlight reseal-credentials

Re-seals every provider credential and every billing source admin key under the first key of CREDENTIALS_KEYS (a billing source uploaded as CSV holds no key and is skipped). Run it after you put a new key first in that list. It counts again afterwards, and exits with status 1 while any credential or billing source is still sealed under an older key. When it succeeds, nothing needs the older keys any more, and you can remove them.

It prints Re-sealed <n> provider credentials and <m> billing source keys. and never prints a key. Credentials and billing sources that no key in CREDENTIALS_KEYS can open are left as they are and reported on standard error, with their ids in the log. Without CREDENTIALS_KEYS it fails with Error: CREDENTIALS_KEYS is not set. The full procedure is in the rotate secrets runbook.

spanlight sync-prices

Inserts the model price snapshots bundled with the server, the current one and the historical ones, into the price table. Rows that already exist are kept. It prints Inserted <n> price rows (snapshots up to <version>)., <version> being the current snapshot. The api and worker run the same seeding when they start. See Cost.

spanlight prices diff [--source URL_OR_PATH]

Compares the current price snapshot with LiteLLM’s public model_prices_and_context_window.json (the default --source; a local copy of the file works too) and prints three lists: rates that differ, models priced here that the file does not list, and chat models the file prices that Spanlight does not, each with a proposed price row. It is a review aid for maintainers, not a price source: it changes nothing, and every figure must be checked against the provider’s own pricing page before it goes into a new snapshot. It needs network access to fetch the default source and no database. It exits with status 1 when a rate differs or a model is missing, so it can run as a scheduled check.

spanlight license-status

Shows which edition the instance runs and what the license key in EE_LICENSE_KEY grants. It reads only that variable, from the environment or a .env file in the working directory, and never prints the key. It takes no options.

Without a key it prints Edition: community (EE_LICENSE_KEY is not set). With a key that verifies it prints Edition: enterprise, then the license id, the licensee, the signing key id, the expiry date with the days left (or expired: ee features are off) and the features the license grants. A key that does not verify exits with status 1 and Error: EE_LICENSE_KEY is invalid: ...; the API and worker refuse to start with such a key, so run this command to find out why. The key is verified offline: nothing is sent anywhere.

spanlight audit-verify [--org <slug>]

Recomputes each organization’s audit log hash chain from the stored events. For each organization it prints ok with the number of events, the seq range and the newest hash, no events, or BROKEN at seq <n> (to standard error) for the first event that no longer matches its hash or its predecessor. It exits with status 1 when any chain is broken. --org checks one organization by slug. Keep the printed head hashes somewhere else: a later run whose chain no longer reaches one means recent events were removed.

spanlight rollups backfill --project UUID --from DATETIME --to DATETIME
Option Required Meaning
--project yes The project id.
--from yes Start of the range, inclusive. ISO 8601; a value without a UTC offset is read as UTC.
--to yes End of the range, exclusive. Same format.

Recomputes the hourly metric rollups of one project over the range [--from, --to). Both ends are widened to whole hours. The scheduled job only refreshes the last 48 hours, so use this for spans that arrived more than 48 hours after they started.

The range may not be longer than 90 days, and --from must be earlier than --to. An unknown project is refused. It prints Wrote <n> rollup rows.

Terminal window
spanlight rollups backfill --project 3f1c9a52-8d0e-4c1b-9a6e-2b7d5e41c0aa --from 2026-09-01 --to 2026-10-01
spanlight recompute-usage-rollups --since DURATION [--batch-size N]
Option Required Meaning
--since yes How far back to recompute: days as 30d or hours as 12h.
--batch-size no Traces per transaction. The default is 500.

Flags the spans whose usage repeats a descendant’s (a framework span over the provider call it wraps) and recomputes the token, cost and span totals of every trace started within --since, across all projects, the way ingestion does after each batch. Spans stored before the upgrade that added usage rollups are not re-evaluated otherwise; see Cost.

Each batch of traces of one project is its own transaction, so a run that stops can be repeated: a second run changes nothing it already corrected. A duration that is not Nd or Nh exits with status 2. It prints Recomputed <n> traces.

Terminal window
spanlight recompute-usage-rollups --since 30d
spanlight attribution backfill --project UUID --from DATE --to DATE [--key KEY]
Option Required Meaning
--project yes The project id.
--from yes First UTC day, inclusive, as YYYY-MM-DD.
--to yes Last UTC day, inclusive, as YYYY-MM-DD.
--key no Recompute only this promoted metadata key. By default every kind (trace name, user, release) and every promoted key.

Recomputes one project’s daily attribution rollups over the days [--from, --to], 31 days per transaction. The scheduled refresh covers the last 8 days, so use this for spans that arrived later than that, or to build the days before a key was promoted, which the refresh backfills over 7 days only.

--from may not be later than --to, and may not be before the project’s retention (the next retention pass would delete those days). An unknown project and a key that is not promoted are refused. It prints Wrote <n> rollup rows, removed <n>, folded <n> values.

Terminal window
spanlight attribution backfill --project 3f1c9a52-8d0e-4c1b-9a6e-2b7d5e41c0aa --from 2026-09-01 --to 2026-09-30 --key customer_id
spanlight alerts evaluate [--dry-run] [--json]
Option Meaning
--dry-run Evaluate, then roll every change back.
--json Print one JSON object instead of text.

Runs one alert evaluation pass over every project now, the same work as the worker’s job.

A real pass is not a preview. It writes each rule’s state, opens and resolves events, and queues the notification of every transition in the outbox, which the worker then delivers. While the worker’s own pass runs, the two split the rules between them, so set ALERTS_EVALUATION_ENABLED to false first when you time a pass.

A dry run keeps nothing. All of the work is rolled back at the end, and nothing is queued. It does this in one long transaction, so every rule it evaluates stays row-locked until the end. The worker’s pass skips those rules for that minute, and an edit of a rule waits. Run it when that is acceptable, with the worker’s evaluation off, or against a copy of the database.

Logs go to standard error, so standard output carries only the result:

Evaluated 12 rules in 3 projects in 0.42 s.
ok: 10
no_data: 1
error: 1
Transitions: 2

With --json, the object has the keys projects, projects_failed, rules, ok, no_data, errors, transitions, seconds and dry_run. The counts of ok, no_data and errors add up to rules. A dry run adds the line Dry run: every change was rolled back. to the text form.

The command exits with status 1 when a rule or a project failed. See Alerts and the alerts runbook.

Terminal window
spanlight alerts evaluate --dry-run --json
spanlight doctor precision [--org UUID]
Option Meaning
--org Only the projects of this organization. Without it, every project counts.

Prints how often the Doctor was right, per detector kind, from the “Was this right?” votes members gave on insights. It reads across projects the way the worker does, and prints only counts: no project, insight or person. The output is a Markdown table:

| kind | right | wrong | n | precision | wilson_low | wilson_high | meets_target |
|---|---|---|---|---|---|---|---|
| rate_limit_pressure | 18 | 2 | 20 | 0.9000 | 0.6990 | 0.9721 | yes |

precision is right divided by n, and wilson_low and wilson_high are its Wilson 95 % interval, rounded half up to four places. A kind without votes shows “—” for the three ratios. meets_target is yes when the precision is at least 0.8, the lower bound at least 0.6 and n at least 20, compared before rounding. An unknown --org exits with status 2 and Invalid value for '--org': no organization with id <id>. The same numbers for one project are on the Doctor’s Precision tab; see The Doctor.

spanlight demo seed-evals

Seeds the demo project with a prompt, a dataset, a judge and an experiment. It takes no options. It is for the public demo workspace, not for a normal installation.

It creates the prompt support/answer (version 1 is the demo bot’s answer prompt, version 2 a tightened rewrite; production points at version 2 and staging at version 1), the dataset support-questions from the 50 newest recorded demo answers, the judge relevance, and the experiment answer-v1-vs-v2. An experiment runs one run at a time, so the first call queues the version 1 run. A second call, once that run has ended, queues the version 2 run. The worker executes the runs. They are real provider calls under the demo credential, and each run is capped at $0.50.

It needs 50 recorded demo answers with stored payloads and an Anthropic credential on the demo organization. Set ANTHROPIC_API_KEY and let the worker’s demo job run first. It is safe to repeat: what exists is kept and nothing is queued twice.

It exits with status 1 and Error: demo org has no Anthropic credential when the credential is missing, and with Error: need 50 traces, found N when there are too few answers. If the version 2 run still has to wait for version 1, the output says so. See the demo evaluations runbook.

Backups need object storage (S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY) and BACKUP_DATABASE_URL. The guide Back up and restore shows how to set them up and how to rehearse a restore.

spanlight backup now

Dumps the database to object storage now and prints the key and the size: Backed up to <key> (<n> bytes). It takes no options.

BACKUPS_ENABLED only controls the nightly job, so it does not stop a backup you ask for. Old backups are not pruned by this command; the nightly job removes them. If BACKUP_DATABASE_URL or the object storage settings are missing, the command exits with status 1 and names what is missing.

Terminal window
spanlight backup now
spanlight backup list

Lists the stored backups, oldest first. Each line is the key, the size in bytes and the upload time. It prints No backups found. when there are none, and it exits with status 1 when object storage is not configured.

spanlight restore --key TEXT --target-url TEXT
Option Required Meaning
--key yes The object key of a backup, from spanlight backup list.
--target-url yes The URL of a new, empty database. It can be given by the RESTORE_TARGET_URL environment variable instead.

Restores a backup into an empty database. It refuses a database that already has tables. Prefer the environment variable for the target: a URL on the command line is visible in the process list and the shell history.

On success it prints Restored <key>. and a reminder to run spanlight ensure-app-role against the restored database, so that the API and worker can connect. Follow the restore runbook for the full procedure, including how to switch the restored database in.

Terminal window
RESTORE_TARGET_URL="postgresql://postgres:secret@restore-host:5432/spanlight" \
spanlight restore --key backups/2026/10/09/spanlight-20261009T030000Z.dump