Repository navigation
cmd backup
Backup operations: create, list, restore, verify, prune, config, status, init-key.
nself backup <subcommand> [flags]
Backup, restore, verify, and schedule ɳSelf project data.
backup stream and backup schedule do not require MinIO credentials, even
when MinIO is enabled in staging or production: the stream contains PostgreSQL
data only. They still load the project's normal environment cascade. Operations
that access MinIO retain the enabled-service credential check.
backup create --remote, backup list --remote, backup restore-remote --from and backup config accept three kinds of destination. The kind follows from the URI.
| Kind | URI | Needs |
|---|---|---|
| rclone |
s3://, r2://, minio://, b2://, gcs://, az://, or a configured rclone remote such as s3:bucket/prefix
|
rclone on PATH |
| path |
path:///mnt/backups (absolute directory, local or mounted disk) |
nothing |
| host |
host://<server>/srv/backups (a server from .nself/control-plane.yaml, absolute directory) |
ssh, scp, a pinned host key |
rclone behaviour is unchanged.
Bare paths (backup stream and backup schedule only: --to, BACKUP_REMOTE, --heartbeat-to, NSELF_BACKUP_HEARTBEAT_REMOTE). nSelf v1.4.13 passed the value to rclone, which wrote a bare path (/mnt/backups, ./backups, backups: no : before the first /) to local disk. A value that starts with / is always a path; a relative value shaped like inline credentials (a , before a :) is never treated as one. In v1.4 mode a bare path still works: it is streamed as path:// plus the absolute directory (a relative path is resolved against the working directory, which for backup schedule is the project directory, the unit's working directory; a plain word such as mybucket with no : is a local folder too, as it was for rclone) and one notice line tells you what to write instead. With NSELF_V15=1 a bare path is refused before anything runs. The path:// rules below then apply, which differ from rclone on purpose: files are written 0600 in 0700 directories and checked by sha256 where rclone used your umask, and a symlink below the destination directory is refused (the destination directory itself may be a symlink; it is followed, as rclone did). backup check and backup drill read a heartbeat only from path://, host:// or an rclone remote, as in v1.4.13, so write the heartbeat remote as path:///<dir> if you check it.
path:// refuses .., a destination directory that is itself a symlink (use the real path), and any symlink below the directory, so a link cannot send a write outside it. A file is written to <key>.tmp, fsynced, renamed into place and re-read to compare its sha256. Directories are created 0700, files 0600.
host:// goes through the shared SSH funnel (sdk/go/remote): one ssh or scp argv builder, remote paths limited to [a-zA-Z0-9/_.-] with no .., and every unsafe name rejected before any connection. The server name must be in the inventory with a host (user@host); ssh_key_ref names an environment variable holding the key path. Host keys are pinned: ssh runs with StrictHostKeyChecking=yes against ~/.config/nself/backup_known_hosts (override with NSELF_BACKUP_KNOWN_HOSTS), and the file must hold a key for the alias nself-ci-<server>. A server with no pinned key is refused. A file is copied to <key>.tmp, its sha256 compared, then moved into place.
The pin file is refused if it holds a line nSelf cannot represent, or if it or its directory is group or world writable or owned by someone other than you or root. A directory above it that breaks the same rule prints one warning in v1.4 mode and is refused with NSELF_V15=1 (fix it with chmod go-w <dir>). That includes running with sudo while the pin directory belongs to your user. A writer that holds the pin lock for over 30 seconds makes the command stop with an error. Rules and fixes: Deploy tiers, pinned host key files.
# Back up to a mounted disk, no rclone installed
nself backup create --remote path:///mnt/backups
nself backup list --remote path:///mnt/backups
nself backup restore-remote --from path:///mnt/backups/myproject_full_20261005.dump --yes
# Back up to an inventory server
nself backup create --remote host://backup1/srv/nself-backupsnself backup config lists the three kinds and marks the one the configured remote selects. backup stream, backup schedule and the heartbeat remote still take rclone remotes only.
Stream a live backup to S3, R2, Backblaze B2, GCS, or Azure Blob. No temp files written.
pg_dump (streaming) | age (encrypt) | rclone rcat (multipart upload)
The database password reaches pg_dump (and the psql row-estimate query) through PGPASSWORD in the child's environment, never on its command line, so ps on the host does not show it. A DSN whose password cannot be split off (a password= query parameter or keyword form) is refused.
nself backup stream --to <url> [--recipient <key>] [--heartbeat-to <remote>] [--heartbeat-required] [--dry-run]| Flag | Default | Description |
|---|---|---|
--to |
BACKUP_REMOTE |
Destination: rclone remote, path:///<dir> or host://<server>/<dir> (a bare /dir works in v1.4 mode with a notice, see Destinations) |
--recipient |
NSELF_BACKUP_RECIPIENT |
Encryption recipient: age key, SSH key, or github:<user> (repeatable) |
--heartbeat-to |
NSELF_BACKUP_HEARTBEAT_REMOTE |
Remote (rclone, path:// or host://) that receives <project>/backup.json after a successful upload |
--heartbeat-required |
false | Exit non-zero when the heartbeat cannot be written. The backup itself is kept either way |
--dry-run |
false | Preview without running |
A stream backup that fails or uploads zero bytes exits non-zero and deletes the partial object from the remote, so nothing that picks the newest object can select it. If that delete fails, the error says the object is still there and the command still exits non-zero.
With a heartbeat remote set, a successful upload is followed by one small object, <project>/backup.json, on that remote. It lets freshness be checked off the box. It is written only after the backup upload succeeded; a failed backup writes nothing. A heartbeat failure is a warning unless --heartbeat-required is set. Use a separate remote or bucket for heartbeats.
The object is JSON with 2-space indent, sorted keys and a trailing newline:
| Field | Type | Value |
|---|---|---|
schema_version |
string | "1" |
kind |
string | "backup" |
project |
string | project name |
at |
string | RFC 3339 UTC time the upload finished |
result |
string |
"ok" (a failed backup writes nothing) |
backup_key |
string | object key of the backup, without the remote |
bytes |
int | size of the uploaded (encrypted) object |
encrypted |
bool | age recipients were used |
cli_version |
string | version of the writing binary |
approx_rows |
object or null |
{"<schema>.<table>": n_live_tup} from pg_stat_user_tables, read just before the dump. null when the estimate could not be read (needs psql on PATH) |
restored_rows |
null | drill only |
mismatches |
string[] | drill only; [] for a backup |
The object never holds a secret, hostname or bucket name. Resumed backups (backup resume) do not write a heartbeat.
# Stream encrypted backup to S3
nself backup stream --to s3:mybucket/backups --recipient age1abc123
# Use GitHub SSH keys for encryption
nself backup stream --to r2:mybucket/backups --recipient github:myusername
# Use env-configured destination (no flags needed)
nself backup stream
# Dry run to confirm destination
nself backup stream --to b2:mybucket --dry-run
# Write the freshness heartbeat to a separate remote after the upload
nself backup stream --to r2:mybucket/backups --recipient age1abc123 --heartbeat-to r2hb:heartbeats-
pg_dumpon PATH -
rcloneon PATH (configured with target remote) -
ageon PATH (only required when encryption recipients are specified) -
psqlon PATH (only used to readapprox_rowswhen a heartbeat remote is set; without itapprox_rowsisnull)
| Variable | Description |
|---|---|
BACKUP_REMOTE |
Default destination for backup stream and backup schedule (NSELF_BACKUP_DESTINATION is not read) |
NSELF_BACKUP_RECIPIENT |
Default age/SSH public key (space-separated for multiple) |
NSELF_BACKUP_NO_AUTO_KEY |
1 keeps the refusal when no recipient is configured instead of creating an identity (v1.5) |
NSELF_BACKUP_HEARTBEAT_REMOTE |
Default heartbeat remote (the --heartbeat-to flag wins). Environment only: nself.yaml has no config keys. |
NSELF_BACKUP_CHUNK_MB |
Multipart chunk size in MB (default: 64, handled by rclone) |
AWS_ACCESS_KEY_ID |
S3/R2/B2 access key |
AWS_SECRET_ACCESS_KEY |
S3/R2/B2 secret key |
With NSELF_V15=1, backup stream and backup create (encryption on) work on a project with no backup configuration. When no recipient is configured, nSelf creates an age identity once and encrypts to its recipient:
- Identity file:
~/.config/nself/<project>-age.key, mode0600, directory0700. The same pathnself backup init-keyuses. - It is created the first time only. An existing identity is never overwritten, and two runs starting together end with one key. A looser mode on an existing file or directory is tightened, and a symlink or a file that is not an age identity is refused (
E222). - The first run prints where the identity lives, its public recipient, and how to back it up. The secret is never printed or logged.
- A recipient from
--recipient,BACKUP_AGE_RECIPIENTSorNSELF_BACKUP_RECIPIENTalways wins and creates no file.--no-encryptkeeps its exact meaning. -
NSELF_BACKUP_NO_AUTO_KEY=1turns automatic creation off. With no recipient the command then refuses (E224in v1.5, the usual refusal in v1.4). - Without
NSELF_V15=1(v1.4 behaviour) the command refuses a missing recipient exactly as before.--dry-runnever creates the file.
NSELF_V15=1 nself backup stream --to r2:mybucket/backups
# restore finds the identity by itself
nself backup restore-remote --from r2:mybucket/backups/<object>.ageA lost identity makes every backup encrypted to it unrecoverable. The recipient (public key) cannot decrypt, the storage provider cannot, and nSelf cannot. There is no recovery path.
- Right after the first backup, copy
~/.config/nself/<project>-age.keysomewhere that is not this machine: a password manager attachment or an offline drive. - Optionally run
touch ~/.config/nself/<project>-age.key.backed-upto record that you did. The marker is an empty file you create yourself and nothing checks that the copy is real. While an auto-created identity exists without the marker,nself doctorprints an advisory line saying so, with thetouchcommand as the fix. The hint never changes the exit code. - Prove the copy once with
nself backup drill --from <remote> --identity <copy>. It restores into a throwaway container. Do not userestore-remoteorrestorefor this: they overwrite the project database.
In v1.5, backup restore, restore-remote and backup drill look for the identity in this order when --decrypt-key, --key or --identity is not given (an explicit path is used as given). A default-lookup identity must be a regular file (not a symlink) that parses as an age identity, otherwise E222 names the path: ~/.config/nself/<project>-age.key, <project>-backup-age.key, then age-key.txt. If none exists they stop with E223.
| Threat | With the auto identity | Hardened setup |
|---|---|---|
| Lost or deleted key | Backups unrecoverable | Keep an off-host copy and test a restore |
| Storage provider reads your bucket | Sees only age ciphertext | Same |
| Host compromise | The attacker holds the identity next to the recipient, so they can decrypt every backup the host can reach | Move the identity off the host and keep only the recipient (BACKUP_AGE_RECIPIENTS=age1...); production works this way |
| Backup uploaded without the key | Not possible: no recipient means a refusal, never plaintext | Same |
The auto identity trades host-compromise resistance for a first backup that works with zero setup. For production, generate the identity on your own machine (age-keygen), put only its recipient on the server, and keep the identity in your vault.
A cron script that runs pg_dump ... | aws s3 cp - s3://bucket/x or rclone copy has no encryption, no heartbeat and no restore proof. Replace it:
| Hand-rolled | nSelf |
|---|---|
pg_dump piped to aws s3 cp or rclone copy
|
nself backup stream --to <remote> (age-encrypted, no temp file) |
| a crontab line | nself backup schedule |
| "did it run?" |
--heartbeat-to <remote> and nself backup status
|
| a manual restore test | nself backup drill --from <remote> --identity <key> |
Destinations are the three kinds above; for stream and schedule use an rclone remote (S3, R2, B2, GCS, Azure) today. An advisory nself doctor hint for a project that still holds such a script is written but not yet wired (P7-SURF-11).
Restore a backup directly from a remote URL. No local disk space required.
rclone cat <from> | age --decrypt | pg_restore
nself backup restore-remote --from <url> [--key <identity-file>] [--yes]| Flag | Default | Description |
|---|---|---|
--from |
— | Source URL: rclone remote path, path://<dir>/<object> or host://<server>/<dir>/<object>
|
--key |
v1.5: ~/.config/nself/<project>-age.key, then <project>-backup-age.key, then age-key.txt (E223 when none). v1.4: ~/.config/nself/age-key.txt
|
Path to age identity file, used as given |
--yes |
false | Skip confirmation on production |
# Restore encrypted backup from S3
nself backup restore-remote --from s3:mybucket/backups/myproject_stream_20260423.sql.age \
--key ~/.config/nself/age-key.txt
# Restore unencrypted backup
nself backup restore-remote --from r2:mybucket/backups/myproject_stream_20260423.sqlResume a previously interrupted streaming backup.
Since rclone rcat uploads are not resumable at the protocol level, resume re-streams the full backup and overwrites the partial remote object at the same key.
nself backup resume <backup-id>Resume state is stored in ~/.nself/backup-state/<id>.json.
Install a systemd timer to run nself backup stream on a cron schedule.
nself backup schedule --cron "0 2 * * *" --to <url> [--recipient <key>]... [--heartbeat-to <remote>] [--env-file <path>] [--dry-run]Run it from the project directory (it must hold a .env plus an nSelf marker: nself.yaml, nself.yml or a docker-compose.yml written by nself build; a directory with only .env or only nself.yaml is refused, which is stricter than before; already installed units are untouched). The unit runs in that directory (WorkingDirectory) and calls the absolute path of the nself binary that is running now (symlinks resolved), so a different nself on PATH can never be picked up. The unit has no EnvironmentFile line unless --env-file is given; the project .env is read from the working directory as usual. The unit carries Environment=ENV=<env>: the ENV the command was started with, else ENV= in .env, else dev. The nightly run then loads the same .env cascade. Re-run backup schedule after changing ENV.
| Flag | Default | Description |
|---|---|---|
--cron |
— | Cron expression (e.g. 0 2 * * *) |
--to |
BACKUP_REMOTE |
Destination, as for backup stream; written into the unit as given |
--recipient |
— | Encryption recipient: age key, SSH key, or github:<user>. Repeatable; each one is written into the unit |
--heartbeat-to |
— | Heartbeat remote written into the unit as --heartbeat-to (see backup stream) |
--env-file |
— | Absolute path of an environment file for the unit. No default: without it the unit has no EnvironmentFile line |
--unit-dir |
/etc/systemd/system |
Systemd unit directory |
--dry-run |
false | Print unit files without writing |
# Schedule nightly encrypted backup at 02:00 UTC
nself backup schedule --cron "0 2 * * *" --to s3:mybucket/backups --recipient age1abc123
# Preview the systemd units
nself backup schedule --cron "0 2 * * *" --to r2:mybucket --dry-run
# Two recipients and a heartbeat remote, from the project directory
nself backup schedule --cron "30 2 * * *" --to r2:mybucket/backups --recipient age1abc123 --recipient age1def456 --heartbeat-to r2hb:heartbeatsAfter scheduling, check the timer with:
nself backup status
systemctl status nself-backup-stream.timerCreate a local backup (written to BACKUP_DIR, default ./backups).
The minio target (also included in all) uses the S3 API at
http://127.0.0.1:MINIO_PORT with MINIO_ROOT_USER, MINIO_ROOT_PASSWORD and
MINIO_REGION. It needs no host mc binary or configured alias. Every visible
bucket, including empty buckets, is saved under minio_<timestamp><tag>/<bucket>/
with directories mode 0700 and files mode 0600. The directory appears only after
all downloads succeed. Zero-byte keys ending in / become directories. Keys
ending in / with nonempty contents fail. Keys that cannot map faithfully to local paths (for
example ../secret, repeated slashes or backslashes) and file/directory collisions
fail the backup; incomplete downloads and objects whose size changes during the
backup are refused and removed. This target saves current
object contents, not object versions, metadata or bucket policies.
nself backup create [--type full|wal|metadata|minio|all] [--encrypt] [--tag <label>] [--dry-run]Restore from a local backup file.
nself backup restore <backup-id|latest> [--only pg,minio,metadata] [--decrypt-key <file>] [--yes]Without --decrypt-key, v1.5 searches ~/.config/nself/<project>-age.key, <project>-backup-age.key, then age-key.txt (E223 when none); v1.4 uses ~/.config/nself/age-key.txt. The decrypted dump is a unique 0600 temp file beside the backup, removed afterwards; an existing <backup>.dec is never touched.
Verify backup integrity, optionally running a restore test in a temporary container.
nself backup verify <backup-id|latest> [--restore-test] [--cleanup] [--keep]Without --restore-test, verification checks only that the file exists and is non-empty.
Use --restore-test --cleanup to restore a .dump or .dump.age backup in a test
Postgres container and run smoke and sentinel checks. Encrypted dumps use the same
default identity search as restore; verification fails if the host cannot decrypt
the backup. Temporary plaintext is removed after the check, including on failure.
Production-tier remote deploys and promotions require this restore test before applying.
List backups in the local backup directory.
nself backup list [--remote <name>] [--since 24h] [--format table|json]Remove backups beyond the retention policy.
nself backup prune [--keep-daily 7] [--keep-weekly 4] [--keep-monthly 12] [--dry-run] [--yes]Prune deletes backups for good. In v1.4 it warns when called without --yes. In v1.5 (NSELF_V15=1) it refuses with [E403] unless you pass --yes or answer y on a terminal. --dry-run only previews, so it never needs --yes.
Show backup subsystem status: last run, next scheduled run, retention policy, and, when a heartbeat remote is set, how fresh the newest off-box backup and restore drill are.
nself backup status [--format json] [--heartbeat-to <remote>] [--max-age 26h] [--max-drill-age 35d] [--project <name>]| Flag | Default | Meaning |
|---|---|---|
--heartbeat-to |
NSELF_BACKUP_HEARTBEAT_REMOTE |
Remote that holds <project>/backup.json and <project>/drill.json (rclone, path:// or host://). |
--max-age |
unset | Exit non-zero with [E217] when the newest off-box backup is older (26h, 1d12h), missing, or reports a failed backup. |
--max-drill-age |
unset | Exit non-zero with [E218] when the newest restore drill is older (35d), missing, or reports result: failed. |
--project |
the current project | Project name that selects the heartbeat objects. With --heartbeat-to no project directory is needed and only the offbox object is printed (the owner-machine jobs run from $HOME). |
The check fails closed. A missing, stale, failed, unparseable, wrong-version, wrong-project or future-dated heartbeat is never OK, and its age is always shown. A heartbeat that cannot be fetched or parsed exits with [E219] whether or not a threshold is given. A missing object (rclone exit code 3 or 4, or no such file for path:// and host://) is told apart from an unreachable or misconfigured remote by exit code, never by words in the output: a missing object is [E217]/[E218] "none found", anything else is [E219] with its cause. Every remote read is cut off after 60 seconds.
A remote is s3://, r2://, minio://, b2://, gcs://, az://, path://, host://, or a configured rclone remote written exactly name:path. Anything that could be read as an rclone option or connection string (a leading -, :backend,opts:path, file://, http(s)://) is refused before rclone starts. --max-age or --max-drill-age with no heartbeat remote also gives [E219].
Existing fields are unchanged. --format json appends the offbox object (contract:cli.backup-status v1); the text form gains two lines, Off-box backup: and Off-box drill:, only when a heartbeat remote is set.
{"offbox": {"source": "heartbeat", "last_backup": {"...": "backup.json"}, "last_drill": {"...": "drill.json"},
"backup_age_seconds": 7200, "drill_age_seconds": 1301460, "max_age_exceeded": false, "max_drill_age_exceeded": false,
"problems": []}}| Field | Meaning |
|---|---|
source |
heartbeat, or none when no heartbeat remote is set |
last_backup, last_drill
|
the heartbeat objects (contract:cli.backup-heartbeat), null when none was found |
backup_age_seconds, drill_age_seconds
|
seconds since at, null when none was found |
max_age_exceeded |
--max-age was given and the backup is stale, missing or failed |
max_drill_age_exceeded |
--max-drill-age was given and the drill is stale, missing or failed |
problems |
additive: one string per missing, stale, failed or unreadable heartbeat ("[E219] drill heartbeat is unreadable: ..."), [] when there is none. A JSON-only consumer reads the reason here; the exit status still carries the verdict |
nself backup status --project nself-web --heartbeat-to r2:nself-backup-heartbeat --max-age 26h --max-drill-age 35d --format jsonIn v1.5 --json mode a stale, failed or unreadable heartbeat (E217-E219) prints the error envelope (error.code, no data member) and exits non-zero; the off-box state goes to stderr. Only a passing check prints the data document above. backup drill --json behaves the same: a failed drill prints the error envelope, not a data document.
Restore the most recent backup into a scratch database, measure how long the restore took against an RTO target, and smoke-check the result.
nself backup drill [--file <backup>] [--rto-hours 4] [--dry-run] [--json]| Flag | Default | Meaning |
|---|---|---|
--file |
most recent backup | Backup file to drill against. |
--rto-hours |
4.0 |
RTO target in hours. 0 disables the gate. |
--dry-run |
false |
Validate inputs and exit without restoring. |
--json |
false |
Emit the drill result as JSON on stdout. |
nself backup drill --from <remote> [--identity <age key>] [--key <object>] [--heartbeat-to <remote>] [--project <name>] [--json]Restores the newest <project>_stream_* backup of a remote (rclone, path:// or host://) into a throwaway postgres:16-alpine container, so a backup is proven by a restore and never by its existence. Run it on the owner machine only, the one that holds the age identity. Production never holds a key that decrypts its backups.
| Flag | Default | Meaning |
|---|---|---|
--from |
none | Remote that holds the backups. Selects the off-box drill. |
--identity |
~/.config/nself/<project>-age.key, then <project>-backup-age.key, then age-key.txt (E223 when none; the drill is new in v1.5, so it has no v1.4 default) |
age identity file that decrypts the object. Only its path is passed to age. |
--key |
newest by name | Object to drill instead of the newest. |
--heartbeat-to |
NSELF_BACKUP_HEARTBEAT_REMOTE |
Remote that receives <project>/drill.json. Without one nothing is written. |
--project |
the current project | Project name for the object prefix and heartbeat keys. With --from no project directory is needed. |
What it does:
- Preflight: Docker reachable,
ageinstalled for.ageobjects, free disk of twice the download size. Any miss exits with[E220]and writes nodrill.json. - Downloads the object through the destination interface and decrypts it into a private (
0700) temp directory, removed on every exit. - Starts a throwaway container: random name
nself-drill-<16 hex>and labelorg.nself.drill, random password passed through the environment (never printed, never in argv), no published port and no network. It never touches the project database or containers; removal refuses any name that is not a throwaway. - Restores by content, not extension: a file that starts with
PGDMPis a custom-format archive and goes throughpg_restore --no-owner --no-acl; anything else is plain SQL and goes throughpsql. Stream objects are custom-format archives named.sql. - Counts rows per table (
count(*)) and compares them with the source counts in<project>/backup.json(approx_rows): a table the backup counted non-empty must have rows, and a table may fall short of its count by at most 10% (the counts aren_live_tupestimates). Without source counts the drill cannot verify: no--heartbeat-to, no readablebackup.json, or abackup_keythat is not the object restored (the drill warns) all fail with[E218]"cannot verify" andresult: failed. It never passes on "some table has rows". Everypg_restoreline that containserror:also fails the drill, unless it is on the (currently empty) allowlist ininternal/backup/drill_verify.go. - Interrupts: SIGINT and SIGTERM stop the drill, remove the container and the decrypted files, and write no
drill.json. After a SIGKILL or a crash, the next drill run starts by removing drill containers (labelorg.nself.drill, throwaway name only) andnself-drill-*temp directories older than 6 hours, owned by the same user. Other containers are never touched. The remote download is bounded (60 seconds plus one second per MiB of the backup). - Writes
<project>/drill.json(kind: drill,restored_rows,mismatches,result: okorfailed), then removes the container. A failed drill still writes it, and exits with[E218].
| Code | Meaning |
|---|---|
| E217 | Off-box backup stale, missing or failed (backup status --max-age) |
| E218 | Restore drill stale, missing or failed (backup status --max-drill-age), or a drill that did not match its backup |
| E219 | Heartbeat unreadable, or no heartbeat remote for a threshold |
| E220 | Drill cannot start: Docker, age or free disk (exit class 2 under NSELF_V15=1) |
--file and --dry-run do not apply with --from. Without --from the drill behaves as before, as described below.
After the restore, the drill checks that a list of critical tables exists by
name in the scratch database. The list comes from BACKUP_CRITICAL_TABLES
(comma-separated) when set, and otherwise falls back to the built-in
np_-prefixed default: np_users, np_licenses, np_audit_log,
np_plugins, np_billing.
Two things about this check are worth knowing before you rely on it:
- It tests presence only. A table that exists but holds no rows counts as present. The drill does not compare row counts against the source database.
- A missing table does not fail the drill. The verdict stays
PASSand the missing names are listed as an advisory. Treat the drill as a restore-path smoke test, not as a data-completeness gate.
Deployments whose schema does not use the np_ prefix will see every default
name reported missing until they set BACKUP_CRITICAL_TABLES.
Generate an age encryption keypair for backup encryption.
nself backup init-keyOutputs the public key to add to .env as BACKUP_AGE_RECIPIENTS.
nself backup prune [--dry-run] [--yes]
nself backup pitr restore --to <time> [--yes]backup prune and backup pitr restore destroy data. In v1.4 they warn when called without --yes. In v1.5 (NSELF_V15=1) they refuse with [E403] unless you pass --yes or answer y on a terminal. prune --dry-run only previews, so it never needs --yes. An agent or HTTP caller gets a confirmation round trip first; the CLI adds --yes once the caller confirms.
| Name | Shorthand | Type | Default | Env | Escalates to | JSON override | Description |
|---|---|---|---|---|---|---|---|
--help |
-h |
bool |
false |
Show help |
| Name | Description |
|---|---|
config |
View backup configuration |
create |
Create a new backup |
drill |
Run a DR drill: restore latest backup into scratch DB and measure RTO |
init-key |
Generate age encryption keypair for backups |
list |
List available backups |
pitr |
Point-in-time recovery: enable, disable, status, base-backup, restore |
prune |
Remove old backups by retention policy |
restore |
Restore from a backup |
restore-remote |
Restore a backup directly from a remote URL |
resume |
Resume an interrupted streaming backup |
schedule |
Schedule recurring streaming backups via systemd timers |
status |
Show backup subsystem status |
stream |
Stream an encrypted backup directly to a remote destination |
verify |
Verify backup integrity |
-
Side effect:
read -
Output:
document -
JSON:
envelope(data schema) - Exit codes: none declared
# Create a local backup
nself backup create
# List available backups
nself backup list
# Stream an encrypted backup straight to S3, no temp files
nself backup stream --to s3:mybucket/backups --recipient age1abc123
# Check backup subsystem status: last run, next scheduled, retention
nself backup status- Commands — full command index
- Core-Services — what a stack is made of
Getting Started
Commands
- Commands, Overview
- Lifecycle: cmd-init · cmd-build · cmd-start · cmd-stop · cmd-restart · cmd-dev
- Monitoring: cmd-status · cmd-logs · cmd-health · cmd-urls · cmd-doctor · cmd-monitor · cmd-alerts · cmd-sentry · cmd-watchdog
- Data: cmd-db · cmd-backup · cmd-dr · cmd-queue · cmd-webhooks
- Config: cmd-config · cmd-service · cmd-env · cmd-promote
- Networking: cmd-ssl · cmd-trust · cmd-dns-setup
- Security: cmd-access · cmd-security · cmd-secrets
- Tenancy: cmd-tenant · cmd-billing
- Plugins: cmd-plugin · cmd-license · cmd-dogfood (extracted, CLI-R11) · cmd-k8s (extracted, CLI-R11) · cmd-encryption (extracted, CLI-R11) · cmd-waf (extracted, CLI-R11) · cmd-federation (extracted, CLI-R11) · cmd-mail (extracted, CLI-R11) · cmd-dlq (extracted, CLI-R11)
- AI: cmd-ai · cmd-claw · cmd-model
- Templates: cmd-template
- Utilities: cmd-exec · cmd-clean · cmd-reset · cmd-update · cmd-upgrade · cmd-version · cmd-admin · cmd-migrate · cmd-migrate-firebase · cmd-migrate-supabase · cmd-completion
Features
- Features, Overview
- Feature-Auth
- Feature-Storage
- Feature-Search
- Feature-Functions
- Feature-Email
- Feature-Monitoring
- Feature-Plugins
- Feature-nClaw, AI Assistant
- Feature-nChat, Messaging
- Feature-nFamily, Family Social
- Feature-nCloud, Managed Hosting
- Feature-Memory-Rooms, Knowledge Organization
- Feature-Agent-Dashboard, Agent Metrics
- Feature-Image-Generation, AI Image Generation
Configuration
- Configuration, Overview
- Config-Env-Vars
- Config-Postgres
- Config-Hasura
- Config-Auth
- Config-Nginx
- Shared-Proxy
- Config-Optional-Services
- Config-Custom-Services
- Config-System
Plugins (87 + 10 monitoring)
- Plugin-Overview
- Plugin-Catalog
- Plugin-Install
- Plugin-Licensing
- Plugin-Architecture
- Plugin-Dev-Guide
- Plugin-Migrations-Contract
Free (25)
- plugin-backup
- plugin-content-acquisition
- plugin-content-progress
- plugin-cron
- plugin-donorbox
- plugin-feature-flags
- plugin-github
- plugin-github-runner
- plugin-invitations
- plugin-jobs
- plugin-link-preview
- plugin-mdns
- plugin-mlflow
- plugin-monitoring
- plugin-notifications
- plugin-notify
- plugin-paypal
- plugin-search
- plugin-shopify
- plugin-stripe
- plugin-subtitle-manager
- plugin-tokens
- plugin-torrent-manager
- plugin-vpn
- plugin-webhooks
Pro (62)
- plugin-access-controls
- plugin-activity-feed
- plugin-admin-api
- plugin-analytics
- plugin-auth
- plugin-bots
- plugin-browser
- plugin-calendar
- plugin-cdn
- plugin-chat
- plugin-claw
- plugin-claw-budget
- plugin-claw-news
- plugin-claw-web
- plugin-cloudflare
- plugin-cms
- plugin-compliance
- plugin-ddns
- plugin-devices
- plugin-documents
- plugin-entitlements
- plugin-file-processing
- plugin-game-metadata
- plugin-geocoding
- plugin-geolocation
- plugin-google
- plugin-home
- plugin-idme
- plugin-knowledge-base
- plugin-linkedin
- plugin-livekit
- plugin-media-processing
- plugin-meetings
- plugin-moderation
- plugin-mux
- plugin-object-storage
- plugin-observability
- plugin-photos
- plugin-podcast
- plugin-post
- plugin-realtime
- plugin-retro-gaming
- plugin-rom-discovery
- plugin-social
- plugin-sports
- plugin-support
- plugin-tmdb
- plugin-voice
- plugin-web3
- plugin-workflows
Planned (26)
plugin-auditplugin-blogplugin-checkoutplugin-commerceplugin-drmplugin-exportplugin-flowplugin-importplugin-ldapplugin-mailgunplugin-mediaplugin-oauth-providersplugin-pagesplugin-postmarkplugin-rate-limitplugin-reportsplugin-samlplugin-schedulerplugin-sendgridplugin-ssoplugin-subscriptionplugin-thumbplugin-transcoderplugin-twilioplugin-wafplugin-watermark
Guides
- Guide-Production-Deployment
- Guide-SSL-Setup
- Guide-Multi-Tenancy
- Guide-Security-Hardening
- Guide-Monitoring-Setup
- Guide-Backup-Restore
- Guide-Custom-Services
- Guide-Migration-from-v1
Architecture
- Architecture
- Architecture-Guardrails
- Service-Graph
- Compose-Generation
- Nginx-Generation
- Plugin-Architecture
Reference
Machine interface
-
JSON-Output,
--jsonenvelope -
Command-Registry,
nself help --json - Exit-Codes
- error-codes, Error Codes
-
Compat-V15,
NSELF_V15gate
Licensing
Security
Brand
Operations
- operations/release-cascade, Release Cascade
- operations/self-healing, Self-Healing Schema
- operations/redis-tuning, Redis Pool Tuning
- operations/meilisearch-warmup, MeiliSearch Warm-Up
- operations/jwt-rotation, JWT Key Rotation
- operations/windows-wsl2-setup, Windows / WSL2 Setup
- operations/gemini-oauth-reauth, Gemini OAuth Reauth
Contributing
Admin
- USER-ACTION-QUEUE, Pending Admin Actions
All commands (21)
- A: cmd-add · cmd-admin
- B: cmd-backup · cmd-build
- C: cmd-clean · cmd-config
- D: cmd-db · cmd-deploy · cmd-doctor
- E: cmd-exec
- I: cmd-init
- L: cmd-license · cmd-logs
- M: cmd-mcp
- R: cmd-remove · cmd-reset · cmd-restart
- S: cmd-start · cmd-status · cmd-stop
- U: cmd-update