PurrOSDocs
Operations

CLI reference

Every purros command: server, setup, configuration, accounts and recovery, backups, and managing integrations, webhooks and features.

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.

Global options

OptionEffect
--env-file <path>Load environment variables from this file instead of $PURROS_CONFIG_DIR/purros.env. Variables already set in the environment win.
--jsonMachine-readable output, for scripts and monitoring
-y, --yesAnswer yes to confirmations (for scripts)
--no-inputNever 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.

Server

CommandWhat it does
serve [--no-migrate]Runs the API, and the background worker unless PURROS_RUN_WORKER=false. Applies migrations on start.
workerRuns 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 statusEvery migration and when it was applied
statusAn overview: version, schema, accounts, queues, email, backup schedule and the last backup
doctorChecks the installation and exits with code 1 when something is wrong (see below)
versionPrints the version

doctor

doctor runs these checks:

CheckWhat it tests
ConfigurationRequired values are present
DatabaseConnection and migrations
SetupThe company exists, and an Owner can sign in
Encryption secretPURROS_SECRET decrypts the stored secrets
EmailThe SMTP server is reachable and offers STARTTLS
QueuesStuck events, disabled webhook endpoints, failed emails
Stock ledgerEvery stock level matches the sum of its movements
Time zonesEvery location has a valid time zone
BackupsA backup succeeded recently, and scheduled backups are on

Warnings (!) don't fail the run.

Setup & configuration

init

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]

setup

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 [email protected] --owner-name "Alex Kim" \
  --timezone America/Chicago --currency USD \
  --location-name "Store 101" --location-external-id 101 --location-cutoff 04:00

config

purros config show      # the effective configuration, secrets masked
purros config check     # validate it

secret

purros 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 secret

Use rotate if PURROS_SECRET may have leaked:

  1. Stop PurrOS.
  2. 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.
  3. Put the new secret in config/purros.env and start PurrOS again.

Keep the old secret as long as you keep backups made before the rotation.

storage

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; resumable

email

purros email test --to [email protected]     # send a test email, showing the SMTP error if it fails
purros email log [--limit 30]              # recent emails: recipient, kind, status and errors

Accounts & recovery

All account commands take the email address as an argument (or --email).

CommandWhat 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 listRoles with members and permissions

The last Owner can't be demoted or deactivated.

recover owner

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 [email protected]
purros recover owner [email protected] --create --name "Pat Lee"

Backups

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/backups when 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, then PURROS_BACKUP_PASSPHRASE, then a prompt.

Management

Integrations, webhook endpoints and the organization can also be managed through the API; see Integrations, Webhooks and Organization & locations.

CommandWhat 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 listLists 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 listStatus, 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 listEvery 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

Planned commands

CommandPurpose
features purge <key>Permanently delete a disabled feature's data
exportExport all company data as JSON and CSV
recalculateRebuild derived data, e.g. after changing usage recipes
generate-vapidKeys for web push notifications
anonymize --employee …Anonymize a former employee after the retention period

On this page