Contents
Why a service is needed
The phone installs the card software. The ops-service keeps the authority and records needed to manage that installation over time.
A blank card arrives with known factory manager keys. CCP replaces them with a set assigned to that card. The service retains those keys, binds the installation to the physical chip and records each attempt. After an interrupted tap or a lost phone, an authorized operator can retrieve the same assigned keys and reconcile the saved work.
- Access: named operator accounts and device-scoped reservations control who can install or reset a card.
- Continuity: saved installation and reset records help distinguish completed work from an unresolved attempt.
- Reuse: a reset can use the recorded manager keys after checking the card's identity, owner authorization and empty state.
The CCP app needs an ops-service for this issuance workflow. Wallets and tills use the card and mint for payments; an installed card can keep operating while its ops-service is down. You can use an issuer's existing service or host your own.
The three kinds of card keys
- Manager keys: the ops-service
- Authorize installing and removing the applet. The issuer keeps them in its operational database and encrypted backups.
- Owner key: the holder wallet
- Controls ownership, PIN changes and card settings. The holder wallet establishes this authority after installation.
- Spending key: the card chip
- Generated on the card at installation. Cashu proofs are locked to it; the applet signs payments with it.
A reset deletes the applet and its spending key. Empty and reconcile the card first; the service's manager-key backup cannot restore deleted card value. See the service threat model.
What you need
- A server with Docker Engine and ComposeLinux AMD64 or ARM64, persistent local storage and a stable Compose project name. Run one service process per SQLite database.
- A hostname and HTTPS reverse proxyFor example,
ops.example.com. This guide uses Caddy on the same host; an existing trusted proxy also works. - Protected credentials and an independent backup destinationKeep a password manager, a separate recovery-key copy and storage outside the service host available.
The downloadable Compose file pins release 0.1.0 by its immutable image digest. SQLite is included. Nextcloud and a notification server are optional choices for your backup and alert tooling.
Start the container
Run these commands on your server, in a new directory for this issuer. Keep its directory name stable: Compose uses it to name the persistent volumes.
mkdir cashu-ops
cd cashu-ops
curl -fL https://cashucard.org/ops-compose.yaml -o compose.yaml
docker compose pull ops setup
docker compose run --rm setup init issuer-admin
docker compose up -d ops
curl -fsS http://127.0.0.1:8821/readyzInitialization reports that private files were created. Readiness returns HTTP 200. The service is listening on host loopback, ready for your HTTPS proxy.
Initialization creates the issuer master and first administrator's credentials in the ops-secrets volume. It refuses an existing secrets directory. Keep ops-data, ops-secrets and ops-backups when replacing containers.
Save your credentials
umask 077
mkdir -p private
chmod 700 private
docker compose cp ops:/secrets/operator-setup.json private/operator-setup.json
docker compose cp ops:/secrets/master-key private/master-key
chmod 600 private/operator-setup.json private/master-keyPrivately import the generated issuer-admin password into your password manager. Save the master with your recovery material, then remove the temporary host copies through your private credential procedure. The console sign-in follows the HTTPS step below.
Issuer custodyThe running SQLite database holds manager keys in plaintext at the application layer. Anyone with database or host access can obtain card-management authority. Restrict host access and protect its disk and volumes; encrypted exports protect the separate backup copies. The holder's owner key and the chip's spending key have the separate roles described above.
Connect your domain
Point your hostname's DNS to this server. Install Caddy, allow inbound ports 80 and 443 to it, and add this block to its Caddyfile, replacing ops.example.com with your hostname. Validate and reload your Caddy service.
ops.example.com {
reverse_proxy 127.0.0.1:8821
}Caddy obtains a trusted certificate for the hostname. The app's API and console share the same origin; preserve the Authorization header. A proxy in another container needs its own private networking arrangement, since 127.0.0.1 there refers to that container. See Caddy's reverse proxy guide.
Open https://ops.example.com/ops/operators, sign in as issuer-admin, then create named accounts for everyday operators. Administrator access is configured by OPS_OPERATOR_ADMINS; include every intended administrator's existing name in that JSON array.
Remove the one-time setup file
After saving the credentials and confirming sign-in, remove the OPS_OPERATOR_SETUP_FILE line from compose.yaml, then run:
docker compose up -d ops
docker compose run --rm --entrypoint node setup -e 'require("node:fs").unlinkSync("/secrets/operator-setup.json")'Keep the master and its protected recovery copy. Future password recovery uses an explicit new setup command through a private file; see operator access recovery.
Connect the CCP app
- Open Cashu Card Provisioner. On sign-in, choose Ops service → Change.
- Add your HTTPS origin, for example
https://ops.example.com, and confirm the service selection. Use the origin without/ops/operators. - Sign in with an operator account created on this service. Confirm the displayed hostname before creating a batch.
Each service has its own accounts, card records and manager keys. Saved batches and resets remain bound to their original service address and operator. Use the same canonical hostname on every operator phone.
Backups and recovery
Complete a backup and restore drill before issuing cards you need to support. Choose the maximum work you can afford to lose, then schedule both exports and verified independent copies within that window.
- On a separate recovery computer, create an age identity. Store the private identity in your restricted vault and protected offline copy.
- Put only its public
age1…recipient inOPS_BACKUP_RECIPIENTin a.envfile besidecompose.yaml. - Recreate the service, run an export, then copy the encrypted files to independent storage.
docker compose up -d ops
docker compose exec -T ops node dist/standalone.js backup
docker compose cp ops:/backups ./encrypted-backupsThe export command reports a unique .age filename. Move the encrypted copies from this host to your chosen independent storage and verify their hashes.
Use cron, a systemd timer or your platform's scheduler for the export and copy jobs. Alert on job failure and stale verified copies. For a 24-hour loss window, a 12-hour export and copy cadence leaves time to detect a failed run and recover.
Nextcloud, ntfy and Atoll Recovery
Nextcloud can hold encrypted copies on a separate server. ntfy can deliver alerts from your scheduler or monitoring system. Keep the private age identity separate from both the service and backup storage.
Atoll Recovery is an optional StartOS companion that copies encrypted Workers exports, reports freshness and can send alerts through ntfy. The Docker service provides filesystem exports; its R2 scheduler, pairing and independent-copy receipt HTTP adapters remain separate work. For this container, arrange your own scheduled copy and alert jobs.
Restore an encrypted export into a fresh database on a recovery host with the private age identity. Restoration disables operator passwords and pauses issuance behind a recovery fence. Recover operator access and reconcile outstanding card work before resuming. Follow the versioned restore procedure.
Updates and configuration
Keep the Compose file, issuer master and persistent volumes together. Before an update, verify a recent independent backup, read the new release notes, and change both services' image pin to that release's digest. Pull and recreate the service, then check readiness and sign-in. Starting with new empty volumes creates a different issuer.
The configuration reference covers administrator names, mint references, mounted secrets, ports and storage. Offering a mint URL in CCP is installation metadata; the holder wallet later configures the card and funds it.
Troubleshooting
- The phone cannot reach the service
- Check the HTTPS origin from the phone's browser, its trusted certificate and the proxy's upstream. Use the hostname, with port 8821 kept on host loopback.
- Initialization refuses to run
- Check whether this issuer already has secrets. Preserve its volumes and recover access through the existing configuration.
- Readiness returns 503
- Health checks the database; readiness also checks admission fences. Review setup, restored state and any configured backup fence. Restored work needs reconciliation before issuance.
- A card belongs to another service or has unresolved work
- Return to its original service and operator. Retain the attempt record and reconcile its history before reset or installation.