The purros binary is the API server, the background worker and the admin tool for setting up, recovering and backing up an install. In Docker, run it inside the api container:
docker compose exec api purros <command> [options]Outside Docker, configuration comes from the environment and from config/purros.env (or $PURROS_CONFIG_DIR/purros.env) when that file exists. Use --env-file <path> (or PURROS_ENV_FILE) to load a different file. In local development: go run ./cmd/purros <command> in api/.
purros help lists every command, and purros <command> --help explains one with examples. Shell completion: purros completion bash|zsh|fish|powershell.
| Option | Effect |
|---|---|
--env-file <path> |
Load environment variables from this file instead of $PURROS_CONFIG_DIR/purros.env. Variables already set in the environment win. |
--json |
Machine-readable output, for scripts and monitoring |
-y, --yes |
Answer yes to confirmations (for scripts) |
--no-input |
Never prompt; fail when something is missing |
Commands prompt only when run in a terminal. Destructive commands ask for confirmation. Restoring over existing data asks you to type the company name.
Every command that changes data is recorded in the audit log as the system actor named cli.
| Command | What it does |
|---|---|
serve [--no-migrate] |
Runs the API, and the background worker unless PURROS_RUN_WORKER=false. Applies migrations on start. |
worker |
Runs only the worker: webhooks, email, KPI alerts and scheduled backups. Run several for heavy loads. |
migrate [--dry-run] |
Applies pending migrations (or lists them with --dry-run). |
migrate status |
Every migration and when it was applied |
status |
An overview: version, schema, accounts, queues, email, backup schedule and the last backup |
doctor |
Checks the installation and exits with code 1 when something is wrong (see below) |
version |
Prints the version |
doctor runs these checks:
| Check | What it tests |
|---|---|
| Configuration | Required values are present |
| Database | Connection and migrations |
| Setup | The company exists, and an Owner can sign in |
| Encryption secret | PURROS_SECRET decrypts the stored secrets |
| The SMTP server is reachable and offers STARTTLS | |
| Queues | Stuck events, disabled webhook endpoints, failed emails |
| Stock ledger | Every stock level matches the sum of its movements |
| Time zones | Every location has a valid time zone |
| Backups | A backup succeeded recently, and scheduled backups are on |
Warnings (!) don't fail the run.
Writes purros.env (with a new PURROS_SECRET) and postgres.env (with a new database password) into the configuration directory, ./config by default ($PURROS_CONFIG_DIR). Both are readable only by you. --force rewrites existing files, but always keeps their PURROS_SECRET and passwords.
purros init --url https://erp.example.com [--dir /opt/purros]Creates the company, the system roles, the first Owner and, optionally, the first location. It then prints a one-time link (valid 7 days) for the Owner to set a password. It only works once.
In a terminal it asks for everything, with suggestions such as the server's time zone. For scripted installs, pass every value as flags:
purros setup --company "Acme Coffee" --owner-email owner@example.com --owner-name "Alex Kim" \
--timezone America/Chicago --currency USD \
--location-name "Store 101" --location-external-id 101 --location-cutoff 04:00purros config show # the effective configuration, secrets masked
purros config check # validate itpurros secret generate # a new random secret
purros secret check # does PURROS_SECRET decrypt the stored data?
purros secret rotate --new-secret-file new # re-encrypt everything with a new secretUse rotate if PURROS_SECRET may have leaked:
- Stop PurrOS.
- Run the rotation. It re-encrypts integration settings, webhook secrets and authenticator secrets in one transaction, and aborts without changes if any value can't be decrypted.
- Put the new secret in
config/purros.envand start PurrOS again.
Keep the old secret as long as you keep backups made before the rotation.
purros storage test [--backups] # write, read and delete a test file (--backups: the backup bucket)
purros storage init [--backups] # create the S3 bucket if it doesn't exist
purros storage verify [--checksums] # every attachment's file is in storage (and matches, with --checksums)
purros storage migrate --to s3|local # copy all files to the other storage; resumablepurros email test --to you@example.com # send a test email, showing the SMTP error if it fails
purros email log [--limit 30] # recent emails: recipient, kind, status and errorsAll account commands take the email address as an argument (or --email).
| Command | What it does |
|---|---|
users list [--status …] [--role …] |
Accounts with role, status, 2FA, lockout and last sign-in |
users show <email> |
One account, with its sessions and personal keys |
users invite <email> [--name] [--role] [--employee-id] |
Creates an account and prints its invitation link |
users sign-in-link <email> [--reset-mfa] |
Prints a one-time link: an invitation, or a password reset. Also clears a lockout. |
users unlock <email> |
Clears a lockout after failed sign-ins |
users reset-mfa <email> |
Removes their authenticator app and recovery codes, and signs them out |
users sign-out <email> |
Ends all their sessions |
users set-role <email> --role <name or ID> |
Changes their role. --role owner makes them an Owner. |
users deactivate <email>, users reactivate <email> |
Deactivating signs them out and stops their personal keys |
roles list |
Roles with members and permissions |
The last Owner can't be demoted or deactivated.
This is break-glass access for when nobody can sign in. It makes the account an Owner, activates it, clears its lockout, removes its 2FA (unless --keep-mfa) and sessions, and prints a sign-in link.
purros recover owner owner@example.com
purros recover owner new-admin@example.com --create --name "Pat Lee"Backups are complete and consistent, and taken while PurrOS keeps running. PurrOS makes them itself, so no pg_dump is needed. Each is a single .purros-backup file, optionally encrypted, and optionally including uploaded files. With PURROS_BACKUP_S3_ENABLED, backups are also uploaded to an S3 bucket. See Backups & upgrades for how they work and how to schedule them.
purros backup create [--dir DIR | --out FILE] [--encrypt] [--passphrase-file F] [--keep N]
[--files | --no-files] [--no-upload]
purros backup list [--dir DIR | --remote] # files (or the S3 bucket), and recent backup runs
purros backup download <name> [--out PATH] # copy a backup from the S3 bucket
purros backup verify <file | s3:name> # complete and undamaged?
purros backup inspect <file | s3:name> # what it contains, and whether PURROS_SECRET matches
purros backup restore <file | s3:name> [--replace] [--no-safety-backup]
purros backup prune [--keep N] [--older-than 30d] [--remote] [--dry-run]- Where backups go: the default directory is
PURROS_BACKUP_DIR, or$PURROS_STATE_DIR/backupswhen S3 uploads are off. With S3 on and no directory, backups go only to the bucket. - Remote backups: refer to a backup in the bucket as
s3:<name>. - Passphrase: it comes from
--passphrase-file, thenPURROS_BACKUP_PASSPHRASE, then a prompt.
Integrations, webhook endpoints and the organization can also be managed through the API; see Integrations, Webhooks and Organization & locations.
| Command | What it does |
|---|---|
locations create --name … [--code] [--external-id] [--timezone] [--currency] [--cutoff] |
Adds a location (and sends location.created). --cutoff is when its business day ends; time zone and currency default to the company's. Org units and departments are managed with the API. |
locations list |
Lists locations |
integrations register --manifest … [--config k=v] [--approve-sensitive] |
Registers an integration and prints its API key and webhook secret once. --config values are converted to each field's type. |
integrations update <name> --manifest … [--config k=v] [--approve-sensitive] |
Installs a new version of its manifest (scopes, config fields, webhooks). Prints a webhook secret only if the update adds an endpoint. |
integrations list |
Status, health, last heartbeat, keys and scopes |
integrations pause <name>, integrations resume <name> |
Pausing makes its keys stop working and holds its webhooks; resuming sends them |
integrations rotate-key <name> [--grace 1h] |
Issues a new API key (printed once). The old keys keep working for --grace (0 revokes them now, at most 168h). |
integrations remove <name> |
Removes it with its keys, config, logs and webhook endpoint (asks to confirm) |
webhooks list |
Every webhook endpoint with its integration, status, failure count, and pending and failed deliveries |
webhooks enable <endpointId>, webhooks disable <endpointId> |
Re-enabling resets the failure count and sends waiting deliveries |
webhooks retry-failed <endpointId> |
Queues every failed delivery again, e.g. after a long outage |
api-keys list, api-keys revoke <keyId> |
Active integration and personal keys |
features list, features enable <key>, features disable <key> |
Switches features on or off, together with what they need or what depends on them |
| Command | Purpose |
|---|---|
features purge <key> |
Permanently delete a disabled feature's data |
export |
Export all company data as JSON and CSV |
recalculate |
Rebuild derived data, e.g. after changing usage recipes |
generate-vapid |
Keys for web push notifications |
anonymize --employee … |
Anonymize a former employee after the retention period |