# Walwarden Documentation — Full Content > Verifiable backup independence for Postgres. Customer-owned S3, signed manifests, auditable restore chain. # Walwarden ## What walwarden does Walwarden drives scheduled logical backups of your Postgres database into an S3 bucket that you own. It signs every artifact with an Ed25519 manifest, records every event in an append-only audit chain, and gives you a single command to restore from any backup on a machine you control. The core guarantee is independence: your backup bytes land in your S3 bucket under your IAM role. Walwarden holds the scheduling, the audit chain, and the evidence bundle. It does not hold your data. ## What walwarden ships today | Capability | Notes | |---|---| | Scheduled logical backup (`pg_dump`) to BYO S3 | Neon and Supabase-compatible Postgres | | Ed25519-signed manifests and audit chain | Verifiable offline via `@walwarden/verifier` | | Dashboard backup history and RPO-at-a-glance | Shows last backup time, manifest hash, bytes | | Operator-driven restore via `walwarden-cli` | Dashboard issues a short-lived token; CLI runs `pg_restore` on your machine; full audit chain | | Live restore progress on dashboard (SSE) | State transitions stream in real time | | Evidence bundle export | Download for auditors; verifiable offline | For the full honest-capability inventory, including what isn't shipped yet, see [honest capability claims](/docs/reference/honest-capability-claims). ## Trust boundary The restore trust boundary is the load-bearing invariant of the product. - Walwarden never holds the customer's target-DB write credentials. - Walwarden never proxies or stores the dump bytes. The CLI pulls directly from S3 via a short-lived presigned URL. - The restore audit chain records every state transition: token issued, claimed, downloading, verifying, restoring, completed or failed. For the full architecture description, see [Trust Boundary](/docs/reference/trust-boundary). ## What you need to get started - An AWS account in which you can create an S3 bucket and an IAM role - A Postgres database hosted on Neon, Supabase, or any managed provider (superuser access not required; see the specific permissions needed in [Protect your first database](/docs/getting-started/quickstart-cli)) - Node 20+ and `pg_restore` 16+ on any machine you plan to run restores from Continue to [Getting started](/docs/getting-started) to set up your first protected database. # Getting started (getting-started/index) ## The three things you need Before walwarden can protect a database, three things must be in place: 1. **An AWS account** where you control an S3 bucket and can create an IAM role. Walwarden never holds your AWS credentials. It assumes a role you create, scoped to the bucket you own. 2. **A Postgres database** that walwarden's worker can reach. The connection uses a non-superuser role with `pg_dump`-compatible read access. Neon and Supabase are the canonical targets; any managed Postgres that exposes a standard DSN works. 3. **A machine you trust for restores.** Restores run on a machine you control, not on walwarden's infrastructure. That machine needs Node 20+ and `pg_restore` matching the major version of your source database. It can be your laptop, a bastion host, or a CI runner. ## How the pieces fit together The walwarden flow has two independent phases: **Backup phase (automated)** The walwarden worker connects to your database on the schedule you configure, runs `pg_dump`, streams the bytes to your S3 bucket, computes a SHA256 checksum in flight, and writes a signed manifest. Every event is recorded in the audit chain. The dashboard shows the last backup time, the manifest hash, and the current RPO. **Restore phase (operator-initiated)** When you need to restore, you click "Restore from this backup" in the dashboard. The dashboard issues a short-lived token and renders a one-liner command. You paste that command into a terminal on the machine that can reach your target database. The CLI claims the restore job, downloads the dump from S3 via a presigned URL, verifies the manifest checksum, and pipes the bytes to `pg_restore`. Walwarden never sees your target DSN or the dump bytes. Progress streams back to the dashboard in real time. ## Pick your path Both quickstarts reach the same success state — a signed manifest and an RPO figure on the dashboard. Pick the one that matches how you work; neither is the "main" path. ## More setup detail - [Install the CLI](/docs/getting-started/install-cli) — required for restores; install before you need it - [BYO AWS S3 destination](/docs/guides/destinations/byo-aws-s3) — the S3 configuration, in detail - [Read your recovery posture](/docs/guides/operate/read-your-dashboard) — once a backup lands, a card-by-card guide to reading the dashboard # "Quickstart: CLI-first backup" (getting-started/quickstart-cli) This is the CLI-first path: you work in the dashboard and a terminal. If you drive an agent and want copy-paste agent commands instead, take the [agent-assisted quickstart](/docs/getting-started/quickstart-agent). Both paths reach the same success state — a signed manifest and an RPO figure on the dashboard. ## Prerequisites Complete these before starting: - An AWS S3 bucket and IAM role configured per [BYO AWS S3](/docs/guides/destinations/byo-aws-s3). The bucket must have Object Lock GOVERNANCE enabled. The role must trust walwarden's AWS account. - A Postgres DSN for the database you want to protect. The database user must have `SELECT` privileges on all tables you want to include in the backup. Superuser is not required. - A walwarden account. Sign in at [walwarden.com](https://walwarden.com). ## Step 1: Sign in via WorkOS Walwarden uses WorkOS for authentication. Click "Sign in" and enter your email. You will receive a magic link. Click it to complete sign-in. On first sign-in, walwarden creates a team for your account. The team is the unit of billing and access control. ## Step 2: Add a destination A destination is the S3 bucket and IAM role that walwarden writes backup artifacts to. 1. In the dashboard sidebar, click **Destinations**. 2. Click **Add destination**. 3. Enter: - **Bucket name** — the name of your S3 bucket (not the ARN) - **Region** — the AWS region of the bucket (for example, `us-east-1`) - **Role ARN** — the ARN of the IAM role walwarden will assume - **External ID** — the external ID walwarden issued when you created the destination (copy it from the dashboard, not from the IAM console) 4. Click **Save**, then run preflight verification from the destination detail page. All checks must pass before backups can run. See [AWS preflight verification](/docs/guides/destinations/preflight-verification) if any check fails. If you have not created the bucket and role yet, do that first: [BYO AWS S3](/docs/guides/destinations/byo-aws-s3). ## Step 3: Add a protected database 1. In the sidebar, click **Protected databases**. 2. Click **Add database**. 3. Enter: - **Name** — a label for your reference (for example, `production-app-db`) - **DSN** — the connection string for the source database, including the password. The connection is used only by the backup worker; it is stored encrypted and never returned to the browser. - **Destination** — select the destination you created in step 2 4. Click **Save**. Walwarden validates that it can reach the database and that the backup role has the necessary permissions. ## Step 4: Set the backup schedule After creating the protected database, the detail page shows a **Schedule** section. The default is no schedule; the database does not back up until you configure one. Enter a cron expression for how often you want backups. Common choices: | Schedule | Cron | |---|---| | Every hour | `0 * * * *` | | Every 6 hours | `0 */6 * * *` | | Daily at 02:00 UTC | `0 2 * * *` | Click **Save schedule**. The first backup will run at the next scheduled tick. You can also trigger an immediate backup via [Ad-hoc backup](/docs/guides/backup/ad-hoc). ## Step 5: Confirm the first backup The dashboard shows a **Backup history** table on the database detail page. After the first scheduled (or ad-hoc) backup completes, the row records the evidence the backup produced: ``` state: completed completed_at: 2026-05-26T14:30:12Z artifact: 148.9 MB manifest: sha256:8f3c…a1d7 (Ed25519-signed) rpo: 12 minutes ``` Each completed backup writes a SHA256 manifest, signs it with Ed25519, and appends the event to the audit chain. The dashboard turns the most recent completed backup into an RPO figure. The dashboard shows a signed manifest hash and an RPO figure for this database. That is the proof your backup ran and landed in your own S3 bucket — not a guarantee of recoverability on its own. See [Recoverability and RPO](/docs/concepts/recoverability-and-rpo) for why backup-complete is not yet proven-recoverable. ## Next: prove you can restore A backup you have never restored from is a backup you have not yet proven recoverable. Run a restore drill now against a non-production target: [Run a restore drill](/docs/guides/restore/run-a-restore-drill) # "Quickstart: agent-assisted backup" (getting-started/quickstart-agent) This is the agent-assisted path: copy-paste commands a coding agent can run on your behalf to take a first backup and confirm the evidence it produced. If you would rather drive the dashboard yourself, take the [CLI-first quickstart](/docs/getting-started/quickstart-cli). Both paths reach the same success state — a signed manifest and an RPO figure on the dashboard. ## Prerequisites - A configured, preflight-verified destination — see [BYO AWS S3](/docs/guides/destinations/byo-aws-s3). - A connected protected database — see [Connect Supabase](/docs/guides/connect/supabase) or [Connect Neon](/docs/guides/connect/neon). - An API key scoped for `databases:read`, `destinations:read`, `backups:trigger`, and `evidence:read`. Set it as `WALWARDEN_API_KEY` and the database id as `WALWARDEN_DATABASE_ID`. ## Step 1: The safe CLI pattern Give your agent this exact sequence. Every command is read-only or an explicit backup trigger; `--json` makes the output machine-readable. ```bash walwarden --json profile validate walwarden --json database list walwarden --json destination list walwarden --json backup trigger --database "$WALWARDEN_DATABASE_ID" --wait walwarden --json evidence list --database "$WALWARDEN_DATABASE_ID" ``` The `backup trigger --wait` call returns when the job reaches a terminal state. The evidence the backup produced is what the agent must check next — not the trigger's exit code alone. ## Step 2: Command beside the evidence it produces The trigger command on the left produces the signed evidence on the right. The agent confirms success by reading the evidence, not by assuming a completed job is recoverable. ```bash # command walwarden --json evidence list \ --database "$WALWARDEN_DATABASE_ID" ``` ```json // signed output { "artifact": "backup-2026-05-26T14:30:12Z", "state": "completed", "manifest": { "sha256": "8f3c…a1d7", "signature": "ed25519:…", "signed": true }, "rpoMinutes": 12 } ``` ## Step 3: Or use the SDK If your agent works through the TypeScript SDK instead of the CLI, the same flow is a few calls: ```ts import { createWalwardenClient } from '@walwarden/sdk'; const walwarden = createWalwardenClient({ baseUrl: process.env.WALWARDEN_BASE_URL!, apiKey: process.env.WALWARDEN_API_KEY!, userAgent: 'my-agent/1.0', }); const { databases } = await walwarden.listDatabases(); // trigger a backup, then read the evidence the backup produced // before reporting success. ``` See the [SDK reference](/docs/reference/sdk) for the full method list and the [agent integration recipes](/docs/reference/agent-integration) for scopes. ## The evidence-before-success rule An agent must not mark a backup, migration, or restore drill successful until it has checked the relevant command result **and** the evidence metadata. Backup completion alone proves only that a job completed; it is not the same as proven recoverability. ## Forbidden claims Carry these constraints into anything the agent reports. (Source of truth: [agent integration recipes](/docs/reference/agent-integration).) - Do not claim the alpha public CLI can wait for or complete restore jobs through `restore create`; use explicit `restore execute` for local execution. - Do not claim destination write, verify, attach, detach, or delete commands exist in the public CLI. - Do not claim login or whoami commands exist. - Do not claim PITR support. - Do not claim offline evidence bundle verification through the public CLI/SDK alpha surface. - Do not claim recoverability unless restore or evidence semantics support that exact claim. The dashboard shows a signed manifest hash and an RPO figure for this database, and the agent has read the evidence metadata confirming it. That is the proof — not a guarantee of recoverability on its own. See [Recoverability and RPO](/docs/concepts/recoverability-and-rpo). ## Next: prove you can restore A backup you have never restored from is a backup you have not yet proven recoverable. [Run a restore drill](/docs/guides/restore/run-a-restore-drill) against a non-production target. # Install the CLI (getting-started/install-cli) The walwarden CLI (`walwarden-cli` on npm, bin: `walwarden`) is the tool that performs restores on your machine. It pulls bytes from S3, verifies the manifest checksum, and pipes the dump to `pg_restore`. Install it before you need it. ## System requirements - **Node.js 20 or later.** Check with `node --version`. - **`pg_restore` on your PATH, matching the major version of your source database.** Check with `pg_restore --version`. A version mismatch is the most common restore failure; see [Troubleshooting](/docs/guides/restore/troubleshooting). ## One-time install (global) ```bash npm install -g walwarden-cli ``` Verify: ```bash walwarden --version ``` ## Run without installing (npx) The dashboard's one-liner uses `npx` so you do not need a prior install: ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target '' \ --mode new_database ``` `npx --yes` downloads the latest published version on first run and caches it. If you prefer a pinned version, install globally with a version specifier: ```bash npm install -g walwarden-cli@0.0.1 ``` ## Matching pg_restore to your source database `pg_restore` must be the same major version as the Postgres server that produced the dump. Mismatched versions produce the error: ``` pg_restore: error: aborting because of server version mismatch ``` How to get the right version: **macOS** — use Homebrew: ```bash brew install postgresql@16 # Then add /opt/homebrew/opt/postgresql@16/bin to PATH ``` **Debian / Ubuntu**: ```bash apt-get install postgresql-client-16 ``` **Other Linux** — use the official PGDG apt or yum repositories at [postgresql.org/download](https://www.postgresql.org/download/). To check which version the source database is running, look at the "Server version" line on the walwarden dashboard database detail page, or run: ```sql SELECT version(); ``` against the source database directly. ## AWS credentials for restore The CLI does not need `AWS_*` environment variables. The presigned S3 URL embedded in the restore token gives the CLI direct, time-limited read access to your specific backup artifact. No AWS configuration is required on the restore machine. # Guides (guides/index) These guides are organized by the task you arrive with. Each one states its goal up front, lists prerequisites, walks the steps, and closes with how to verify it worked. If you are setting up for the first time, start with a [quickstart](/docs/getting-started) instead. ## Connect a database - [Connect Supabase](/docs/guides/connect/supabase) — register a Supabase Postgres database for backup - [Connect Neon](/docs/guides/connect/neon) — register a Neon Postgres database for backup ## Configure a destination - [BYO AWS S3](/docs/guides/destinations/byo-aws-s3) — the strongest trust-boundary path - [Destinations overview](/docs/guides/destinations) — MinIO, GCS S3-compat, Wasabi, Backblaze B2, and how they compare - [Preflight verification](/docs/guides/destinations/preflight-verification) — confirm a destination before backups run ## Take backups - [Scheduled backups](/docs/guides/backup/scheduled) — cron-driven logical backups - [Ad-hoc backup](/docs/guides/backup/ad-hoc) — trigger a backup on demand ## Restore and prove recoverability - [Run a restore drill](/docs/guides/restore/run-a-restore-drill) — restore to a target you control and confirm it lands - [Restore modes](/docs/guides/restore/modes) — `new_database` vs `in_place` - [Troubleshooting](/docs/guides/restore/troubleshooting) — common failures and remedies ## Operate day-to-day - [Read your recovery posture](/docs/guides/operate/read-your-dashboard) — a card-by-card guide to the dashboard: RPO thresholds, jobs-log verification states, and what each recovery action does ## Get alerted - [Configure notification routes](/docs/guides/notifications/configure-routes) — wire failed backups, restore drills, audit-chain anomalies, and a stopped worker to Slack, Discord, email, or a signed webhook ## Produce evidence - [Produce an evidence bundle](/docs/guides/evidence/produce-an-evidence-bundle) — export a verifiable bundle for an auditor ## Manage your team & plan - [Invite teammates & roles](/docs/guides/team/invite-teammates) — add teammates and assign Admin or Member roles (Team plan) - [Plans & billing](/docs/guides/team/plans-and-billing) — what Free, Pro, and Team unlock, and how upgrades work - [Share a temporary full-plan grant](/docs/guides/team/temporary-full-plan-grant) — issue, renew, and revoke time-boxed full-plan invite links for design partners - [Issue a dashboard API token](/docs/reference/issue-api-tokens) — mint scoped bearer tokens for automation (Team plan, Admin-only) # Connect Supabase (guides/connect/supabase) This guide: connect a Supabase Postgres database so walwarden can run scheduled logical backups into a destination you own. Supabase is a shipping provider. ## Prerequisites - A configured, preflight-verified destination — see [BYO AWS S3](/docs/guides/destinations/byo-aws-s3) or the [destinations overview](/docs/guides/destinations). - A Supabase project with a Postgres database. - A database role with `SELECT` on the tables you want to back up. Superuser is not required. ## Step 1: Get the connection string In the Supabase dashboard, open **Project Settings → Database** and copy the connection string (the direct connection URI, not a pooled `pgbouncer` URI — `pg_dump` needs a direct session). It looks like: ``` postgresql://postgres:@db..supabase.co:5432/postgres ``` ## Step 2: Add the protected database 1. In the walwarden dashboard, click **Protected databases → Add database**. 2. Enter: - **Name** — a label for your reference (for example, `supabase-prod`) - **DSN** — the connection string from step 1, including the password. It is stored encrypted and never returned to the browser. - **Destination** — select your verified destination. 3. Click **Save**. Walwarden validates that it can reach the database and that the role has the permissions `pg_dump` needs. ## Step 3: Set a schedule On the database detail page, set a cron schedule (see [Scheduled backups](/docs/guides/backup/scheduled)). The first backup runs at the next tick; you can also [trigger one ad-hoc](/docs/guides/backup/ad-hoc). ## Verify it worked The **Backup history** table shows a `completed` row with a signed manifest hash, and the dashboard shows an RPO figure for this database. That confirms the backup ran and landed in your bucket. To prove recoverability, [run a restore drill](/docs/guides/restore/run-a-restore-drill). # Connect Neon (guides/connect/neon) This guide: connect a Neon Postgres database so walwarden can run scheduled logical backups into a destination you own. Neon is a shipping provider. ## Prerequisites - A configured, preflight-verified destination — see [BYO AWS S3](/docs/guides/destinations/byo-aws-s3) or the [destinations overview](/docs/guides/destinations). - A Neon project with a Postgres database and a branch you want to protect. - A database role with `SELECT` on the tables you want to back up. Superuser is not required. ## Step 1: Get the connection string In the Neon console, open your project, select the branch and database, and copy the connection string. Use the **direct** (unpooled) connection string so `pg_dump` gets a direct session: ``` postgresql://:@.neon.tech/?sslmode=require ``` ## Step 2: Add the protected database 1. In the walwarden dashboard, click **Protected databases → Add database**. 2. Enter: - **Name** — a label for your reference (for example, `neon-main`) - **DSN** — the direct connection string from step 1, including the password. It is stored encrypted and never returned to the browser. - **Destination** — select your verified destination. 3. Click **Save**. Walwarden validates that it can reach the database and that the role has the permissions `pg_dump` needs. ## Step 3: Set a schedule On the database detail page, set a cron schedule (see [Scheduled backups](/docs/guides/backup/scheduled)). The first backup runs at the next tick; you can also [trigger one ad-hoc](/docs/guides/backup/ad-hoc). ## Verify it worked The **Backup history** table shows a `completed` row with a signed manifest hash, and the dashboard shows an RPO figure for this database. That confirms the backup ran and landed in your bucket. To prove recoverability, [run a restore drill](/docs/guides/restore/run-a-restore-drill). On the first restore to a Neon target, use `new_database` mode; for repeat restores to the same target, use `--created-database` or `in_place` mode. See [Restore modes](/docs/guides/restore/modes) for the Neon specifics. # Backup destinations overview (guides/destinations/index) A backup destination is the bucket walwarden writes encrypted backup artifacts to. You bring the bucket — walwarden brings the backup engine. Walwarden never stores your source database credentials in the same place as your backup storage credentials; the two are separated at the architectural level. Walwarden requires the following of every destination: - **BYO bucket.** The bucket is in your account. Walwarden cannot create or delete it. - **Immutability.** The bucket must enforce some form of write-once / retention lock. Backups that an attacker (or a compromised walwarden account) can delete are not independent backups. The mechanism differs by provider; see the per-provider guides for the exact requirement. - **TLS-only access.** All data in transit must be encrypted. Walwarden refuses to write to an endpoint that accepts plain HTTP. - **Audit integrity.** Walwarden records a hash-chained audit log of every backup event. If the destination is later used as evidence in an incident review, the audit chain must be intact. --- ## Provider comparison | Provider | Auth model | Immutability mechanism | KMS support | Trust boundary | Configuration steps | |---|---|---|---|---|---| | [AWS BYO S3](/docs/guides/destinations/byo-aws-s3) | IAM AssumeRole (short-lived session credentials) | S3 Object Lock (GOVERNANCE mode) | SSE-KMS with customer-managed key | Strongest — walwarden never holds a long-lived write credential | 6 | | [MinIO](/docs/guides/destinations/minio) | Static access key (HMAC) | Object Lock (S3-compat API) | SSE-S3 only via S3-compat | Lower — walwarden holds your access key; rotate periodically | 4 | | [GCS S3-compat](/docs/guides/destinations/gcs-s3-compat) | HMAC key (not service account) | Bucket Lock (bucket-wide retention; not per-object) | Cloud KMS not exposed via S3-compat path | Lower — walwarden holds your HMAC key; rotate periodically | 5 | | [Wasabi](/docs/guides/destinations/wasabi) | Static access key (HMAC) | Object Lock (S3-compat API) | SSE-S3 only via S3-compat | Lower — walwarden holds your access key; rotate periodically | 4 | | [Backblaze B2](/docs/guides/destinations/backblaze-b2) | Application key (not master key) | File Lock (S3-compat API subset) | SSE-B2 (Backblaze-managed) | Lower — walwarden holds your application key; rotate periodically | 4 | ### Trust boundary note AWS BYO S3 is the trust-boundary-strongest path because walwarden uses IAM AssumeRole with a customer-owned role. The credentials walwarden uses are short-lived session tokens that expire; there are no long-lived keys for an attacker to steal from walwarden's side. All other providers use static keys that walwarden stores. This is a real trust-boundary regression: if walwarden's key store were compromised, an attacker would have a key that could write to your backup bucket until you rotate it. This is a known and accepted trade-off for customers who cannot use AWS. Walwarden documents this honestly rather than pretending static-key providers are equivalent. ### Immutability semantics **S3 Object Lock (AWS, MinIO, Wasabi):** Retention is enforced per object. Each object carries a retention timestamp; deleting or overwriting before that timestamp requires bypassing the lock. Object Lock must be enabled at bucket creation — you cannot enable it on an existing bucket. **Bucket Lock (GCS via S3-compat):** Retention is enforced at the bucket level. All objects in the bucket inherit the same retention period. GCS's S3-compat API does not expose per-object Object Lock; Bucket Lock is the equivalent. Once set, the retention period can only be increased, never decreased. **File Lock (Backblaze B2):** Backblaze B2 exposes an S3-compat subset of Object Lock for File Lock. The mechanism is similar to S3 Object Lock but is Backblaze-specific. File Lock must also be enabled at bucket creation. --- ## Preflight verification After you register a destination, run preflight to verify walwarden can reach the bucket and that the required immutability settings are in place. All checks must pass before walwarden schedules backups to that destination. All destinations start in `unverified` state after creation. Run preflight from the dashboard after saving the destination. AWS BYO S3 verifies the AssumeRole path and S3 controls. Non-AWS providers verify static-key authentication, bucket access, and the provider-specific retention checks walwarden can observe. See [Preflight verification](/docs/guides/destinations/preflight-verification) for AWS-specific check details and remediation guidance. Non-AWS provider pages document the provider-specific checks and manual verification boundaries. # BYO AWS S3 destination (guides/destinations/byo-aws-s3) Walwarden writes backups to an S3 bucket you own. This page covers creating and configuring that bucket and the IAM role walwarden uses to access it. Walwarden's AWS account ID is `194343789105`. The IAM role trust policy you create in step 4 must list this account as the trusted principal. --- ## Step 1: Create the S3 bucket 1. In the AWS console, go to S3 and click **Create bucket**. 2. Choose a name. The name must be globally unique. A pattern like `my-company-walwarden-backups` works. 3. Choose the region closest to your database. Keep a note of the region — you will enter it in walwarden later. 4. Leave all defaults in place. Do not enable public access. Do not enable static website hosting. 5. Click **Create bucket**. --- ## Step 2: Configure the bucket These settings must all be applied after creation. ### Enable Object Lock (GOVERNANCE mode) Object Lock prevents walwarden from deleting your backups, even if walwarden's account is compromised. Use GOVERNANCE mode, not COMPLIANCE. COMPLIANCE prevents even the root account from deleting objects before the retention period, which creates an operational risk if you need to recover storage. GOVERNANCE mode is enforced unless a user with specific IAM permissions overrides it. 1. On the bucket detail page, click **Properties**. 2. Scroll to **Object Lock** and click **Edit**. 3. Enable Object Lock. 4. Set the default retention mode to **Governance**. 5. Set the default retention period. A 30-day minimum is recommended. Object Lock requires versioning. AWS enables versioning automatically when you enable Object Lock. ### Block all public access On the **Permissions** tab: 1. Click **Block public access (bucket settings)**. 2. Enable all four checkboxes. 3. Click **Save**. ### Enforce TLS-only access (bucket policy) Attach the following bucket policy to reject any unencrypted requests. Replace `BUCKET-NAME` with your bucket name. ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "DenyNonTLS", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": [ "arn:aws:s3:::BUCKET-NAME", "arn:aws:s3:::BUCKET-NAME/*" ], "Condition": { "Bool": { "aws:SecureTransport": "false" } } } ] } ``` ### Default encryption 1. On the **Properties** tab, scroll to **Default encryption**. 2. Select **SSE-S3** (AES-256, AWS-managed keys). This is the minimum. 3. If your compliance requirements require customer-managed keys, select **SSE-KMS** and specify your KMS key ARN. --- ## Step 3: Create the IAM policy Create an IAM policy that grants walwarden the permissions it needs to write and read backup artifacts. This policy will be attached to the IAM role in step 4. Walwarden uses this same destination role for backups, preflight, and restore URL signing. Do not create a write-only role for normal destinations: restore needs `s3:GetObject` on walwarden backup artifacts, and preflight will fail if the role cannot read back the probe object it just wrote. Least privilege comes from using a dedicated backup bucket, requiring the walwarden external ID in the trust policy, and keeping Object Lock/TLS/public-access controls enabled. In the AWS console, go to **IAM > Policies > Create policy**. Use the JSON editor and paste the policy below. Replace `BUCKET-NAME` with your bucket name. ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "WalwardenBackupRW", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:HeadObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:PutObjectRetention", "s3:PutObjectLegalHold", "s3:GetObjectRetention", "s3:GetObjectLegalHold", "s3:GetObjectVersion", "s3:ListBucketVersions", "s3:GetBucketObjectLockConfiguration", "s3:GetEncryptionConfiguration", "s3:GetBucketPublicAccessBlock", "s3:GetBucketPolicy", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts" ], "Resource": [ "arn:aws:s3:::BUCKET-NAME", "arn:aws:s3:::BUCKET-NAME/*" ] } ] } ``` The `PutObjectRetention` and `PutObjectLegalHold` actions are required for Object Lock. Omitting them is the most common cause of preflight failure after initial configuration. The `Get*` configuration actions (`GetBucketObjectLockConfiguration`, `GetEncryptionConfiguration`, `GetBucketPublicAccessBlock`, `GetBucketPolicy`) let preflight read back the bucket settings it verifies. The multipart actions (`AbortMultipartUpload`, `ListBucketMultipartUploads`, `ListMultipartUploadParts`) are required for large-object uploads and cleanup. This matches the policy the [Terraform module](https://github.com/noncelogic/terraform-aws-walwarden) (`git::https://github.com/noncelogic/terraform-aws-walwarden.git//?ref=v1.0.0`) provisions, so both flows pass the same preflight checks. Name the policy something identifiable, for example `walwarden-backup-policy`. --- ## Step 4: Create the IAM role and trust policy 1. In **IAM > Roles**, click **Create role**. 2. Select **Another AWS account** as the trusted entity type. 3. Enter `194343789105` as the Account ID. 4. Check **Require external ID** and paste the external ID that walwarden generated for your destination. You can find this on the destination detail page in the walwarden dashboard. **Do not use a placeholder.** Using a placeholder external ID is a security misconfiguration and will be rejected by preflight. 5. Attach the policy you created in step 3. 6. Name the role, for example `walwarden-backup-role`. 7. Click **Create role** and copy the Role ARN (format: `arn:aws:iam::ACCOUNT-ID:role/walwarden-backup-role`). The trust policy walwarden requires looks like this (AWS populates this automatically when you use the console flow above): ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::194343789105:root" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "YOUR-WALWARDEN-EXTERNAL-ID" } } } ] } ``` Replace `YOUR-WALWARDEN-EXTERNAL-ID` with the exact value from the walwarden dashboard destination page. The value is case-sensitive. --- ## Step 5: Register the destination in walwarden 1. In the walwarden dashboard, go to **Destinations > Add destination**. 2. Fill in: - **Bucket name** — the bucket name (not the ARN) - **Region** — the AWS region (for example, `us-east-1`) - **Role ARN** — the ARN of the role you created in step 4 - **External ID** — the same external ID you used in the trust policy 3. Click **Save**. --- ## Step 6: Run preflight verification Run preflight from the destination detail page after you save the destination. Preflight attempts every required operation against the bucket: write a probe object, read it back with `s3:GetObject`, verify metadata, verify Object Lock is active. All checks must pass before backups can run. Preflight typically completes within 30 seconds. If it fails, see [Preflight verification](/docs/guides/destinations/preflight-verification) for per-check guidance. --- ## Common footguns **Forgot `PutObjectRetention` in the IAM policy** Preflight fails with: `User is not authorized to perform: s3:PutObjectRetention`. Re-attach the policy from step 3 and confirm the action is listed. **Used the placeholder external ID** If you pasted a placeholder value (such as `your-external-id-here`) into the IAM trust policy instead of the walwarden-issued value, preflight fails with an `Access denied` error on `sts:AssumeRole`. Delete the role and re-create it with the correct external ID from the walwarden dashboard. **Object Lock set to COMPLIANCE instead of GOVERNANCE** COMPLIANCE mode locks objects even against root-account deletion. This is not required by walwarden and creates operational risk. If you created the bucket with COMPLIANCE mode, you cannot change it; create a new bucket with GOVERNANCE mode. **Public access not fully blocked** Preflight checks that the bucket has all four public-access-block settings enabled. Partial blocking is rejected. # MinIO destination (guides/destinations/minio) Walwarden supports MinIO as a backup destination via the S3-compatible API. This page covers creating and configuring the bucket and the access key walwarden uses to write backups. Walwarden supports the following parameters for this provider: | Parameter | Description | |---|---| | `endpointUrl` | The HTTPS URL of your MinIO instance (for example, `https://minio.your-domain.com`). No trailing slash. | | `bucketName` | The name of the bucket (for example, `walwarden-backups`). | | `region` | The region configured in your MinIO cluster. MinIO accepts any non-empty string; use `us-east-1` if you have not configured a specific region. | | `accessKeyId` | The access key ID of the MinIO user walwarden authenticates as. | | `secretAccessKey` | The corresponding secret access key. | | `retentionFloorDays` | Minimum number of days each backup is retained under Object Lock. Must be between 1 and 365. | **Trust boundary note.** Walwarden stores the access key for this destination. Unlike the AWS BYO S3 path (which uses short-lived AssumeRole session credentials), this key is long-lived. Use a minimum-scope key (see step 3 below) and rotate it periodically. Revoke the key immediately if walwarden is ever compromised. **Preflight status.** MinIO destinations start in `unverified` state. Run preflight after registration so walwarden can verify the access key, bucket access, Object Lock, versioning, public-access posture, encryption behavior, and retention writes. --- ## Step 1: Create the bucket with Object Lock enabled Object Lock in MinIO must be enabled at bucket creation. You cannot enable it on an existing bucket. Using the MinIO client (`mc`): ```bash mc mb --with-lock minio-alias/walwarden-backups ``` Using the MinIO console: 1. Go to **Buckets** and click **Create bucket**. 2. Enter a bucket name. 3. Toggle **Object Locking** on. 4. Click **Create bucket**. Set the default retention mode to GOVERNANCE with a period that matches or exceeds your `retentionFloorDays` setting in walwarden: ```bash mc retention set --default GOVERNANCE 30d minio-alias/walwarden-backups ``` --- ## Step 2: Block public access MinIO buckets are private by default. Confirm no public access policy has been applied to the bucket. Walwarden's preflight verifies the public-access posture it can observe through the S3-compatible API. --- ## Step 3: Create a minimum-scope user and access key Create a user and policy that grants walwarden only the permissions it needs. Walwarden requires `PutObject`, `GetObject`, `HeadObject`, `DeleteObject`, `ListBucket`, `GetBucketLocation`, `GetObjectRetention`, and `PutObjectRetention` on the specific bucket. Create a policy file: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:HeadObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation", "s3:GetObjectRetention", "s3:PutObjectRetention", "s3:GetObjectLegalHold", "s3:PutObjectLegalHold", "s3:GetBucketVersioning", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload" ], "Resource": [ "arn:aws:s3:::walwarden-backups", "arn:aws:s3:::walwarden-backups/*" ] } ] } ``` Apply the policy and create the user: ```bash mc admin policy create minio-alias walwarden-policy /path/to/policy.json mc admin user create minio-alias walwarden-user mc admin policy attach minio-alias walwarden-policy --user walwarden-user ``` Generate an access key for the user and note the access key ID and secret access key. --- ## Step 4: Register the destination in walwarden 1. In the walwarden dashboard, go to **Destinations > Add destination**. 2. Select **MinIO (S3-compatible)** as the provider. 3. Fill in: - **Endpoint URL** — the HTTPS URL of your MinIO instance (for example, `https://minio.your-domain.com`). - **Bucket name** — the bucket name from step 1. - **Region** — `us-east-1` or whatever region string your MinIO cluster uses. - **Access key ID** — from step 3. - **Secret access key** — from step 3. 4. Click **Save**. The destination will appear in state `unverified`. Run preflight from the destination detail page before attaching it to scheduled backups. --- ## Common footguns **Object Lock not enabled at bucket creation** MinIO does not allow enabling Object Lock on an existing bucket. If you skipped the `--with-lock` flag, you must delete the bucket and re-create it with Object Lock enabled. There is no in-place migration path. **HTTP endpoint instead of HTTPS** Walwarden refuses to connect to endpoints that accept plain HTTP. Make sure the endpoint URL you paste starts with `https://`. If your MinIO instance only serves HTTP, you must terminate TLS in front of it (nginx, Caddy, or a load balancer) before walwarden can use it. **Forgot `PutObjectRetention` in the policy** Without `PutObjectRetention`, walwarden can write objects but cannot stamp the Object Lock retention hold on them. Preflight catches this. Add the action to the policy and re-apply it. **Access key attached to the root user or admin user** Walwarden only needs write access to one bucket. Attaching a root or admin key gives walwarden far more than it needs and amplifies blast radius if the key is compromised. Always use a purpose-built user with the policy from step 3. **MinIO version too old to support Object Lock via S3-compat** MinIO's S3-compatible Object Lock support requires MinIO server version RELEASE.2021-01-30 or later. Earlier versions may accept the bucket creation flag without actually enforcing locks. Upgrade MinIO before using walwarden against it. # GCS S3-compatible destination (guides/destinations/gcs-s3-compat) Walwarden supports Google Cloud Storage (GCS) as a backup destination via the S3-compatible endpoint at `https://storage.googleapis.com`. This page covers creating and configuring the bucket, enabling Bucket Lock, and creating the HMAC key walwarden uses to authenticate. Walwarden supports the following parameters for this provider: | Parameter | Description | |---|---| | `endpointUrl` | Always `https://storage.googleapis.com` for GCS S3-compat. | | `bucketName` | The name of the GCS bucket (for example, `my-company-walwarden-backups`). | | `region` | The GCS region (for example, `us-east1`). Use GCS region names, not AWS region names. | | `accessKeyId` | The HMAC access key ID (from the GCS HMAC keys page in the Cloud Console). | | `secretAccessKey` | The corresponding HMAC secret. | | `retentionFloorDays` | Minimum number of days each backup is retained under Bucket Lock. Must be between 1 and 365. | **Trust boundary note.** Walwarden stores the HMAC key for this destination. Unlike the AWS BYO S3 path (which uses short-lived AssumeRole session credentials), HMAC keys are long-lived. Use a minimum-scope service account (see step 4 below) and rotate the HMAC key periodically. Revoke the key immediately if walwarden is ever compromised. **Preflight status.** GCS S3-compatible destinations start in `unverified` state. Run preflight after registration so walwarden can verify the HMAC key, bucket access, object write/read/head behavior, and public-access posture it can observe through the S3-compatible API. --- ## Bucket Lock vs. Object Lock — important semantic difference GCS Bucket Lock is **bucket-wide** retention. When you enable Bucket Lock with a 30-day retention period, every object in the bucket is retained for at least 30 days from when it was written — regardless of what walwarden sets at the object level. You cannot set a shorter retention on individual objects. AWS S3 Object Lock (and MinIO's Object Lock equivalent) is **per-object** retention. Each object carries its own retention timestamp. Walwarden stamps each backup artifact with its own retention hold. Both mechanisms prevent deletion before the retention period expires. The practical difference: GCS Bucket Lock applies uniformly to all objects in the bucket; AWS Object Lock can vary per object if needed. For walwarden's use case both are equivalent — walwarden sets the same retention floor on every backup artifact. --- ## Step 1: Create the GCS bucket 1. In the GCS Cloud Console, go to **Cloud Storage > Buckets** and click **Create**. 2. Enter a bucket name. A pattern like `my-company-walwarden-backups` works. 3. Choose the region closest to your database. Use a single-region bucket rather than multi-region for predictable latency and cost. 4. Under **Choose how to control access to objects**, select **Uniform** (not Fine-grained). Bucket Lock requires uniform access control. 5. Click **Create**. Do not choose a multi-regional or dual-regional bucket unless you have a specific redundancy requirement — the extra replication increases cost without benefit for walwarden's access pattern. --- ## Step 2: Enable Bucket Lock Bucket Lock must be enabled on the bucket before any objects are written. If you enable it after writing objects, the retention applies only to objects written after the lock is set. ```bash gcloud storage buckets update gs://my-company-walwarden-backups \ --retention-period=30d \ --lock-retention-policy ``` The `--lock-retention-policy` flag locks the policy so it cannot be removed and can only have its period extended, never decreased. This is the correct configuration for walwarden. Verify the lock is in place: ```bash gcloud storage buckets describe gs://my-company-walwarden-backups \ --format="value(retentionPolicy)" ``` The output should show `isLocked: true` and a `retentionPeriod` matching your setting. --- ## Step 3: Create a service account with minimum scope Create a dedicated GCS service account for walwarden. Minimum required GCS roles: `storage.objectCreator` and `storage.objectViewer` on the specific bucket. ```bash gcloud iam service-accounts create walwarden-backup-sa \ --display-name="Walwarden backup service account" gcloud storage buckets add-iam-policy-binding gs://my-company-walwarden-backups \ --member="serviceAccount:walwarden-backup-sa@PROJECT.iam.gserviceaccount.com" \ --role="roles/storage.objectCreator" gcloud storage buckets add-iam-policy-binding gs://my-company-walwarden-backups \ --member="serviceAccount:walwarden-backup-sa@PROJECT.iam.gserviceaccount.com" \ --role="roles/storage.objectViewer" ``` Do not grant `roles/storage.admin`, `roles/storage.objectAdmin`, or any bucket-delete permission. Walwarden does not need those and they amplify blast radius. --- ## Step 4: Create an HMAC key HMAC keys (not service account JSON keys) are required for GCS S3-compatible access. Create an HMAC key for the service account: ```bash gcloud storage hmac create walwarden-backup-sa@PROJECT.iam.gserviceaccount.com ``` The command outputs an `accessId` and a `secret`. Record both — the secret is shown only once. Alternatively, in the Cloud Console go to **Cloud Storage > Settings > Interoperability** and create an HMAC key for your service account. --- ## Step 5: Register the destination in walwarden 1. In the walwarden dashboard, go to **Destinations > Add destination**. 2. Select **Google Cloud Storage (S3-compatible endpoint)** as the provider. 3. Fill in: - **Endpoint URL** — `https://storage.googleapis.com` (exact, no trailing slash). - **Bucket name** — the bucket name from step 1. - **Region** — the GCS region (for example, `us-east1`). - **Access key ID** — the HMAC `accessId` from step 4. - **Secret access key** — the HMAC secret from step 4. 4. Click **Save**. The destination will appear in state `unverified`. Run preflight from the destination detail page before attaching it to scheduled backups. --- ## What walwarden can and cannot verify on GCS S3-compat **What walwarden can verify:** - The HMAC key authenticates successfully. - Walwarden can write, read, and head an object in the bucket. - The bucket is not publicly accessible. **What walwarden cannot verify via the S3-compat path:** - Bucket Lock is active. The S3-compat path does not expose GCS Bucket Lock settings. Walwarden cannot assert the retention policy is in place the way it can check S3 Object Lock via `GetObjectLockConfiguration`. You must verify Bucket Lock independently (see step 2 above). - Cloud KMS customer-managed encryption keys. GCS's S3-compat API uses GCS-managed encryption regardless of any KMS configuration you have on the bucket. The S3-compat path does not accept AWS-style SSE-KMS headers for Cloud KMS. Walwarden's audit log will record the `endpointUrl` and `bucketName` for every backup event, but the lock-active assertion relies on your bucket configuration rather than a walwarden-run check. --- ## Common footguns **Using a service account JSON key instead of an HMAC key** GCS S3-compat authentication requires HMAC keys, not service account JSON keys. If you paste the access key ID from a JSON key file, authentication will fail. **Bucket Lock not locked** If you set a retention period without `--lock-retention-policy`, the retention period can be shortened or removed later. Walwarden requires a locked retention policy. Re-run the `update` command with `--lock-retention-policy`. **Fine-grained access control instead of uniform** GCS Bucket Lock requires uniform bucket-level access control (IAM, not ACLs). If you created the bucket with Fine-grained access control, you must recreate it — you cannot switch modes on an existing GCS bucket. **Wrong region format** GCS regions use a different naming convention than AWS. Use GCS region names (`us-east1`, `europe-west1`) not AWS names (`us-east-1`, `eu-west-1`). An incorrect region string will not cause authentication failures but will appear on the destination detail page and in the audit log. **HMAC key for a user account, not a service account** User account HMAC keys are bound to a personal Google account and expire when the person leaves the project. Always create HMAC keys for a dedicated service account. # Wasabi destination (guides/destinations/wasabi) Walwarden supports Wasabi as a backup destination via the S3-compatible API. This page covers creating and configuring the bucket and the access key walwarden uses to write backups. Walwarden supports the following parameters for this provider: | Parameter | Description | |---|---| | `endpointUrl` | The HTTPS endpoint for your Wasabi region (for example, `https://s3.us-east-1.wasabisys.com`). See the region table below. | | `bucketName` | The name of the Wasabi bucket (for example, `my-company-walwarden-backups`). | | `region` | The Wasabi region slug (for example, `us-east-1`). Must match the endpoint URL. | | `accessKeyId` | The Wasabi access key ID. | | `secretAccessKey` | The corresponding Wasabi secret key. | | `retentionFloorDays` | Minimum number of days each backup is retained under Object Lock. Must be between 1 and 365. | **Trust boundary note.** Walwarden stores the access key for this destination. Unlike the AWS BYO S3 path (which uses short-lived AssumeRole session credentials), Wasabi keys are long-lived. Use a minimum-scope key (see step 3 below) and rotate it periodically. Revoke the key immediately if walwarden is ever compromised. **Preflight status.** Wasabi destinations start in `unverified` state. Run preflight after registration so walwarden can verify the access key, bucket access, Object Lock, versioning, public-access posture, encryption behavior, and retention writes. --- ## Wasabi region endpoints Use the endpoint matching the region your bucket is in. Mixing endpoint and region causes authentication failures. | Region | Endpoint | |---|---| | US East 1 (N. Virginia) | `https://s3.us-east-1.wasabisys.com` | | US East 2 (N. Virginia) | `https://s3.us-east-2.wasabisys.com` | | US Central 1 (Texas) | `https://s3.us-central-1.wasabisys.com` | | US West 1 (Oregon) | `https://s3.us-west-1.wasabisys.com` | | EU Central 1 (Amsterdam) | `https://s3.eu-central-1.wasabisys.com` | | EU Central 2 (Frankfurt) | `https://s3.eu-central-2.wasabisys.com` | | EU West 1 (London) | `https://s3.eu-west-1.wasabisys.com` | | EU West 2 (Paris) | `https://s3.eu-west-2.wasabisys.com` | | AP Northeast 1 (Tokyo) | `https://s3.ap-northeast-1.wasabisys.com` | | AP Northeast 2 (Osaka) | `https://s3.ap-northeast-2.wasabisys.com` | | AP Southeast 1 (Singapore) | `https://s3.ap-southeast-1.wasabisys.com` | --- ## Step 1: Create the bucket with Object Lock enabled Wasabi Object Lock, like S3 Object Lock, must be enabled at bucket creation. You cannot enable it on an existing bucket. 1. Log in to the Wasabi Console. 2. Click **Create Bucket**. 3. Enter a bucket name. 4. Select your region. 5. Under **Object Locking**, toggle it on. 6. Click **Create Bucket**. After creation, set the default retention mode to GOVERNANCE: Using the AWS CLI with a Wasabi endpoint: ```bash aws s3api put-object-lock-configuration \ --bucket my-company-walwarden-backups \ --object-lock-configuration '{ "ObjectLockEnabled": "Enabled", "Rule": { "DefaultRetention": { "Mode": "GOVERNANCE", "Days": 30 } } }' \ --endpoint-url https://s3.us-east-1.wasabisys.com ``` Use GOVERNANCE mode, not COMPLIANCE. COMPLIANCE mode prevents even the account owner from deleting objects before the retention period, which creates operational risk if you need to recover storage. --- ## Step 2: Block public access By default, Wasabi buckets are private. Confirm the bucket has no public access policy applied. In the Wasabi Console, go to your bucket's **Settings > Access Control** and verify no public grants are in place. --- ## Step 3: Create a minimum-scope sub-user and access key Create a Wasabi sub-user for walwarden with a bucket-scoped policy. In the Wasabi Console: 1. Go to **Access Management > Users** and click **Create User**. 2. Enter a username, for example `walwarden-backup`. 3. Under **Programmatic (Access Key/Secret Key)**, generate an access key. Note the access key ID and secret. Create and attach a policy that grants only the permissions walwarden needs: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:HeadObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:GetObjectRetention", "s3:PutObjectRetention", "s3:GetObjectLegalHold", "s3:PutObjectLegalHold", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload" ], "Resource": [ "arn:aws:s3:::my-company-walwarden-backups", "arn:aws:s3:::my-company-walwarden-backups/*" ] } ] } ``` Attach the policy to the sub-user. --- ## Step 4: Register the destination in walwarden 1. In the walwarden dashboard, go to **Destinations > Add destination**. 2. Select **Wasabi** as the provider. 3. Fill in: - **Endpoint URL** — the endpoint for your Wasabi region (for example, `https://s3.us-east-1.wasabisys.com`). - **Bucket name** — the bucket name from step 1. - **Region** — the Wasabi region slug (for example, `us-east-1`). - **Access key ID** — from step 3. - **Secret access key** — from step 3. 4. Click **Save**. The destination will appear in state `unverified`. Run preflight from the destination detail page before attaching it to scheduled backups. --- ## Common footguns **Endpoint and region slug do not match** The endpoint URL and region field must refer to the same Wasabi region. Using `https://s3.us-east-1.wasabisys.com` with region `eu-central-1` will cause authentication failures. Copy both from the region table above together. **Object Lock not enabled at bucket creation** Wasabi, like AWS S3, does not allow enabling Object Lock on an existing bucket. If you created the bucket without Object Lock, delete it and re-create it with Object Lock on. **Using the root account key instead of a sub-user key** The root account has unrestricted access to all resources. Walwarden only needs access to one bucket. Always use a sub-user key with a bucket-scoped policy. **Wasabi egress charges in some regions** Wasabi's free egress policy applies when the destination of the traffic is not AWS. If you run walwarden on AWS and point it at a Wasabi bucket, some configurations may incur Wasabi egress charges. Review Wasabi's current pricing policy before choosing the region. **Forgot `PutObjectRetention` in the policy** Without `PutObjectRetention`, walwarden can write objects but cannot stamp the Object Lock retention hold on them. Preflight catches this. Add the action to the policy. # Backblaze B2 destination (guides/destinations/backblaze-b2) Walwarden supports Backblaze B2 as a backup destination via the S3-compatible API. This page covers creating and configuring the bucket, enabling File Lock, and creating the application key walwarden uses to authenticate. Walwarden supports the following parameters for this provider: | Parameter | Description | |---|---| | `endpointUrl` | The HTTPS S3-compatible endpoint for your B2 region (for example, `https://s3.us-west-002.backblazeb2.com`). See the region table below. | | `bucketName` | The name of the B2 bucket (for example, `my-company-walwarden-backups`). | | `region` | The B2 region slug (for example, `us-west-002`). Must match the endpoint URL. | | `accessKeyId` | The B2 application key ID (not the master key ID). | | `secretAccessKey` | The corresponding application key. | | `retentionFloorDays` | Minimum number of days each backup is retained under File Lock. Must be between 1 and 365. | **Trust boundary note.** Walwarden stores the application key for this destination. Unlike the AWS BYO S3 path (which uses short-lived AssumeRole session credentials), B2 application keys are long-lived. Use a minimum-scope, bucket-scoped application key (see step 3 below) and rotate it periodically. Revoke the key immediately if walwarden is ever compromised. **Preflight status.** Backblaze B2 destinations start in `unverified` state. Run preflight after registration so walwarden can verify the application key, bucket access, File Lock-compatible retention behavior, public-access posture, encryption behavior, and retention writes. --- ## File Lock vs. S3 Object Lock — semantic difference Backblaze B2 File Lock is B2's immutability mechanism. When accessed via the S3-compatible API, File Lock behaves similarly to S3 Object Lock: each file (object) carries an expiration timestamp before which deletion is refused. Differences from S3 Object Lock worth noting: - B2 File Lock uses COMPLIANCE mode internally. B2 does not distinguish between GOVERNANCE and COMPLIANCE mode via the S3-compat API. This means even the account owner cannot delete objects before the retention expires. Plan your retention period accordingly — an overly long period creates operational risk if you need to recover storage. - The S3-compat API exposes a subset of Object Lock operations. Some edge cases in S3 Object Lock (legal hold, per-request mode override) may not behave identically in B2. Walwarden uses only the subset that is reliably available. --- ## Backblaze B2 S3-compatible endpoints | Region | Endpoint | |---|---| | US West 001 (Phoenix) | `https://s3.us-west-001.backblazeb2.com` | | US West 002 (Phoenix) | `https://s3.us-west-002.backblazeb2.com` | | EU Central 003 (Amsterdam) | `https://s3.eu-central-003.backblazeb2.com` | --- ## Step 1: Create the bucket with File Lock enabled B2 File Lock must be enabled at bucket creation. You cannot enable it on an existing bucket. 1. Log in to the Backblaze console. 2. Go to **B2 Cloud Storage > Buckets** and click **Create a Bucket**. 3. Enter a bucket name. 4. Set **Files in Bucket** to **Private**. 5. Under **Object Lock**, toggle it on. 6. Click **Create a Bucket**. After creation, set the default retention mode and period. Using the AWS CLI with a B2 endpoint: ```bash aws s3api put-object-lock-configuration \ --bucket my-company-walwarden-backups \ --object-lock-configuration '{ "ObjectLockEnabled": "Enabled", "Rule": { "DefaultRetention": { "Mode": "COMPLIANCE", "Days": 30 } } }' \ --endpoint-url https://s3.us-west-002.backblazeb2.com ``` Note that B2 only supports COMPLIANCE mode via the S3-compat API. Plan your retention period carefully — objects cannot be deleted before the period expires, not even by the account owner. --- ## Step 2: Enable versioning B2 Object Lock (File Lock) requires versioning. Create the bucket with versioning enabled. In the Backblaze console this is automatic when File Lock is turned on. Verify: ```bash aws s3api get-bucket-versioning \ --bucket my-company-walwarden-backups \ --endpoint-url https://s3.us-west-002.backblazeb2.com ``` The response should show `Status: Enabled`. --- ## Step 3: Create a minimum-scope application key Do not use the master application key for walwarden. Create a bucket-scoped application key with only the capabilities walwarden needs. In the Backblaze console: 1. Go to **App Keys** and click **Add a New Application Key**. 2. Set **Name of Key** to `walwarden-backup`. 3. Under **Allow access to Bucket(s)**, select your walwarden bucket only. 4. Under **Type of Access**, select **Read and Write**. 5. Click **Create New Key**. The console will show the application key ID (`keyID`) and the application key once. Record both — the key is shown only at creation. Required capabilities: `readFiles`, `writeFiles`, `deleteFiles`, `listBuckets`, `listFiles`, `readBucketEncryption`, `readBucketRetentions`, `writeFileRetentions`. --- ## Step 4: Register the destination in walwarden 1. In the walwarden dashboard, go to **Destinations > Add destination**. 2. Select **Backblaze B2 (S3-compatible)** as the provider. 3. Fill in: - **Endpoint URL** — the S3-compat endpoint for your B2 region (for example, `https://s3.us-west-002.backblazeb2.com`). - **Bucket name** — the bucket name from step 1. - **Region** — the B2 region slug (for example, `us-west-002`). - **Access key ID** — the application key ID (`keyID`) from step 3. - **Secret access key** — the application key from step 3. 4. Click **Save**. The destination will appear in state `unverified`. Run preflight from the destination detail page before attaching it to scheduled backups. --- ## Common footguns **Using the master key instead of an application key** The master application key has unrestricted access to all buckets. If it is compromised, all your B2 data is at risk. Always create a bucket-scoped application key. **File Lock not enabled at bucket creation** You cannot enable File Lock on an existing B2 bucket. Delete the bucket and re-create it with File Lock on. **Endpoint and region do not match** The endpoint URL contains the region slug (`s3.us-west-002.backblazeb2.com` → region `us-west-002`). Pasting the wrong endpoint or mismatched region will produce authentication errors. Copy endpoint and region together from the table above. **COMPLIANCE mode retention is permanent** Unlike AWS GOVERNANCE mode, B2 File Lock in COMPLIANCE mode cannot be shortened or removed. If you set a 365-day retention period, every object is locked for at least 365 days. Plan the `retentionFloorDays` setting accordingly before registering the destination. **Egress charges** Backblaze B2 offers free egress to Cloudflare-peered providers. If walwarden runs on a host that is not Cloudflare-peered (including AWS, GCP, Azure), standard Backblaze egress rates apply. Review Backblaze's current bandwidth alliance partners before committing to B2 as a destination. # AWS preflight verification (guides/destinations/preflight-verification) When you add or update an AWS BYO S3 destination, run preflight verification before any backup can be scheduled against it. Preflight is a series of probes that confirm every required S3 and IAM capability is in place. For MinIO, GCS S3-compatible, Wasabi, and Backblaze B2 destinations, use the provider-specific destination guide. Those providers share the destination workflow but differ in retention semantics, public-access APIs, and which controls walwarden can verify directly. Preflight also runs on a recurring schedule after a destination is active, so configuration drift is detected before it blocks a backup. For AWS BYO S3, the destination role is intentionally backup-and-restore capable. Walwarden assumes the same external-ID-protected role when it writes backups, runs preflight, and signs restore GET URLs. A write-only role cannot be verified because restore would later fail to fetch the artifact. Least privilege is enforced by using a dedicated backup bucket and by the public-access block, TLS-only, external-ID, and Object Lock checks. ## What preflight checks | Check | Failure surface | |---|---| | `sts:AssumeRole` with the configured role ARN and external ID | Role does not exist, external ID mismatch, or walwarden's account is not listed as a trusted principal | | `s3:PutObject` — write a probe object to the bucket | Missing `PutObject` permission, bucket does not exist, or bucket is in a different region than configured | | `s3:GetObject` — read the probe object back for restore | Missing `GetObject` permission; restore presigning would not be able to fetch artifacts | | `s3:HeadObject` — check object metadata | Missing `HeadObject` permission | | `s3:PutObjectRetention` — apply a GOVERNANCE retention hold | Missing `PutObjectRetention` permission, or Object Lock is not enabled on the bucket | | `s3:GetObjectRetention` — verify the retention hold | Missing `GetObjectRetention` permission | | Public access block — confirm all four settings are enabled | Any setting is off | | Bucket versioning — confirm versioning is enabled | Versioning is disabled (Object Lock requires it) | | TLS enforcement — confirm the bucket policy denies non-TLS requests | Bucket policy missing or incomplete | | Cleanup — delete the probe object | Missing `DeleteObject` permission | ## What to do when preflight fails ### `sts:AssumeRole` failed The IAM role trust policy does not match. Most common causes: - The external ID in the trust policy does not match the value walwarden issued. Copy the external ID directly from the walwarden dashboard destination page and re-create the trust policy. Do not modify the value. - The trusted principal is wrong. It must be `arn:aws:iam::194343789105:root`. - The role ARN was entered incorrectly in walwarden. Verify the ARN in the IAM console and update the destination. ### Write probe object failed The IAM role does not have `s3:PutObject` on the bucket, or the bucket name or region is wrong. - Verify the bucket name in walwarden exactly matches the bucket name in the S3 console (case-sensitive). - Verify the region matches. - Open the IAM policy attached to the role and confirm `s3:PutObject` is listed with the correct bucket ARN. ### Read probe object failed The IAM role does not have `s3:GetObject` on the walwarden backup bucket. Add `s3:GetObject` for the dedicated bucket used by backup artifacts. Without it, preflight stays failed because restore URL signing uses this destination role to fetch the artifact. ### `PutObjectRetention` failed The IAM policy is missing `s3:PutObjectRetention`, or Object Lock is not enabled on the bucket. - Add `s3:PutObjectRetention` to the policy from [BYO AWS S3 step 3](/docs/guides/destinations/byo-aws-s3#step-3-create-the-iam-policy). - In the S3 console, go to the bucket **Properties** tab and confirm Object Lock is enabled. If it is not, you must create a new bucket — Object Lock cannot be enabled after creation. ### Public access block failed Go to the S3 bucket **Permissions** tab and confirm all four **Block public access** settings are checked. ### Object Lock not in GOVERNANCE mode If preflight reports that Object Lock is enabled but the mode is wrong, check the bucket's default retention settings. Walwarden requires GOVERNANCE mode. If the bucket was created with COMPLIANCE mode, create a new bucket. ### Preflight passes but backups still fail Preflight confirms IAM permissions and bucket configuration at a point in time. If an IAM policy is later modified or rotated, the next backup will fail even though the last preflight passed. Re-run preflight manually from the Destinations page after any IAM change. # Scheduled backups (guides/backup/scheduled) ## Setting the schedule On the database detail page in the walwarden dashboard, the **Schedule** section shows the current cron expression. A blank schedule means no automatic backups run. Enter a cron expression in five-field UTC format: ``` minute hour day-of-month month day-of-week ``` Examples: | Intent | Cron | |---|---| | Every hour, on the hour | `0 * * * *` | | Every 6 hours | `0 */6 * * *` | | Daily at 02:00 UTC | `0 2 * * *` | | Every 12 hours (02:00 and 14:00 UTC) | `0 2,14 * * *` | Click **Save schedule**. The change takes effect immediately; the next backup runs at the next matching tick. ## What the worker does on each run 1. Claims a `backup_job` row and transitions it from `queued` to `claimed`. 2. Opens a connection to the source database using the stored DSN. 3. Starts a `pg_dump --format=custom --compress=9` subprocess, streaming stdout to S3 via multipart upload. 4. Computes a SHA256 checksum of the dump bytes in flight. 5. Writes the dump object to S3 at the path `org//db//backup////.dump`. 6. Writes a manifest JSON to S3 at the same path with extension `.manifest.json`. The manifest includes the checksum, size in bytes, Postgres server version, and the job ID. 7. Signs the manifest with walwarden's Ed25519 private key. 8. Re-fetches the manifest from S3 and verifies size, checksum, and canonical body equality. 9. Transitions the job to `finalizing`, then to `completed`. 10. Writes a `backup.completed` audit event. If any step fails, the job transitions to a failed state and a `backup.failed` audit event is written. The dashboard shows the failure and the error classification. ## Audit chain entry Every backup produces the following sequence of audit events: - `backup.queued` — scheduler created the job - `backup.claimed` — worker claimed the job - `backup.running` — `pg_dump` subprocess started - `backup.finalizing` — dump uploaded; manifest written; verification in progress - `backup.completed` — manifest verified; job sealed You can view the full audit chain on the backup detail page in the dashboard, or export it as part of an evidence bundle. ## RPO and loss window The dashboard hero shows "Last backup: N minutes ago" based on the `finalized_at` timestamp of the most recent `completed` backup job. The "You would lose at most N minutes of data" calculation is the elapsed time since that timestamp. If the scheduled backup fails, the RPO display reflects the last successful backup, not the failed attempt. A failed backup does not advance the RPO bound. # Ad-hoc backup (guides/backup/ad-hoc) ## When to use an ad-hoc backup Use an ad-hoc backup before a schema migration, a bulk data operation, or any change that raises the cost of data loss. An ad-hoc backup runs immediately rather than waiting for the next scheduled tick. ## How to trigger an ad-hoc backup 1. Go to the database detail page in the walwarden dashboard. 2. Click **Back up now** in the top-right of the page. 3. Confirm the dialog. The backup job appears in the backup history table immediately with state `queued`. The dashboard updates the state in real time as the worker processes the job. ## Ad-hoc vs scheduled Ad-hoc backups go through the same worker pipeline as scheduled backups: `pg_dump`, checksum, S3 upload, manifest write, manifest verification, audit chain. The resulting artifact is identical in structure to a scheduled backup and can be restored using the same restore flow. Ad-hoc backups do not affect the schedule. If a scheduled backup is due in 30 minutes, it will still run. ## Availability Ad-hoc backups are available when: - The destination passes preflight. - No other backup job for the same database is in a non-terminal state (`queued`, `claimed`, or `running`). One backup per database at a time is enforced. If a backup is already in progress, the **Back up now** button is disabled until the current job completes or fails. # Restore overview (guides/restore/index) ## Two outcomes, one mechanism Restore is the same mechanism used for two very different reasons. Decide which one you are here for before you start: - **Recover from data loss** — production is down or damaged and you need the data back now. Start with [Recover from data loss](/docs/guides/restore/recover-from-data-loss): it is framed for the incident, leads with the safe default, and flags the destructive `in_place` path so you do not overwrite the wrong thing under pressure. - **Run a restore drill** — there is no incident; you are proving recoverability before you need it, against a target you control. Start with [Run a restore drill](/docs/guides/restore/run-a-restore-drill). The commands are identical for both. The difference is the target you point at and the stakes if you get the mode wrong. A backup you have never restored from is unproven — so do the drill before the incident forces the real thing. ## Why the CLI, not the dashboard Restoration is a destructive operation against a database you control. Walwarden cannot hold the credentials needed to write to your target database without becoming a high-value attack target and requiring you to trust walwarden with access you cannot audit. The trust boundary is enforced by architecture, not policy: - The dashboard issues a short-lived, single-use HMAC token. The token encodes which backup artifact to restore, which mode to use, and an expiration. - The CLI runs on a machine you control. It uses the token to claim the restore job, retrieve a presigned S3 URL for the artifact, download the bytes, verify the manifest checksum, and pipe the bytes to `pg_restore` on your machine. - Your target DSN never leaves your machine. The CLI accepts it as a flag or environment variable; it is not sent to walwarden's server. - The dump bytes flow from S3 to your machine to `pg_restore`. Walwarden's server never sees them. - Every state transition (download started, checksum verified, restore completed) is posted back to walwarden for the audit chain, but the payload contains no data content — only state labels and timestamps. ## High-level flow 1. You click "Restore from this backup" on the backup detail in the dashboard. 2. The dashboard issues a token and renders the one-liner command. 3. You copy the one-liner, replace the target DSN placeholder with your actual DSN, and run it in a terminal on a machine that can reach the target database. 4. The CLI claims the restore job, downloads the dump from S3 via presigned URL, verifies the SHA256 manifest checksum, and pipes bytes to `pg_restore`. 5. State transitions stream to the dashboard in real time via the audit event channel. 6. The CLI exits 0 on success. The token is invalidated. ## What you need on the restore machine - Node 20+ - `pg_restore` matching the major version of the source database (see [Install the CLI](/docs/getting-started/install-cli)) - Network access to the target Postgres (can be localhost, a VPN, a bastion — anywhere `psql` would work) - No AWS credentials required — the presigned URL handles S3 authentication ## Continue - [Recover from data loss](/docs/guides/restore/recover-from-data-loss) — the outcome-first runbook for a real incident - [Run a restore drill](/docs/guides/restore/run-a-restore-drill) — step-by-step with the actual commands, as a planned practice run - [Restore modes](/docs/guides/restore/modes) — `new_database` vs `in_place`, when to use each - [Troubleshooting](/docs/guides/restore/troubleshooting) — common failures and their remedies # Recover from data loss (guides/restore/recover-from-data-loss) You lost production data and you need it back. This is the page for the real event, not a practice run. It walks the exact path from "which backup do I trust" to "the data is back and I have checked it," and it flags the one step where a panicked operator can make things worse. If you are practising recovery before an incident, follow [Run a restore drill](/docs/guides/restore/run-a-restore-drill) instead — same mechanics, no urgency, no production target. The mechanics are identical; this page is framed for the moment they actually matter. ## The 60-second version 1. Open the source database in the dashboard and pick the most recent `completed` backup. Note its manifest hash. 2. Decide the target. Restoring into a **fresh** database is the safe default. Overwriting the live database is `in_place` and is destructive — only choose it when you are certain. 3. Click **Restore from this backup**, choose the mode, and issue the token. 4. Run the one-liner on a machine that can reach the target, with your target DSN. 5. Confirm the tables and row counts are back, then cut traffic over. The rest of this page is the same five steps, slower, with the safety calls spelled out. ## Step 1: Pick the backup to restore from Open the dashboard, go to the affected source database, and look at **Backup history**. Restore from the most recent backup whose state is `completed` — that is the lowest-data-loss point you have. The elapsed loss window shown on the dashboard tells you how much data sits between that backup and now; see [Recoverability and RPO](/docs/concepts/recoverability-and-rpo) for how to read it. If the most recent `completed` backup is from before the data loss event, prefer it. If the loss happened slowly (a bad migration that ran for hours, a creeping corruption), you may need to step back to an earlier backup that predates the damage. Backup history lists every retained artifact with its completion time and manifest hash so you can choose the right point. The manifest hash is your identity for the artifact. The CLI verifies the SHA256 checksum against the signed manifest before it writes anything, so a corrupt or wrong artifact fails closed rather than restoring bad bytes. ## Step 2: Choose the mode — and do not overwrite by accident This is the step where haste does damage. There are two modes, and they differ in exactly one way that matters during an incident: whether they destroy what is currently in the target. - **`new_database` (safe default).** Restores into a brand-new database and touches nothing that already exists on the target cluster. Nothing is overwritten. If you are not certain, choose this. You can restore here, verify the data, and then promote it. - **`in_place` (destructive).** Drops every object in the target database (`pg_restore --clean --if-exists`) before loading the backup. This is how you recover the live database in place — but if you point it at the wrong database, you have just destroyed a second one. `in_place` carries a deliberate guard so a panicked operator cannot trigger it by reflex: - The CLI **requires the `--confirm-destructive` flag**. Without it, the CLI exits with code 2 *before making any API call or touching the target* — nothing happens. The dashboard adds this flag to the one-liner automatically when, and only when, you select in-place mode. - In the dashboard modal, in-place mode shows a destructive-intent warning and disables **Issue restore token** until you tick "I understand this will overwrite the target database." A calm rule for the incident: **restore into a fresh database first, verify it, then decide whether to promote or overwrite.** Reach for `in_place` only when the target must keep its existing identity (connection strings, replication, provider constraints) and you have confirmed the target DSN names the exact database you mean to overwrite. The full mode comparison, including the Neon repeat-restore case, is in [Restore modes](/docs/guides/restore/modes). ## Step 3: Issue the token and run the restore Click **Restore from this backup** on the chosen backup row, select your mode, and click **Issue restore token**. The dashboard returns a one-liner. Run it on a machine that can reach the target Postgres, substituting your target DSN. Safe-default (`new_database`) recovery into a fresh database: ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target 'postgresql://user@host:5432/postgres' \ --created-database recovery_2026_06_30 \ --mode new_database ``` Setting `PGPASSWORD` instead of inlining the password keeps the secret out of your shell history: ```bash export PGPASSWORD='your-password' ``` Destructive in-place recovery of the live database (note the explicit guard flag, and that the target path names the database being overwritten): ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target 'postgresql://user@host:5432/the-live-db-name' \ --mode in_place \ --confirm-destructive ``` The CLI downloads the artifact from S3 via a presigned URL, verifies the manifest checksum, and pipes the bytes to `pg_restore` on your machine. Your target DSN never leaves the machine and walwarden never sees the dump bytes — the trust boundary is the same in an incident as in a drill (see [Restore overview](/docs/guides/restore)). Progress streams to your terminal and, in real time, to the dashboard's **Restore history**. The CLI exits 0 on success and the token is invalidated. Restore tokens are valid for one hour. If yours expires mid-incident, issue a new one from the same backup row. ## Step 4: Confirm the data is actually back A restore that exits 0 is not yet a recovery you have proven. Connect to the restored database and check that the data you lost is present: ```bash psql 'postgresql://user@host:5432/recovery_2026_06_30' -c "\dt" psql 'postgresql://user@host:5432/recovery_2026_06_30' -c "SELECT count(*) FROM your_critical_table;" ``` For a `new_database` restore, connect to the name you created (or the source database name if you did not pass `--created-database`). For `in_place`, connect to the database you overwrote. Spot-check the tables and row counts that matter for this incident before you cut production traffic over. Even one verified table count beats trusting an unverified restore. If you restored into a fresh database as the safe default, this is the point where you decide how to promote it — repoint the application, rename, or run a second `in_place` restore once you have confirmed the data is good. ## If the restore fails Every failure mode seen in testing and production — version mismatches, expired or already-used tokens, `database already exists` on Neon, jobs stuck in `downloading`, and non-zero `pg_restore` exit codes — has a diagnosis and remedy in [Restore troubleshooting](/docs/guides/restore/troubleshooting). The dashboard **Restore history** shows the failure state and classification for the job; start there, match it to the troubleshooting entry, and retry with a fresh token. ## Related - [Run a restore drill](/docs/guides/restore/run-a-restore-drill) — the same flow as a planned practice run, with no production target - [Restore modes](/docs/guides/restore/modes) — `new_database` vs `in_place` in full, including the `--confirm-destructive` guard - [Restore troubleshooting](/docs/guides/restore/troubleshooting) — failure modes and their fixes - [Restore overview](/docs/guides/restore) — why restore runs on your machine via the CLI # Run a restore drill (guides/restore/run-a-restore-drill) This guide: run an operator-initiated restore drill — restore a backup to a target you control and confirm it lands. A restore drill is how you turn a completed backup into proven recoverability. ## Before you start - `pg_restore` on the restore machine must match the major version of the source database. Check: `pg_restore --version`. If there is a mismatch, see [Install the CLI](/docs/getting-started/install-cli). - The restore machine must have network access to the target database. - You need a target Postgres database to restore into. For `new_database` mode, the target can be any Postgres cluster — walwarden will create a new database inside it. For `in_place` mode, the target database must already exist and you must accept that it will be overwritten. ## Step 1: Click "Restore from this backup" Navigate to the database detail page for the source database. In the **Backup history** table, find the backup you want to restore from and click **Restore from this backup**. A modal appears with a mode picker: - **New database** (default) — walwarden creates a new database on the target cluster using the source database name. The target cluster must not already have a database with that name, or the restore will fail. Use this for restore drills, cloning, and migrations. - **In-place** — walwarden overwrites an existing database on the target. The target database must already exist. A destructive-intent warning is shown. Selecting in-place enables a checkbox: "I understand this will overwrite the target database." The **Issue restore token** button is disabled until you check it. Select your mode and click **Issue restore token**. ## Step 2: Copy the one-liner The modal is replaced by the one-liner panel. The full command looks like: ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target '' \ --mode new_database ``` Click **Copy** to copy it to the clipboard. The panel shows an expiration countdown. Restore tokens are valid for 1 hour. If the token expires before you run the command, issue a new one from the same backup row. ## Step 3: Fill in the target DSN and run On the restore machine, paste the command and replace `` with your target Postgres DSN: ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target 'postgresql://user:password@host:5432/postgres' \ --mode new_database ``` If you prefer not to put the password inline, omit it from the DSN and set `PGPASSWORD`: ```bash export PGPASSWORD='your-password' WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target 'postgresql://user@host:5432/postgres' \ --mode new_database ``` Run the command. ## What happens next The CLI prints a rich progress view in a TTY, or JSONL events if stdout is not a TTY or `--json` is passed. A successful run produces output like: ``` phase: download bytes: 142MB / 142MB elapsed: 8.2s phase: verify checksum: ok elapsed: 0.3s phase: restore pg_restore: running elapsed: 4.1s phase: finalize state: completed exit: 0 ``` In JSONL mode (or if you pass `--json`): ```json {"state":"downloading","at":"2026-05-26T14:32:00.001Z","payload":{}} {"state":"verifying","at":"2026-05-26T14:32:08.412Z","payload":{}} {"state":"manifest_verified","at":"2026-05-26T14:32:08.723Z","payload":{"checksumSha256":""}} {"state":"restoring","at":"2026-05-26T14:32:08.800Z","payload":{}} {"state":"finalizing","at":"2026-05-26T14:32:12.901Z","payload":{}} {"state":"completed","at":"2026-05-26T14:32:13.001Z","payload":{"bytesRestored":148897024,"checksumSha256":""}} ``` The CLI exits 0. The restore token is now invalidated; it cannot be reused. ## Step 4: Verify in the dashboard Return to the dashboard database detail page. The **Restore history** section shows the completed restore job with its full state timeline, the manifest hash, and the bytes transferred. If the restore failed, the timeline shows the failure state and classification. See [Troubleshooting](/docs/guides/restore/troubleshooting). ## Step 5: Verify the target database (optional but recommended) Connect to the target database and confirm the expected tables and row counts are present: ```bash psql 'postgresql://user:password@host:5432/restored-db-name' -c "\dt" ``` For a `new_database` restore, the database was created with the source database name. Connect to that database name on the target cluster. A backup you have not verified by connecting to the restored database is a backup you have not fully proven. Even spot-checking one table count is better than no check. # Restore modes (guides/restore/modes) The CLI's `--mode` flag controls how `pg_restore` is invoked against the target database. ## `new_database` ```bash walwarden restore --mode new_database ... ``` By default, this calls `pg_restore --create` against a maintenance database on the target cluster. Postgres creates a new database with the same name as the source database, then restores all objects into it. When you pass `--created-database `, Walwarden first runs `createdb ` against the maintenance database, then restores into that fresh database without `pg_restore --create`. Use this when the source database name would collide with an existing database on providers like Neon. **When to use:** Restore drills, cloning to a staging environment, migrations, and any case where you want the data to land in a fresh database without touching anything that already exists on the target cluster. **Target requirement:** The target DSN must point at a maintenance database on the target cluster, typically `postgres`. If you omit `--created-database`, the source database name must not already exist on the target cluster. If it does, the restore fails at the `CREATE DATABASE` step. Fix: rerun with `--created-database `, use `--mode in_place` against the existing database, or drop the conflicting database first. **Neon note:** Neon manages its own databases. When you restore in `new_database` mode to a Neon target, use a maintenance DSN such as the project's `postgres` database and pass `--created-database ` when the dump's source database name already exists. **Optional flag:** `--created-database ` creates and restores into a fresh target database name. Names must start with a letter or underscore and contain only letters, digits, underscores, or hyphens. ## `in_place` ```bash walwarden restore --mode in_place --confirm-destructive ... ``` Internally, this calls `pg_restore --clean --if-exists` against the target database. All existing objects in the target database are dropped before the restore data is loaded. **When to use:** Restoring into an existing database when you accept overwriting its contents. Common cases: re-running a restore drill against the same target database, recovering a production database in place, or the Neon case after the first `new_database` restore has already created the database. **Target requirement:** The target DSN must include the database name as the path component (for example, `postgresql://user:password@host:5432/target-db-name`). The database must exist on the target cluster. **`--confirm-destructive` is required.** The flag must be present or the CLI exits with code 2 before making any API call. This is a local guard; no server confirmation is needed. The flag is included automatically in the dashboard one-liner when in-place mode is selected. ## Comparison | | `new_database` | `in_place` | |---|---|---| | pg_restore flags | `--create`, or restore into `--created-database` | `--clean --if-exists` | | Target database must exist | No | Yes | | Overwrites existing data | No | Yes | | Extra flag required | None; `--created-database` optional | `--confirm-destructive` | | Neon first restore | Yes | No | | Neon repeat restore | Yes, with `--created-database`; or use in_place | Yes | ## Choosing Use `new_database` for the first restore to any target. Use `in_place` when the database already exists and you accept overwriting it. When in doubt, use `new_database` and drop the created database afterward if it was only a verification run. # Restore troubleshooting (guides/restore/troubleshooting) ## pg_restore: error: aborting because of server version mismatch **What happened:** The `pg_restore` binary on your restore machine is a different major version than the Postgres server that produced the dump. **How to fix:** Install `pg_restore` matching the major version of the source database. - The source database's Postgres version is shown on the walwarden dashboard database detail page. - On macOS: `brew install postgresql@16` (replace `16` with the required version), then add `/opt/homebrew/opt/postgresql@16/bin` to your PATH. - On Debian/Ubuntu: `apt-get install postgresql-client-16`. - On other Linux: use the PGDG repositories at [postgresql.org/download](https://www.postgresql.org/download/). After installing, confirm the version with `pg_restore --version` and retry. --- ## User is not authorized to perform: s3:PutObjectRetention **What happened:** The IAM role walwarden assumed does not have `s3:PutObjectRetention` in its policy. This is required because the bucket has Object Lock enabled and walwarden applies retention holds to every artifact. **How to fix:** Add `s3:PutObjectRetention` (and `s3:PutObjectLegalHold`, `s3:GetObjectRetention`, `s3:GetObjectLegalHold`) to the IAM policy attached to the role. The full required policy is in [BYO AWS S3 step 3](/docs/guides/destinations/byo-aws-s3#step-3-create-the-iam-policy). After updating the policy, re-run preflight from the walwarden Destinations page. --- ## Could not load credentials from any providers **What happened:** The CLI is attempting to make AWS API calls directly but cannot find AWS credentials in the environment. This should not happen in the normal CLI restore flow (which uses a presigned URL, not AWS credentials), but can occur if the restore job was configured for a managed mode that requires direct S3 access. **How to fix:** In the standard `npx walwarden-cli restore` flow, AWS credentials on the restore machine are not required. The presigned URL in the restore token handles S3 authentication. If you see this error: 1. Confirm you are using the one-liner from the walwarden dashboard, not a manually assembled command. 2. Confirm `WALWARDEN_TOKEN` is set and is the token from the dashboard, not an older or different token. 3. If you are running a custom invocation, verify your CLI version with `walwarden --version` and update to the latest if needed. --- ## token already used **What happened:** Restore tokens are single-use. Once a `triggerRestore` API call succeeds, the token's intent ID is marked as consumed. Any subsequent attempt to use the same token is rejected. **How to fix:** Issue a new restore token from the dashboard. Navigate to the same backup row and click "Restore from this backup" again. --- ## creating DATABASE neondb ... database already exists **What happened:** You ran a `new_database` restore against a Neon target where the source database name already exists. This is common on the second and subsequent restores to the same Neon target — the first restore (in `new_database` mode) created the database; it is still there. **How to fix:** Use `in_place` mode for subsequent restores to the same target database: ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target 'postgresql://user:password@host:5432/the-existing-db-name' \ --mode in_place \ --confirm-destructive ``` The dashboard one-liner generator automatically adds `--confirm-destructive` when you select in-place mode in the modal. Alternatively, connect to the Neon target and drop the database manually: ```bash psql 'postgresql://user:password@host:5432/postgres' \ -c "DROP DATABASE \"the-existing-db-name\";" ``` Then re-run the original `new_database` restore. --- ## Token expired **What happened:** Restore tokens are valid for 1 hour from issuance. The token was not used within that window. **How to fix:** Issue a new token from the dashboard. The token expiration countdown is shown in the one-liner panel; if it reaches zero before you run the command, close the panel and click "Restore from this backup" again. --- ## restore_job stuck in downloading / verifying / restoring **What happened:** The CLI process was killed (SIGTERM, `kill -9`, terminal closed) while the restore was in progress. The server has a watchdog that detects inactive restore jobs and transitions them to `timed_out` after a configurable interval (typically a few minutes). **What to do:** Wait for the dashboard to show `timed_out`. Then issue a new token and start the restore again. The previous partial restore did not write any data to the target database unless the process reached the `restoring` state and `pg_restore` completed partially. In that case, use `--mode in_place --confirm-destructive` to overwrite the partial state on the next run. --- ## pg_restore exit code non-zero (generic) **What happened:** `pg_restore` failed. The CLI captures the exit code and classifies the failure as `failed_terminal`. The dashboard shows the error classification. Common causes: - The target database is unreachable (wrong host, port, or credentials in `--target`). Verify with `psql ''` before running the restore. - The dump file is corrupt. This should not happen if the manifest checksum verified successfully; if the checksum passed and `pg_restore` still fails, contact support with the restore job ID from the dashboard. - Insufficient privileges on the target — the target database user must have `CREATE` privileges (for `new_database` mode) or `DROP` and `CREATE` on the target database (for `in_place` mode). # Read your recovery posture (guides/operate/read-your-dashboard) This guide: read the dashboard the way an operator on call would. It walks every card on the logged-in surface — the **Recovery posture** hero, the activity strip, and the **Recent jobs log** — and tells you what a healthy value looks like, what a value that needs attention looks like, and where each button takes you. You arrive here once you have at least one database connected. Before that, the dashboard shows an onboarding checklist instead; see [Getting started](/docs/getting-started). ## The page at a glance The active dashboard stacks three regions, top to bottom: 1. **Recovery posture** — the hero. Your headline RPO, the last signed manifest, a restore-time estimate, the most recent drill outcome, and the two recovery actions. 2. **Activity strip** — three compact cards: open incidents, next scheduled events, retention compliance. 3. **Recent jobs log** — the unified timeline of snapshots, verifications, restore drills, and restores. Everything on the page is derived from data walwarden already holds. None of these numbers are estimates of the future; they are statements about what has actually happened and what is signed. ## Recovery posture (the hero) The hero answers one question: *if the source database failed right now, how much would I lose and how fast could I get back?* ### RPO tile The largest number on the page. RPO (Recovery Point Objective) is your **loss window** — the age of your most recent recoverable backup, rendered as elapsed time: | Loss window | Renders as | |---|---| | Under an hour | `12m 00s` (sub-minute resolution kept for incident reading) | | An hour to a day | `8h 36m` | | A day or more | `1d 4h` | | No successful backup yet | `—` | A loss window of `8h 36m` means your last `completed` backup finished eight hours and thirty-six minutes ago, so a failure now would lose at most the writes since then. For the full meaning of RPO here — and why it is a backup-recency figure, not a continuous-protection guarantee — see [Recoverability and RPO](/docs/concepts/recoverability-and-rpo). **Green vs amber.** The tile colours itself against the interval walwarden derives from *your* backup schedule (your RPO target — derived from the cron you set, never a fixed number): | Colour | Meaning | Example | |---|---|---| | Green | The loss window is inside your schedule's expected interval. This is the healthy state. | A database on a daily schedule reading `8h 36m` | | Amber | The loss window has drifted past your interval — a backup is overdue or recently failed. Check the jobs log and your schedule. | An hourly database reading `3h 10m` | | `—` (neutral) | No successful backup has landed yet. Run [pre-flight](/docs/guides/destinations/preflight-verification) and take a first backup. | A newly connected database | There is no red on the RPO number itself — a genuine failure surfaces as a red **Open incidents** count and a **Failed** row in the jobs log (below), which is where you act. ### Manifest hash tile The short hash (`ec664a…450c`) of the last Ed25519-signed manifest, with a caption like *signed 4 minutes ago by walwarden-worker*. This is the proof that the backup artifact exists, is signed, and is verifiable offline. A hash here means the [audit chain](/docs/concepts/audit-chain) recorded a signed artifact; `—` means no signed manifest yet. You can verify any manifest offline — see [Produce an evidence bundle](/docs/guides/evidence/produce-an-evidence-bundle). ### Restore estimate tile `≈ 4m 12s` is a derived estimate of how long a restore would take, based on recent backup durations as a proxy. It is honestly an estimate, never a guarantee. When no estimate exists the tile shows `—` with the caption *drill to learn* — the way to produce a real number is to run a drill. ### Drill status line Across the top of the hero: *Restore drill status: Passed 7m ago — no diff*, with a chip (**Passed**, **Failed**, **Pending**, or **no drill yet**). A backup you have never restored from is unproven; the drill line is where the most recent proof — or its absence — lives. `Restore drill status: —` means no drill has run. ### Recovery actions Two buttons sit in the **Recovery actions** panel, with the restore target named above them (*Target: your-database*): | Button | What it does | Where it leads | |---|---|---| | **Restore now** *(Recover this database)* | The accent action — restores your most recent backup to a target you control. Use this mid-incident. | [Run a restore drill](/docs/guides/restore/run-a-restore-drill) walks the same one-liner flow; pick a mode in [Restore modes](/docs/guides/restore/modes). | | **Run restore drill** *(Test recoverability — safe, no impact)* | Restores to a target you control to confirm recoverability. Safe to run any time; it does not touch your source database. | [Run a restore drill](/docs/guides/restore/run-a-restore-drill) | If no backup is available to restore from, both buttons disable and the panel reads *No backup to restore yet*. ## Activity strip Three cards beneath the hero. Each shows a count and a one-word status chip. ### Open incidents The count of failed snapshots plus failed restore drills in the last 24 hours. - **0** with *all systems verified* (green, chip *all clear*) is the healthy state. - A non-zero count is red (chip *needs attention*) with a breakdown such as *2 failed snapshots · 1 failed drill*. Click into the named database row in the jobs log to see the failure detail. ### Next scheduled events Counts down to what runs next: *Next backup in 56m* and *Next restore drill in 3h 14m*. If nothing is scheduled, the line shows `—` with a *Schedule one* affordance. Configure cadence in [Scheduled backups](/docs/guides/backup/scheduled). ### Retention compliance `30/30 days met` — how many of your target retention days currently have a recoverable backup: | Chip | Meaning | |---|---| | *on policy* (green) | Every target day is covered. | | *partial* (amber) | Fewer days met than targeted, no violation in the last 24h yet. | | *behind retention* (amber) | A retention violation occurred in the last 24 hours. | | *no policy set* (neutral) | No retention target configured. | Retention follows from your [backup schedule](/docs/guides/backup/scheduled). ## Recent jobs log The unified timeline. Every snapshot, verification, restore drill, and restore appears as one row, newest first. Columns: **Status**, **Time**, **Type**, **Database**, **Destination**, **Duration**, **Bytes**, **Manifest Hash**. ### Status chips | Status | Tone | |---|---| | **Completed** | Green | | **Running** / **Pending** | Amber (in flight) | | **Failed** | Red — click the database name to open the failure detail | ### What the blanks and `—` mean This is the most common point of confusion, because two different absences look similar: - **A blank cell** (Destination, Duration, or Bytes on a non-snapshot row) means the metric is **structurally not-applicable**. A verification, restore drill, or restore does not write bytes to S3, so it has no destination, duration-of-write, or byte count. The cell is intentionally empty — hover it for the reason. A blank here is *not* a gap in the proof. - **`—`** means **expected but absent**. On a snapshot row, a `—` under Duration, Bytes, or Manifest Hash means that value was expected and is genuinely missing — worth a look. A `—` in the Manifest Hash column means no signed manifest was recorded for that row. In short: **blank = not-applicable, `—` = expected-but-missing.** Verification rows are also rendered in a muted tone because they are routine paired checks folded beneath the snapshot they verify. ### Reading the log during an incident Start at the top. A green **Completed** snapshot with a manifest hash is a clean run. A red **Failed** row is your incident — open the database to see why. A run of **Verification** rows beneath a snapshot is the audit chain doing its job. The Time column carries a local-timezone timestamp, so you never do UTC math while the world is on fire. ## Getting alerted instead of watching You should not have to keep this page open. Wire [notification routes](/docs/guides/notifications/configure-routes) so failed backups, failed restore drills, audit-chain anomalies, and a stopped worker reach Slack, Discord, email, or a signed webhook — then the dashboard becomes the place you confirm, not the place you watch. ## The honest boundary These cards report on **scheduled logical backups** and operator-initiated restores. They are not a continuous-protection or unattended-restore claim. For exactly what the product does and does not do today, see [What is not shipped](/docs/concepts/what-is-not-shipped) and the [honest capability claims](/docs/reference/honest-capability-claims) reference. # Configure notification routes (guides/notifications/configure-routes) For a backup product the day-two question is simple: *who gets paged when something breaks?* Walwarden's notification center wires each failure condition to one or more delivery channels — Slack, Discord, email, or a signed webhook — scoped to your team. This guide walks the four alert conditions, adds a route for each channel, and confirms a route fires with a test send. ## Before you start - You must be a **team admin or owner**. Members can view the routes but only admins and owners can add, edit, enable/disable, delete, or test them. - For Slack and Discord, create an **incoming webhook** in that workspace/server first and copy its URL — you paste it into walwarden as the route destination. - For a generic webhook, your receiver must be reachable over public HTTPS. Walwarden blocks destinations that resolve to loopback, private, link-local, or otherwise reserved addresses (SSRF protection), so a `localhost` or internal-only endpoint will be rejected. ## The four alert conditions Each route is bound to exactly one event type. The notification center groups routes under these four conditions, each carrying a fixed severity: | Condition | Severity | What it means | |---|---|---| | **Failed backup** | Warning | A backup reached `failed_terminal` — it exhausted its retries and did not produce an artifact. Recoverability is degraded until the next successful run. | | **Failed restore drill** | Critical | A restore drill could not recover the database. The recoverability claim is unverified until the next drill succeeds. | | **Audit chain anomaly** | Critical | An audit-chain integrity check found a hash mismatch. Evidence bundles are not authoritative until verified offline with [`@walwarden/verifier`](/docs/concepts/audit-chain). | | **Worker not running** | Critical | The walwarden worker has not reported a heartbeat for more than five minutes. Backups stop running silently until the worker is restarted. | A condition with **no active route reaches nobody**. The notification center shows an "Alerts are off — setup incomplete" callout until at least one route exists, and the dashboard alerts card links here so the gap is visible. ## The four channels | Channel | Destination format | |---|---| | **Slack** | An incoming webhook URL on `hooks.slack.com`, e.g. `https://hooks.slack.com/services/T.../B.../...` | | **Discord** | A webhook URL on `discord.com`, e.g. `https://discord.com/api/webhooks/.../...` | | **Email** | A plain address, e.g. `security@example.com` | | **Webhook** | Any public HTTPS endpoint, e.g. `https://relay.example.com/walwarden`. Each delivery is HMAC-signed. | ## Step 1: Open the notification center In the dashboard, go to **Settings → Notification routes** to open the notification center. Each of the four conditions is a card. Existing routes for a condition appear in its table, with a **Destination**, **Delivery health** (last delivered, 24h failure count, last error), and **State** (active/disabled) column. ## Step 2: Add a route On the condition you want to wire, click **+ Add route**. A small form appears with three fields: - **Channel** — Slack, Discord, Email, or Webhook. The destination placeholder updates to match. - **Destination** (required) — the webhook URL or email address, per the formats above. Walwarden validates it on submit: Slack expects a `hooks.slack.com` URL, Discord a `discord.com` webhook URL, email a syntactically valid address, and a generic webhook any public HTTPS URL that passes the SSRF check. An invalid destination is rejected with a specific reason. - **Label** (optional) — a short human tag, e.g. `ops-channel` or `oncall-email`, shown next to the channel name so you can tell two routes on the same condition apart. Click **Add**. ### Webhook signing secret (webhook routes only) When you add a **webhook** route, walwarden mints a per-route **signing secret** and shows it **exactly once**. Copy it immediately — it is not retrievable later. Stamp it into your receiver's signature-verification config so it can authenticate deliveries. Each webhook delivery carries: - `X-Walwarden-Signature` — `v1=` where `` is the HMAC-SHA256 of `.` using the signing secret. - `X-Walwarden-Timestamp` — the signing timestamp, so your receiver can verify the signature and enforce a clock-skew window against replay. - `X-Walwarden-Event` and `X-Walwarden-Delivery` — the event type and a per-delivery id. The JSON body uses the `walwarden.notification.v1` schema: ```json { "schema": "walwarden.notification.v1", "eventType": "failed_backup", "eventId": "...", "severity": "warning", "occurredAt": "2026-06-30T12:00:00.000Z", "organizationSlug": "your-team", "subject": "Backup failed for db_prod", "summary": "...", "details": {} } ``` Slack, Discord, and email routes do not need a secret — they post to a destination you already trust. ## Step 3: Verify the route fires (test send) Do not wait for a real failure to find out a route is misconfigured. Each route row has a **Send test** action. Click it to push a synthetic alert through the full delivery path — the same dispatcher, signing, and counters a real event uses. The result is one of two outcomes: - **Delivered** — the channel accepted the delivery. The route's **Delivery health** updates with a fresh "Last delivered" time. - **Queued (pending)** — the test was enqueued and will deliver on the next worker tick. Watch the route's delivery health; "Last delivered" advances once it lands. If the channel rejects the delivery (a bad Slack/Discord URL, a 4xx from your webhook receiver, or the per-minute send cap), the test surfaces an error banner and the route's **Failures (24h)** and **Last error** advance — a failed test is never a silent no-op. ## Managing routes - **Disable / Enable** — temporarily stop a route from firing without deleting it (useful while you fix a downstream receiver). Disabled routes are skipped by the dispatcher. - **Delete** — remove the route. A confirmation is required. Deleting a webhook route invalidates its signing secret. - **Edit** — update the destination or label on an existing route. Changing a webhook destination re-validates it against the SSRF blocklist. Every add, edit, and delete is written to the org's audit chain (`notification.route_created` / `route_updated` / `route_deleted`), so route configuration is itself part of the evidence trail. ## Delivery, retries, and rate limits - Deliveries follow bounded retries — 1 minute, 5 minutes, 15 minutes, 1 hour — with a hard cap of five attempts inside a 24-hour window. A `4xx` (other than 408/425/429) is treated as a permanent failure and is not retried. - Each route is capped at 60 outbound deliveries per minute, so a noisy failure loop on one route cannot self-DoS your Slack/Discord/webhook receiver. - Walwarden honors a `Retry-After` (Slack/webhook) or `X-RateLimit-Reset-After` (Discord) the provider returns, never posting sooner than asked, but never extending past the 24-hour budget. ## Related - [What is not shipped](/docs/concepts/what-is-not-shipped) — the honest boundary of what walwarden does today. - [Audit chain](/docs/concepts/audit-chain) — what an audit-chain anomaly means and how to verify a bundle offline. - [Run a restore drill](/docs/guides/restore/run-a-restore-drill) — the action a failed-restore-drill alert tells you to repeat. # Produce an evidence bundle (guides/evidence/produce-an-evidence-bundle) This guide: export an evidence bundle for a backup artifact and verify it offline. The bundle is what a compliance reviewer needs to confirm a backup exists, is signed, and has an intact audit chain — without trusting the dashboard. ## Prerequisites - A protected database with at least one `completed` backup. - Node 20+ on the machine where you will verify the bundle. ## Step 1: Export the bundle 1. In the dashboard, open the database detail page. 2. In **Backup history**, find the backup artifact you want evidence for. 3. Click **Download evidence bundle**. You get a `.tgz` containing the signed manifest and every audit event for that artifact. ## Step 2: Verify it offline The bundle is verifiable without contacting walwarden. Download the public Ed25519 verification key, then run the verifier: ```bash # Walwarden's published public key curl -O https://walwarden.com/.well-known/walwarden-pubkey.pem # Verify the bundle you exported npx @walwarden/verifier \ --bundle evidence-bundle.tgz \ --pubkey walwarden-pubkey.pem ``` A successful run prints, for example: ``` OK: 1 manifests verified, 6 audit events chain-intact (exit 0) ``` ## Verify it worked The verifier exits `0` and reports the manifest signature valid and the audit event sequence contiguous. That bundle is now self-contained evidence: an auditor can re-run the same command on an air-gapped machine given only the bundle and the public key. For what the audit chain records and how verification works in detail, see [The audit chain](/docs/concepts/audit-chain). # CLI reference (reference/cli) The `walwarden` binary has two separate command surfaces: - SDK-backed public commands for API-key automation, CI, and agents. - Legacy restore-token execution using `walwarden restore --manifest ...` from a dashboard one-liner. This generated reference covers the public alpha commands from `packages/cli/src/publicCommandMetadata.ts`. ## Install ```bash npm install -g walwarden-cli # or run without installing npx --yes walwarden-cli --json database list ``` Configuration precedence: flags, environment, legacy `WALWARDEN_API_URL` alias, then profile config at `~/.config/walwarden/config.json`. ## Global Flags | Flag | Description | |---|---| | `--json` | Emit structured JSON output. | | `--profile ` | Select a profile from the Walwarden config file. Defaults to default. | | `--config ` | Read profile configuration from a JSON config file. | | `--api-url ` | Override WALWARDEN_BASE_URL for this invocation. | | `--api-key ` | Override WALWARDEN_API_KEY for this invocation. | | `--timeout-ms ` | Set the SDK request timeout in milliseconds. | ## Public Commands | Command | Description | Scopes | |---|---|---| | `profile validate` | Validates local config/API-key wiring through the public profile endpoint. | none | | `database list` | Lists protected databases visible to the API key. | `databases:read` | | `database get ` | Reads one protected database. | `databases:read` | | `destination list` | Lists backup destinations visible to the API key without returning credential material. | `destinations:read` | | `destination get ` | Reads one backup destination summary without returning credential material. | `destinations:read` | | `backup list --database ` | Lists backup jobs for a database. | `databases:read` | | `backup trigger --database [--wait]` | Triggers an ad hoc backup and optionally polls backup status. | `backups:trigger` | | `backup status ` | Reads backup job status and artifact metadata when available. | `databases:read` | | `evidence list [--database ]` | Lists public evidence metadata, optionally filtered to one database. | `evidence:read` | | `evidence get ` | Reads public evidence detail for one backup job. | `evidence:read` | | `restore create --backup --target --mode [--created-database ] [--confirm-destructive]` | Creates a restore execution session from a completed backup. Raw target DSN remains CLI-local. For new_database restores, pass a maintenance database DSN such as postgres; add --created-database to avoid source-name collisions on providers like Neon. | `restores:write` | | `restore execute --backup --target --mode [--created-database ] [--confirm-destructive]` | Creates a restore session, executes it locally, and reports terminal status. Raw target DSN and execution coordinates remain CLI-local. For new_database restores, pass a maintenance database DSN and optionally --created-database for the fresh database to create. | `restores:write`, `restores:read` | | `restore status ` | Reads restore job status. | `restores:read` | | `capabilities list` | Lists the bundled Walwarden capability contract with registry compatibility and per-capability CLI support. Runs locally; no API call. | none | | `capabilities describe ` | Describes one capability entry (auth, risk, human gate, schemas, structured errors) from the bundled contract. Runs locally; no API call. | none | | `verify-environment` | Runs a local pre-flight over contract compatibility, Node runtime, and non-secret connection config. Never inspects or persists target-DB write credentials. | none | | `explain-error --from-json ` | Explains a structured API/CLI error object from a file or stdin. | none | ### `profile validate` Validates local config/API-key wiring through the public profile endpoint. ```bash walwarden --json profile validate ``` Kind: `profile.validate` Required flags: none Optional flags: none SDK methods: `checkCompatibility`, `getProfile` ### `database list` Lists protected databases visible to the API key. ```bash walwarden --json database list ``` Kind: `database.list` Required flags: none Optional flags: none SDK methods: `listDatabases` ### `database get ` Reads one protected database. ```bash walwarden --json database get ``` Kind: `database.get` Required flags: none Optional flags: none SDK methods: `getDatabase` ### `destination list` Lists backup destinations visible to the API key without returning credential material. ```bash walwarden --json destination list ``` Kind: `destination.list` Required flags: none Optional flags: none SDK methods: `listDestinations` ### `destination get ` Reads one backup destination summary without returning credential material. ```bash walwarden --json destination get ``` Kind: `destination.get` Required flags: none Optional flags: none SDK methods: `getDestination` ### `backup list --database ` Lists backup jobs for a database. ```bash walwarden --json backup list --database ``` Kind: `backup.list` Required flags: `--database ` Optional flags: none SDK methods: `listDatabaseBackups` ### `backup trigger --database [--wait]` Triggers an ad hoc backup and optionally polls backup status. ```bash walwarden --json backup trigger --database --wait ``` Kind: `backup.trigger` Required flags: `--database ` Optional flags: `--wait` SDK methods: `triggerBackup`, `pollBackup` ### `backup status ` Reads backup job status and artifact metadata when available. ```bash walwarden --json backup status ``` Kind: `backup.status` Required flags: none Optional flags: none SDK methods: `getBackup` ### `evidence list [--database ]` Lists public evidence metadata, optionally filtered to one database. ```bash walwarden --json evidence list --database ``` Kind: `evidence.list` Required flags: none Optional flags: `--database ` SDK methods: `listEvidence` ### `evidence get ` Reads public evidence detail for one backup job. ```bash walwarden --json evidence get ``` Kind: `evidence.get` Required flags: none Optional flags: none SDK methods: `getEvidence` ### `restore create --backup --target --mode [--created-database ] [--confirm-destructive]` Creates a restore execution session from a completed backup. Raw target DSN remains CLI-local. For new_database restores, pass a maintenance database DSN such as postgres; add --created-database to avoid source-name collisions on providers like Neon. ```bash walwarden --json restore create --backup --target "$TARGET_MAINTENANCE_DATABASE_URL" --mode new_database --created-database restored_db_20260608 ``` Kind: `restore.create` Required flags: `--backup `, `--target `, `--mode ` Optional flags: `--created-database `, `--confirm-destructive` SDK methods: `createRestore` ### `restore execute --backup --target --mode [--created-database ] [--confirm-destructive]` Creates a restore session, executes it locally, and reports terminal status. Raw target DSN and execution coordinates remain CLI-local. For new_database restores, pass a maintenance database DSN and optionally --created-database for the fresh database to create. ```bash walwarden --json restore execute --backup --target "$TARGET_MAINTENANCE_DATABASE_URL" --mode new_database --created-database restored_db_20260608 ``` Kind: `restore.execute` Required flags: `--backup `, `--target `, `--mode ` Optional flags: `--created-database `, `--confirm-destructive` SDK methods: `createRestore`, `getRestore` ### `restore status ` Reads restore job status. ```bash walwarden --json restore status ``` Kind: `restore.status` Required flags: none Optional flags: none SDK methods: `getRestore` ### `capabilities list` Lists the bundled Walwarden capability contract with registry compatibility and per-capability CLI support. Runs locally; no API call. ```bash walwarden --json capabilities list ``` Kind: `capabilities.list` Required flags: none Optional flags: none SDK methods: none ### `capabilities describe ` Describes one capability entry (auth, risk, human gate, schemas, structured errors) from the bundled contract. Runs locally; no API call. ```bash walwarden --json capabilities describe backup.trigger ``` Kind: `capabilities.describe` Required flags: none Optional flags: none SDK methods: none ### `verify-environment` Runs a local pre-flight over contract compatibility, Node runtime, and non-secret connection config. Never inspects or persists target-DB write credentials. ```bash walwarden --json verify-environment ``` Kind: `verify-environment` Required flags: none Optional flags: none SDK methods: none ### `explain-error --from-json ` Explains a structured API/CLI error object from a file or stdin. ```bash walwarden --json explain-error --from-json ./error.json ``` Kind: `explain-error` Required flags: none Optional flags: `--from-json ` SDK methods: none ## Explicit Public CLI Non-Goals The alpha public CLI does not include destination write/verify commands, login, whoami, PITR, or offline evidence bundle verification. ## Legacy Restore-Token Command The dashboard-generated restore command remains available for customer-side restore execution. The target DSN stays on the customer machine. ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target 'postgresql://user:password@host:5432/dbname' \ --mode new_database ``` For `--mode in_place`, pass `--confirm-destructive`. See restore docs for the legacy restore-token walkthrough. # SDK install and examples (reference/sdk) Install the dependency-free TypeScript ESM client: ```bash npm install @walwarden/sdk ``` ```ts import { createWalwardenClient } from '@walwarden/sdk'; const walwarden = createWalwardenClient({ baseUrl: process.env.WALWARDEN_BASE_URL!, apiKey: process.env.WALWARDEN_API_KEY!, userAgent: 'my-service/1.0', }); const { databases } = await walwarden.listDatabases(); ``` Source: [GitHub `packages/sdk`](https://github.com/noncelogic/walwarden/tree/main/packages/sdk). Package: [`@walwarden/sdk` on npm](https://www.npmjs.com/package/@walwarden/sdk). ## Methods | SDK method | API operation | Scope | Notes | |---|---|---|---| | `getProfile` | `getProfile` | `none` | Validate API key profile | | `listDatabases` | `listDatabases` | `databases:read` | List protected databases | | `getDatabase` | `getDatabase` | `databases:read` | Read a protected database | | `listDestinations` | `listDestinations` | `destinations:read` | List backup destinations | | `getDestination` | `getDestination` | `destinations:read` | Read a backup destination | | `listDatabaseBackups` | `listDatabaseBackups` | `databases:read` | List backup jobs for a database | | `triggerBackup` | `triggerBackup` | `backups:trigger` | Trigger an ad-hoc backup | | `getBackup` | `getBackup` | `databases:read` | Read backup job status | | `listEvidence` | `listEvidence` | `evidence:read` | List evidence metadata | | `getEvidence` | `getEvidence` | `evidence:read` | Read evidence detail | | `createRestore` | `createRestore` | `restores:write` | Create restore job | | `getRestore` | `getRestore` | `restores:read` | Read restore job status | | `getRecoverySummary` | `getRecoverySummary` | `databases:read` | Read recovery and custody summary | | `getRecoveryCandidate` | `getRecoveryCandidate` | `databases:read` | Find incident-time recovery candidate | | `getRecoveryWindowProof` | `getRecoveryWindowProof` | `databases:read` | Read recovery-window proof status | ## Backup With Evidence Check ```ts const triggered = await walwarden.triggerBackup(databaseId, { idempotencyKey: crypto.randomUUID(), trigger: 'adhoc', }); const completed = await walwarden.pollBackup(triggered.backupJobId, { timeoutMs: 120_000 }); if (completed.state !== 'completed' || !completed.artifact) throw new Error('backup did not produce artifact evidence'); const evidence = await walwarden.listEvidence({ databaseId }); const item = evidence.evidence.find((candidate) => candidate.backupJobId === completed.id); if (!item?.integrityVerification || item.integrityVerification.result !== 'passed') { throw new Error('backup does not have passed integrity evidence'); } ``` Do not report recoverability from backup completion alone. Treat evidence as successful only after checking the evidence response semantics. Restore stays a two-step surface: use `createRestore` to open a CLI-local execution session and `getRestore`/`pollRestore` for status reads. End-to-end execution runs through `restore execute` on the CLI, which is proven against a live disposable target (E2E run `e2e-20260607T1941-8beaa08`, audit chain reaching `restore.completed`; evidence in #320). # API reference (reference/api) Walwarden Public API 1.0.0-alpha. Public REST API v1 for scoped machine clients. This alpha slice exposes database and destination reads, backup trigger/status, evidence metadata, and restore create/status for CLI-local execution. The `restore create`/`status` pair opens and tracks a CLI-local restore session; end-to-end execution runs through the CLI `restore execute` bridge, proven against a live disposable target (E2E run e2e-20260607T1941-8beaa08, audit chain reaching restore.completed; evidence in #320). Base path: `/api/v1`. Send API keys as `Authorization: Bearer `. Issue and scope a token from the dashboard — see [Issue a dashboard API token](/docs/reference/issue-api-tokens). The alpha surface is intentionally small. `restore create` opens a CLI-local restore session; `restore execute` is the proven end-to-end local execution bridge, verified against a live disposable target (E2E run `e2e-20260607T1941-8beaa08`, audit chain reaching `restore.completed`; evidence in #320). ## Operations | Method | Path | Operation | Scope | Success | |---|---|---|---|---| | `GET` | `/profile` | Validate API key profile (`getProfile`) | `none` | `200` | | `GET` | `/databases` | List protected databases (`listDatabases`) | `databases:read` | `200` | | `GET` | `/databases/{databaseId}` | Read a protected database (`getDatabase`) | `databases:read` | `200` | | `GET` | `/destinations` | List backup destinations (`listDestinations`) | `destinations:read` | `200` | | `GET` | `/destinations/{destinationId}` | Read a backup destination (`getDestination`) | `destinations:read` | `200` | | `GET` | `/databases/{databaseId}/backups` | List backup jobs for a database (`listDatabaseBackups`) | `databases:read` | `200` | | `POST` | `/databases/{databaseId}/backups` | Trigger an ad-hoc backup (`triggerBackup`) | `backups:trigger` | `202` | | `GET` | `/backups/{backupJobId}` | Read backup job status (`getBackup`) | `databases:read` | `200` | | `GET` | `/evidence` | List evidence metadata (`listEvidence`) | `evidence:read` | `200` | | `GET` | `/evidence/{backupJobId}` | Read evidence detail (`getEvidence`) | `evidence:read` | `200` | | `POST` | `/restores` | Create restore job (`createRestore`) | `restores:write` | `202` | | `GET` | `/restores/{restoreJobId}` | Read restore job status (`getRestore`) | `restores:read` | `200` | | `GET` | `/databases/{databaseId}/recovery/summary` | Read recovery and custody summary (`getRecoverySummary`) | `databases:read` | `200` | | `GET` | `/databases/{databaseId}/recovery/candidate` | Find incident-time recovery candidate (`getRecoveryCandidate`) | `databases:read` | `200` | | `GET` | `/recovery/windows/{windowId}/proof` | Read recovery-window proof status (`getRecoveryWindowProof`) | `databases:read` | `200` | ## Validate API key profile `GET /profile` Operation ID: `getProfile` Required scope: `none` Success responses: `200` Returns safe machine-client identity metadata for the active API key. Requires a valid Bearer API key but no resource scope. Parameters: none Request body: none Error responses: `401` ## List protected databases `GET /databases` Operation ID: `listDatabases` Required scope: `databases:read` Success responses: `200` Parameters: none Request body: none Error responses: `401`, `403` ## Read a protected database `GET /databases/{databaseId}` Operation ID: `getDatabase` Required scope: `databases:read` Success responses: `200` Parameters: `databaseId in path required` Request body: none Error responses: `401`, `403`, `404` ## List backup destinations `GET /destinations` Operation ID: `listDestinations` Required scope: `destinations:read` Success responses: `200` Lists backup destination summaries without returning credential material. Parameters: none Request body: none Error responses: `401`, `403` ## Read a backup destination `GET /destinations/{destinationId}` Operation ID: `getDestination` Required scope: `destinations:read` Success responses: `200` Reads one backup destination summary without returning credential material. Parameters: `destinationId in path required` Request body: none Error responses: `401`, `403`, `404` ## List backup jobs for a database `GET /databases/{databaseId}/backups` Operation ID: `listDatabaseBackups` Required scope: `databases:read` Success responses: `200` Parameters: `databaseId in path required` Request body: none Error responses: `401`, `403`, `404` ## Trigger an ad-hoc backup `POST /databases/{databaseId}/backups` Operation ID: `triggerBackup` Required scope: `backups:trigger` Success responses: `202` Parameters: `databaseId in path required`, `Idempotency-Key in header required` Request body: `TriggerBackupRequest` Error responses: `400`, `401`, `403`, `404`, `409`, `412` ## Read backup job status `GET /backups/{backupJobId}` Operation ID: `getBackup` Required scope: `databases:read` Success responses: `200` Parameters: `backupJobId in path required` Request body: none Error responses: `401`, `403`, `404` ## List evidence metadata `GET /evidence` Operation ID: `listEvidence` Required scope: `evidence:read` Success responses: `200` Parameters: `databaseId in query` Request body: none Error responses: `401`, `403` ## Read evidence detail `GET /evidence/{backupJobId}` Operation ID: `getEvidence` Required scope: `evidence:read` Success responses: `200` Parameters: `backupJobId in path required` Request body: none Error responses: `401`, `403`, `404` ## Create restore job `POST /restores` Operation ID: `createRestore` Required scope: `restores:write` Success responses: `202` Creates a restore job for CLI-local execution from a completed backup. Raw target database credentials must remain client-side; the request sends only targetRedactedDsn. Do not claim this restore flow production-green until the #320 live disposable-target E2E proof is attached. Parameters: `Idempotency-Key in header required` Request body: `CreateRestoreRequest` Error responses: `400`, `401`, `403`, `404`, `409`, `412` ## Read restore job status `GET /restores/{restoreJobId}` Operation ID: `getRestore` Required scope: `restores:read` Success responses: `200` Parameters: `restoreJobId in path required` Request body: none Error responses: `401`, `403`, `404` ## Read recovery and custody summary `GET /databases/{databaseId}/recovery/summary` Operation ID: `getRecoverySummary` Required scope: `databases:read` Success responses: `200` Returns the latest decision-grade recovery window summary for a database. Parameters: `databaseId in path required` Request body: none Error responses: `401`, `403`, `404` ## Find incident-time recovery candidate `GET /databases/{databaseId}/recovery/candidate` Operation ID: `getRecoveryCandidate` Required scope: `databases:read` Success responses: `200` Returns the closest restore-ready snapshot/cursor candidate before the requested incident time. Parameters: `databaseId in path required`, `incidentTime in query required` Request body: none Error responses: `400`, `401`, `403`, `404` ## Read recovery-window proof status `GET /recovery/windows/{windowId}/proof` Operation ID: `getRecoveryWindowProof` Required scope: `databases:read` Success responses: `200` Returns proof and custody status for a selected recovery window without exposing raw segment internals. Parameters: `windowId in path required` Request body: none Error responses: `401`, `403`, `404` ## Contract Artifact Download the OpenAPI artifact at `/openapi/walwarden.v1.json`. # API auth and scopes (reference/api-auth-scopes) Use a scoped Walwarden API key with the HTTP bearer scheme: ```bash curl -H 'Authorization: Bearer $WALWARDEN_API_KEY' \ '$WALWARDEN_BASE_URL/api/v1/databases' ``` Issue, scope, and revoke these keys from the dashboard — see [Issue a dashboard API token](/docs/reference/issue-api-tokens). Missing, inactive, or expired keys return `401 unauthorized`. Keys without the endpoint scope return `403 forbidden` with `requiredScope` and `nextAction` in the structured error body. Mutation endpoints require an `Idempotency-Key` header. Reusing a key with a different request body returns `409 conflict`. ## Scopes Used By Public API v1 Alpha | Scope | Label | Description | |---|---|---| | `databases:read` | Read protected databases | List and inspect protected database records. | | `destinations:read` | Read backup destinations | List and inspect configured backup destinations. | | `backups:trigger` | Trigger backup jobs | Start, cancel, and dismiss backup jobs. | | `restores:read` | Read restore jobs | List and inspect restore jobs and restore-drill state. | | `restores:write` | Run restore jobs | Start restore jobs and restore drills with explicit targets. | | `evidence:read` | Read evidence bundles | Read backup, restore, and verification evidence artifacts. | ## Endpoint Scope Matrix | Method | Path | Operation | Required scope | |---|---|---|---| | `GET` | `/profile` | `getProfile` | `none` | | `GET` | `/databases` | `listDatabases` | `databases:read` | | `GET` | `/databases/{databaseId}` | `getDatabase` | `databases:read` | | `GET` | `/destinations` | `listDestinations` | `destinations:read` | | `GET` | `/destinations/{destinationId}` | `getDestination` | `destinations:read` | | `GET` | `/databases/{databaseId}/backups` | `listDatabaseBackups` | `databases:read` | | `POST` | `/databases/{databaseId}/backups` | `triggerBackup` | `backups:trigger` | | `GET` | `/backups/{backupJobId}` | `getBackup` | `databases:read` | | `GET` | `/evidence` | `listEvidence` | `evidence:read` | | `GET` | `/evidence/{backupJobId}` | `getEvidence` | `evidence:read` | | `POST` | `/restores` | `createRestore` | `restores:write` | | `GET` | `/restores/{restoreJobId}` | `getRestore` | `restores:read` | | `GET` | `/databases/{databaseId}/recovery/summary` | `getRecoverySummary` | `databases:read` | | `GET` | `/databases/{databaseId}/recovery/candidate` | `getRecoveryCandidate` | `databases:read` | | `GET` | `/recovery/windows/{windowId}/proof` | `getRecoveryWindowProof` | `databases:read` | # Agent integration recipes (reference/agent-integration) Agents may use the SDK or public CLI for read-only database/destination inspection, ad hoc backup trigger, backup status polling, restore session creation, explicit CLI-local restore execution, restore status reads, and evidence metadata reads when provided with an appropriately scoped API key. ## Safe CLI Pattern ```bash walwarden --json profile validate walwarden --json database list walwarden --json destination list walwarden --json backup trigger --database "$WALWARDEN_DATABASE_ID" --wait walwarden --json evidence list --database "$WALWARDEN_DATABASE_ID" ``` ## Evidence-Before-Success Rule An agent must not mark a backup, migration, or restore drill successful until it has checked the relevant command result and evidence metadata. Backup completion alone proves only that a job completed; it is not the same as proven recoverability. ## Forbidden Claims - Do not claim the alpha public CLI can wait for or complete restore jobs through `restore create`; that command only opens a CLI-local session. Use `restore execute` for the proven end-to-end local execution bridge (live disposable-target E2E run `e2e-20260607T1941-8beaa08`, evidence in #320). - Do not claim destination write, verify, attach, detach, or delete commands exist in the public CLI. - Do not claim login or whoami commands exist. - Do not claim PITR support. - Do not claim offline evidence bundle verification through the public CLI/SDK alpha surface. - Do not claim recoverability unless restore or evidence semantics support that exact claim. ## Capability Status From Source Registry | Capability | Status | Required scopes | |---|---|---| | Manage protected databases (`databases.manage`) | `planned` | `databases:read`, `databases:write` | | Run and inspect backup jobs (`backups.run`) | `available` | `databases:read`, `backups:trigger` | | Inspect backup destinations (`destinations.inspect`) | `available` | `destinations:read` | | Run CLI-local restore jobs (`restores.run`) | `available` | `restores:write`, `restores:read`, `evidence:read` | | Read and verify evidence bundles (`evidence.verify`) | `available` | `evidence:read`, `audit:read` | | Preflight logical recovery eligibility (`recovery.preflight`) | `planned` | `recovery:read` | | Enable logical recovery window (`recovery.enable`) | `planned` | `recovery:read`, `recovery:write` | | Disable logical recovery window (`recovery.disable`) | `planned` | `recovery:read`, `recovery:write` | | Read logical recovery status (`recovery.status`) | `planned` | `recovery:read` | | Restore from logical recovery window (`recovery.restore`) | `planned` | `recovery:read`, `recovery:write` | Unsupported or planned capabilities stay out of agent success claims until their status changes in `packages/core/src/developerSurface.ts`. # MCP and external-agent integration (reference/mcp-integration) External agent clients (Claude Desktop, Cursor, ChatGPT-compatible clients, and other MCP/HTTP consumers) discover Walwarden capabilities through an agent-scoped API token and execute them against the authenticated REST API v1. The authoritative tool catalog is served from the live capability registry, not from this page: authenticated clients should read it from the API. Surface version `w4.2.0` (min client contract `w4.2.0`). Generated from `packages/core/src/capabilityContract.ts`. ## Transport Status There is no native MCP server transport (JSON-RPC `initialize`/`tools/list`/`tools/call` over streamable HTTP or SSE) yet, so a `mcpServers.url` entry in `claude_desktop_config.json` or `.cursor/mcp.json` cannot complete an MCP handshake against Walwarden today. A native MCP endpoint is planned but not yet shipped. Until it lands, integrate over the HTTP surface below: discover tools from `GET /api/v1/mcp/tools` and execute them through the REST API v1 (`/api/v1/...`). ## Live Tool Discovery Authenticated clients read the live catalog instead of trusting these docs for what a token can do: ```bash curl -H 'Authorization: Bearer $WALWARDEN_API_KEY' \ '$WALWARDEN_BASE_URL/api/v1/mcp/tools' ``` This is a plain JSON catalog (a `GET`, not an MCP `tools/list` call). The dashboard tRPC surface exposes the same catalog via `capability.mcpTools` for the web client. ## Agent-Scoped Token Setup Create an agent-scoped API token from the dashboard Settings · API tokens page or the walwarden CLI with clientKind=mcp_external, an explicit TTL, org binding, and the capability scopes for the tools you intend to call. Send it as Authorization: Bearer <token>. Tokens are least-privilege, revocable, and audited per #257; destructive or human-gated tools return a confirmation challenge instead of executing directly. Tokens are least-privilege, listable, and revocable from the dashboard or CLI; every call is attributed in the audit chain. ## Connecting A Client Native `mcpServers.url` config blocks for Claude Desktop (`claude_desktop_config.json`), Cursor (`.cursor/mcp.json`), and ChatGPT-compatible desktop clients are intentionally omitted: they require the native MCP transport described under Transport Status, which has not shipped. Adding one now would point a client at an endpoint that cannot handshake. For every supported client (claude-desktop, cursor, chatgpt-desktop, generic-mcp), integrate over the HTTP surface today: 1. Mint an agent-scoped token (see Agent-Scoped Token Setup) and send it as `Authorization: Bearer $WALWARDEN_API_KEY`. 2. Discover available tools from `GET $WALWARDEN_BASE_URL/api/v1/mcp/tools`. 3. Execute a tool by calling its REST API v1 route (see the API reference). Tools whose route scope is `none` in the table below are advisory or not yet exposed as a live route; do not invoke them. ## Callable Tools | Tool | Risk | Capability scope | Route scope | Human gate | |---|---|---|---|---| | `walwarden.audit.inspect` | `read_only` | `capability:audit.inspect` | none | `none` | | `walwarden.backup.trigger` | `safe_write` | `capability:backup.trigger` | `backups:trigger` | `none` | | `walwarden.destination.preflight` | `sensitive_write` | `capability:destination.preflight` | none | `none` | | `walwarden.evidence.export` | `sensitive_write` | `capability:evidence.export` | none | `confirmation_required` | | `walwarden.notification.check` | `read_only` | `capability:notification.check` | none | `none` | | `walwarden.restore.progress` | `read_only` | `capability:restore.progress` | none | `none` | | `walwarden.worker.check` | `read_only` | `capability:worker.check` | none | `none` | Tools with a human gate other than `none` return a confirmation challenge instead of executing directly. An agent must surface that challenge to a human before retrying. ## Advisory (Contract-Only) Tools These tools describe diagnosis/intelligence capabilities an agent can reason about, but they are not live-callable routes yet. Do not claim a live diagnosis/remediation action through them. | Tool | Summary | |---|---| | `walwarden.diagnostics.diagnose` | Return typed health findings and safe next capabilities for a database, destination, worker, or backup job. | | `walwarden.environment.verify` | Check whether the current client has the tools, auth, and network reachability needed for an operation. | | `walwarden.errors.explain` | Turn a stable error code and debug evidence into safe next actions and human-readable context. | | `walwarden.recovery.disable` | Close a future logical recovery window with an explicit close reason. | | `walwarden.recovery.enable` | Create a future logical recovery window anchored by a full baseline snapshot before transaction tailing begins. | | `walwarden.recovery.preflight` | Return contract-only source and destination eligibility metadata for a future logical recovery window. | | `walwarden.recovery.restore` | Future restore flow that replays a full baseline snapshot plus transaction segments to an explicit target. | | `walwarden.recovery.status` | Read future logical recovery window state, schema digest, replay cursor, and proof metadata. | ## Forbidden Claims - Do not claim a native MCP server transport or a working `mcpServers.url` endpoint; none has shipped. Integrate over `GET /api/v1/mcp/tools` plus the REST API v1. - Do not expose or invoke a tool that is not in the live `/api/v1/mcp/tools` catalog. - Do not execute a human-gated or destructive tool without surfacing the confirmation challenge to a human. - Do not treat this page as the authority for live actions; the live capability registry is authoritative for authenticated clients. - Do not claim PITR, continuous backup, or unattended restore drills through any tool. - Do not claim recoverability from backup completion alone; check restore/evidence semantics first. # CI recipes (reference/ci-recipes) These recipes use only the current public CLI/SDK alpha surface. They assume your CI job can create and destroy its own ephemeral Postgres database. ## Backup Before Migration ```bash set -euo pipefail backup_json="$(walwarden --json backup trigger --database "$WALWARDEN_DATABASE_ID" --wait)" backup_id="$(printf %s "$backup_json" | jq -r '.data.completed.id // .data.triggered.backupJobId')" walwarden --json backup status "$backup_id" > backup-status.json jq -e '.data.state == "completed" and (.data.artifact.checksumSha256 | test("^[a-f0-9]{64}$"))' backup-status.json ``` ## Restore To Ephemeral Database Public REST restore creation is available for CLI-local execution with API keys. The raw target DSN must stay on the CI runner; the API receives only a redacted target identity. `restore execute` is the proven end-to-end local execution bridge, verified against a live disposable target (E2E run `e2e-20260607T1941-8beaa08`, audit chain reaching `restore.completed`; evidence in #320). ```bash createdb "$EPHEMERAL_DATABASE_NAME" WALWARDEN_TOKEN="$WALWARDEN_RESTORE_TOKEN" walwarden restore \ --manifest "$WALWARDEN_RESTORE_MANIFEST_SHA256" \ --target "$EPHEMERAL_DATABASE_URL" \ --mode new_database \ --json ``` ## Verify Evidence Before Success ```bash walwarden --json evidence list --database "$WALWARDEN_DATABASE_ID" > evidence.json jq -e --arg backup_id "$backup_id" ' .data.evidence[] | select(.backupJobId == $backup_id) | .integrityVerification.result == "passed" ' evidence.json ``` A completed backup job without passed integrity evidence is not a successful CI gate. ## Teardown ```bash dropdb --if-exists "$EPHEMERAL_DATABASE_NAME" rm -f backup-status.json evidence.json ``` # Exit codes (reference/exit-codes) The walwarden CLI uses structured exit codes so scripts and CI pipelines can distinguish actionable failures. | Code | Name | Meaning | |---|---|---| | `0` | `OK` | Restore completed successfully. The `completed` state was reached and the audit chain was sealed. | | `1` | `RETRYABLE` | A transient failure occurred. The restore job transitioned to `failed_retryable`. Common causes: network error during S3 download, transient S3 throttle. Retrying the restore (by issuing a new token) is reasonable. | | `2` | `USER_ERROR` | Bad arguments or a rejected token. The CLI exited before creating a restore job. Common causes: missing `--manifest`, `--target`, or `--mode`; `--confirm-destructive` absent for in-place mode; `WALWARDEN_TOKEN` not set; token expired or already used; target database unreachable on pre-flight. Fix the input and reissue the token before retrying. | | `3` | `TERMINAL` | A non-retryable failure. The restore job transitioned to `failed_terminal`. Common causes: manifest hash mismatch (the artifact may be corrupt), `pg_restore` exited with a non-zero code, malformed dump. Do not retry without investigating. Contact support with the restore job ID from the dashboard. | | `4` | `CANCELLED` | The restore was interrupted by a signal. The CLI received SIGTERM or SIGINT (Ctrl-C), or the parent process was killed. The server watchdog will eventually transition the restore job to `timed_out`. Issue a new token and restart to complete the restore. | ## Using exit codes in scripts ```bash WALWARDEN_TOKEN= npx --yes walwarden-cli restore \ --manifest \ --target '' \ --mode new_database EXIT=$? if [ $EXIT -eq 0 ]; then echo "Restore completed" elif [ $EXIT -eq 1 ]; then echo "Transient failure — retry with a new token" elif [ $EXIT -eq 2 ]; then echo "Bad input or expired token — check flags and reissue" elif [ $EXIT -eq 3 ]; then echo "Terminal failure — do not retry without investigating" elif [ $EXIT -eq 4 ]; then echo "Cancelled — restart when ready" fi ``` # Honest capability claims (reference/honest-capability-claims) This page is the canonical source for what walwarden can claim. Marketing copy, pitch decks, and technical writeups must not exceed these claims. The design commitment is copy-equals-code parity. Update this page alongside each capability landing. Do not add a claim before the behavior is shipped and tested. ## Shipping (as of 2026-06-23) | Capability | Notes | |---|---| | Scheduled logical backup (`pg_dump`) to BYO S3 | Neon and Supabase-compatible Postgres. Worker streams bytes direct to customer-owned S3. | | Ed25519-signed manifests and audit chain | Every artifact is signed at write time. Verifiable offline via `@walwarden/verifier` without trusting the walwarden dashboard. | | Dashboard backup history and RPO-at-a-glance | Shows last backup time, artifact size, manifest hash, and loss window at a glance. | | Operator-driven restore via `walwarden-cli` | Dashboard issues a short-lived token; CLI runs `pg_restore` on the customer's machine; full audit chain written for every state transition. | | Live restore progress on dashboard (SSE) | State transitions stream to the dashboard in real time via the audit event channel. | | Evidence bundle export | Downloadable bundle including the signed manifest and full audit event chain. Verifiable offline. | | Notification routes | Per-event delivery routes (Slack, Discord, email, HMAC-signed webhook) for failed backup, failed restore drill, audit-chain anomaly, and worker-not-running. Admin-managed, test-sendable, with bounded retries and per-route rate limiting. See [configure routes](/docs/guides/notifications/configure-routes). | | Logical recovery proof lane | `npm run proof:logical-recovery` proves baseline snapshot, transaction-tail replay, schema-change resnapshot, gap fail-closed behavior, unsupported-shape rejection, and redacted live-evidence schema. This is proof infrastructure, not a customer-facing recovery-window claim. | ## Not yet shipped | Capability | Status | |---|---| | Automated restore drill (cron-driven, ephemeral target) | Deferred. Operator-driven restore only. | | Continuous PITR / WAL streaming | Deferred. Scheduled logical backups only. | | Customer-facing logical recovery windows | Planned. The proof lane exists, but live disposable Neon/Supabase evidence and product enablement must land before customer claims change. | | Automated restore verification (unattended drill) | Deferred. | Do not use the following phrases in documentation, marketing copy, or customer communications until the corresponding capability has shipped: - "Automated restore drill" or "automated restore verification" - "PITR" or "point-in-time recovery" - "continuous backup" - "unattended restore" - "logical recovery window" as a shipping customer feature, unless the copy explicitly says it is planned or proof-only ## Trust boundary claims (always true) These are architectural invariants, not features. They hold for every restore. - Walwarden never holds the customer's target-DB write credentials. - Walwarden never proxies or stores the dump bytes. The CLI pulls directly from S3 via a short-lived presigned URL. - The restore audit chain records every state transition. Customers see the full evidence trail in the dashboard. - The signed manifest can be verified offline without trusting walwarden's servers. ## Source documents These claims are derived from and must remain consistent with: - `CLAUDE.md` (repo root) — Shipping capabilities table - `docs/roadmap/walwarden-prd.md` — PRD Shipping table - `docs/runbooks/cli-restore-live-e2e.md` — Honest claims after a successful run - `docs/runbooks/logical-recovery-proof.md` — Logical recovery proof lane and live-evidence handoff # Trust boundary (reference/trust-boundary) ## The invariant The restore trust boundary is the load-bearing architectural invariant of the product. It is enforced by the system design, not by policy. **What walwarden holds:** - Backup job state and scheduling configuration (stored in walwarden's Postgres metadata database) - Ed25519-signed backup manifests (checksums, sizes, Postgres version metadata — not the dump bytes) - The audit event chain (state transitions and timestamps — not data content) - Short-lived HMAC-signed restore tokens (signed with a secret held by the walwarden control plane, valid for 1 hour, single-use) - Presigned S3 URLs (valid for a bounded window, scoped to a single artifact) - The walwarden-to-S3 IAM role credentials (used to write backup artifacts and to generate presigned read URLs for restores) **What walwarden never holds:** - The customer's target-DB write credentials. The `--target` DSN passed to the CLI is processed entirely on the customer's machine and is not transmitted to walwarden's servers. - The dump bytes. The backup worker writes bytes from `pg_dump` directly to the customer's S3 bucket. The restore CLI downloads bytes from the customer's S3 bucket via presigned URL directly to the restore machine. Walwarden's servers are not in the data path. - The customer's raw database rows. Walwarden never reads the data it backs up beyond what `pg_dump` produces; `pg_dump` output is treated as opaque bytes. - The customer's AWS root credentials or any credentials beyond the scoped IAM role the customer creates. ## How the architecture enforces it ### Backup path ``` pg_dump (on walwarden worker) → streams bytes to customer S3 bucket via walwarden's assumed IAM role → walwarden worker never holds bytes in memory beyond a streaming buffer → manifest written to S3, signed with walwarden's Ed25519 key → audit event written to walwarden's metadata DB (hash + size only, not bytes) ``` The customer's S3 bucket is the primary storage. Walwarden's metadata DB holds the manifest, not the asset. ### Restore path ``` Customer clicks "Restore from this backup" in dashboard → walwarden control plane issues a short-lived HMAC token + presigned S3 URL → customer copies one-liner to their machine → CLI on customer machine calls triggerRestore (token auth only) → CLI downloads dump from S3 via presigned URL (no walwarden proxy) → CLI pipes bytes to pg_restore on customer machine → CLI posts state transitions to walwarden (state labels only, no data) → walwarden writes audit events ``` The presigned URL gives the CLI direct S3 read access, scoped to the specific artifact and bounded in time. No walwarden proxy is involved. ### Token design Restore tokens are HMAC-signed by the walwarden control plane. The payload contains: - `orgId` — the team that owns the database - `databaseId` — the specific protected database - `manifestHash` — the SHA256 of the artifact being restored - `mode` — `new_database` or `in_place` - `jobIntentId` — a unique identifier that makes the token single-use - `exp` — expiration timestamp (1 hour from issuance) The server verifies the HMAC signature on every API call. A token cannot be forged without the HMAC secret. Once `triggerRestore` is called, the `jobIntentId` is marked as used; subsequent calls with the same token are rejected. ## What this means for your security posture - A leak of the AWS destination role or restore-token signing path does not expose target database credentials. For AWS BYO S3, the external-ID-protected destination role is backup-and-restore capable: while walwarden holds valid assumed-role credentials, it can read walwarden backup artifacts in the configured bucket scope to issue restore downloads. The restore token still controls customer-initiated CLI restores, and the role must stay scoped to the dedicated backup bucket with Object Lock, TLS-only access, public-access block, and the external-ID trust policy enabled. - A walwarden outage does not affect your existing backups. Your backup artifacts are in your S3 bucket under your IAM account. If walwarden is unavailable, you can restore directly from S3 using `pg_restore` with your own AWS credentials, bypassing the CLI entirely. - Your target database credentials are not in walwarden's threat model. They never leave your restore machine. ## Source This section is derived from `docs/decompositions/2026-05-26-walwarden-cli-restore-design.md` (Trust boundaries section). If the architecture changes, update that document and this page together. # Explanation (concepts/index) These pages explain *why* walwarden works the way it does. They are for the careful engineer and the compliance reviewer who want to understand the trust model before relying on it. For step-by-step tasks, see the [guides](/docs/guides); for lookups, see the [reference](/docs/reference). ## The independence wedge Walwarden exists to give you backups that do not depend on the database provider you do not fully trust. The product is built around four invariants: 1. **Independent destination ownership.** Backup bytes land in an S3 bucket under your IAM role. Walwarden holds the schedule and the evidence, not your data. 2. **Verifiable artifacts.** Every backup is an Ed25519-signed manifest you can verify offline. 3. **Auditable evidence.** Every state transition is recorded in an append-only audit chain you can export. 4. **Honest recoverability claims.** A completed backup is not the same as a proven restore. We say so plainly. ## Read next - [Agent-native workflow](/docs/concepts/agent-native-workflow) — how a coding agent discovers the surface and runs the backup → restore-drill → evidence loop under scoped credentials - [Trust boundary](/docs/concepts/trust-boundary) — what walwarden holds and what stays on your side - [The audit chain](/docs/concepts/audit-chain) — what the append-only event log records and how to verify it - [Recoverability and RPO](/docs/concepts/recoverability-and-rpo) — why backup-complete is not yet proven-recoverable - [What is not shipped](/docs/concepts/what-is-not-shipped) — the honest boundary of what the product does today # Trust boundary (concepts/trust-boundary) The trust boundary is the load-bearing invariant of walwarden. It is what makes the backups *independent* rather than just another copy held by a vendor you would have to trust. ## The short version - Walwarden never holds your target-DB write credentials. - Walwarden never proxies or stores the dump bytes. The CLI pulls directly from your S3 bucket via a short-lived presigned URL. - Backup bytes land in a bucket under your IAM role. Walwarden holds the schedule, the signed manifest, and the audit chain — not your data. - Every restore state transition (token issued, claimed, downloading, verifying, restoring, completed or failed) is recorded in the audit chain. The payloads carry state labels and timestamps, never data content. ## Why this matters If walwarden held your write credentials and proxied your dump bytes, a compromise of walwarden would be a compromise of your data and your recovery path. The boundary is enforced by architecture — presigned URLs, customer-owned roles, CLI-local restore — not by policy. ## Canonical reference The full architectural description of the trust boundary, including the AssumeRole path and per-provider credential handling, lives in the reference: - [Trust boundary reference](/docs/reference/trust-boundary) - [Destinations overview](/docs/guides/destinations) — how each provider's auth model affects the boundary # The audit chain (concepts/audit-chain) ## What the audit chain records Walwarden appends an audit event for every state transition in every job. Events are append-only — no event is modified after it is written. Each event includes: - **kind** — the event type, for example `backup.completed` or `restore.downloading` - **seq** — a monotonically increasing sequence number within the job - **at** — ISO 8601 timestamp with sub-second precision - **job_id** — the ID of the backup or restore job that produced the event - **payload** — event-specific metadata (manifest hash, byte count, error classification, etc.) ### Backup events (in order) | Kind | Meaning | |---|---| | `backup.queued` | Scheduler enqueued the job | | `backup.claimed` | Worker claimed the job | | `backup.running` | `pg_dump` subprocess started | | `backup.finalizing` | Dump uploaded; manifest written; verification running | | `backup.completed` | Manifest verified; artifact sealed | | `backup.failed` | Job failed; payload includes error classification | ### Restore events (in order) | Kind | Meaning | |---|---| | `restore.token_issued` | Dashboard issued a restore token | | `restore.triggered` | CLI called triggerRestore; restore_job row created | | `restore.claimed` | CLI claimed the restore job | | `restore.downloading` | CLI started downloading the dump from S3 | | `restore.verifying` | Dump fully downloaded; checksum verification running | | `restore.manifest_verified` | Checksum matched; manifest confirmed | | `restore.restoring` | pg_restore subprocess started | | `restore.finalizing` | pg_restore completed; cleanup in progress | | `restore.completed` | Restore job sealed | | `restore.failed` | Job failed; payload includes `retryable` flag and error classification | | `restore.timed_out` | Server watchdog detected an inactive job and sealed it | | `restore.token_rejected` | A token was presented but rejected; payload includes reason | ## Dashboard view The audit chain is surfaced in two places: - **Job timeline** — on the backup or restore job detail page, every event in the chain is shown with its timestamp and payload. - **Evidence bundle** — downloadable from the database detail page. Includes the signed manifest and every audit event for a given backup artifact, in a format the `@walwarden/verifier` package can verify offline. ## Offline verification You can verify a backup artifact offline without trusting the walwarden dashboard: ```bash # Download walwarden's public Ed25519 verification key curl -O https://walwarden.com/.well-known/walwarden-pubkey.pem # Download the evidence bundle from the dashboard # Then verify: npx @walwarden/verifier \ --bundle evidence-bundle.tgz \ --pubkey walwarden-pubkey.pem ``` A successful verification prints: ``` OK: N manifests verified, M audit events chain-intact (exit 0) ``` The verifier confirms: - The Ed25519 signature over the manifest is valid against walwarden's published public key - The audit event sequence numbers are contiguous with no gaps - The manifest hash in the audit events matches the artifact on disk The verifier is a zero-runtime-dependency npm package. It does not contact walwarden's servers. An auditor can run it on an air-gapped machine given only the evidence bundle and the public key. # Recoverability and RPO (concepts/recoverability-and-rpo) Walwarden leads with an uncomfortable truth: **a completed backup is not the same as a proven restore.** This page explains the distinction and what the RPO figure on the dashboard does and does not promise. ## Backup-complete ≠ proven-recoverable When a backup reaches `completed`, walwarden has: - run `pg_dump` against your source database, - streamed the bytes to your S3 bucket, - computed a SHA256 checksum and written an Ed25519-signed manifest, - appended every step to the audit chain. That proves an artifact exists, is signed, and has an intact audit chain. It does **not** prove that the artifact will restore cleanly into a working database. Dump/restore version skew, extension mismatches, or target-side privilege gaps can all surface only at restore time. The only thing that proves recoverability is a restore. That is why we call it a [restore drill](/docs/guides/restore/run-a-restore-drill): you restore a backup to a target you control and confirm the tables and row counts are there. Treat a backup you have never restored from as unproven. ## What RPO means here RPO (Recovery Point Objective) is the amount of data you could lose, measured as the age of your most recent recoverable backup. On the dashboard, the RPO figure is derived from the time since the last `completed` backup and rendered as an elapsed loss window: `12m 00s` while it is under an hour, rolling over to `8h 36m` and then `1d 4h` as it ages. A loss window of `12m 00s` means the most recent backup completed twelve minutes ago — so a failure now would lose at most the writes since then. The figure reads green while it sits inside the interval walwarden derives from your backup schedule and turns amber once it drifts past that interval. For a card-by-card walkthrough of the dashboard, see [Read your recovery posture](/docs/guides/operate/read-your-dashboard). RPO here is a property of **scheduled logical backups**. It is a backup-recency figure, not a continuous-protection guarantee. Walwarden does not stream every write in real time; it backs up on the schedule you set. Choose a schedule whose interval matches the data loss you can tolerate. ## The honest boundary For the full list of what the product does and does not do today, see [What is not shipped](/docs/concepts/what-is-not-shipped) and the [honest capability claims](/docs/reference/honest-capability-claims) reference. # What is not shipped (concepts/what-is-not-shipped) Honesty about limits is part of the product, not a footnote. This page states plainly what walwarden does **not** do today. For the exact, code-synced claim table, see the [honest capability claims](/docs/reference/honest-capability-claims) reference. ## What ships today - Scheduled logical backups (`pg_dump`) to a BYO S3 bucket, for Neon and Supabase Postgres. - Ed25519-signed manifests and an append-only audit chain, verifiable offline. - Dashboard backup history and an RPO figure. - Operator-initiated restore via the CLI, on a machine you control. - Evidence bundle export. - Notification routes that page Slack, Discord, email, or a signed webhook when a backup fails, a restore drill cannot recover, the audit chain breaks, or the worker stops. See [configure notification routes](/docs/guides/notifications/configure-routes). ## Not yet shipped The following are **on the roadmap and not yet available.** Do not rely on them, and do not describe them as if they work today. | Capability | Status | |---|---| | Automated restore drill (cron-driven, ephemeral target) | On the roadmap — not yet available. Restore is operator-initiated only. | | Continuous PITR / WAL streaming | On the roadmap — not yet available. Backups are scheduled logical dumps. | | Automated (unattended) restore verification | On the roadmap — not yet available. | | AWS RDS / Aurora as a connected provider | On the roadmap — not yet available. Today's shipping providers are Supabase and Neon. | A completed backup is not the same as a proven restore, and scheduled backups are not continuous protection. Stating the boundary is how a compliance reviewer can trust the claims we *do* make. See [Recoverability and RPO](/docs/concepts/recoverability-and-rpo). Roadmap items and the dates they change status live in the [changelog](/docs/changelog). # Changelog (changelog/index) What changed, newest first. Each entry is dated. Roadmap items are kept in a clearly separated block and are explicitly labeled not yet shipped. ## 2026-06 - **Documentation IA refactored to a task-first structure.** Docs are now organized as Start here, Guides, Reference, Explanation, and Changelog, with dual CLI-first and agent-assisted quickstarts. ## 2026-05 - **Operator-initiated restore via the CLI shipped.** The dashboard issues a short-lived token; the CLI runs `pg_restore` on a machine you control, with the full audit chain recorded. - **Evidence bundle export shipped.** Download a verifiable bundle (signed manifest + audit events) and verify it offline with `@walwarden/verifier`. - **Dashboard backup history and RPO-at-a-glance shipped.** - **Scheduled logical backups (`pg_dump`) to BYO S3 shipped** for Neon and Supabase Postgres, with Ed25519-signed manifests and an append-only audit chain. --- ## Not yet shipped (roadmap) These items are **on the roadmap and not yet available.** They are listed here for transparency only; nothing below works in the product today. | Item | Status | |---|---| | AWS RDS / Aurora as a connected provider | On the roadmap — not yet available. Shipping providers today are Supabase and Neon. | | Automated restore drill (cron-driven, ephemeral target) | On the roadmap — not yet available. | | Continuous PITR / WAL streaming | On the roadmap — not yet available. | | Automated (unattended) restore verification | On the roadmap — not yet available. | | Customer-managed encryption keys (BYOK) | On the roadmap — not yet available. | For why these limits exist, see [What is not shipped](/docs/concepts/what-is-not-shipped). # Agent-native workflow (concepts/agent-native-workflow) Walwarden is built to be driven by a coding agent — Claude Code, Codex, Cursor, or anything that can run a CLI and read JSON — without handing it credentials it should not hold or letting it claim more than the evidence proves. This page explains *how* the agent-native path fits together. For the copy-paste commands, jump to the [agent-assisted quickstart](/docs/getting-started/quickstart-agent). ## How an agent discovers the surface Everything an agent needs to use walwarden safely is machine-readable and published. An agent does not need to scrape the dashboard. | Surface | What it is | Where | |---|---|---| | Skill artifact | The `@walwarden/agent-skills` package — a `SKILL.md` plus a `surface.json` manifest describing supported commands, scopes, capability status, and forbidden claims. Generated from source, so it never drifts from the shipping CLI/SDK. | [`@walwarden/agent-skills`](https://www.npmjs.com/package/@walwarden/agent-skills) | | `llms.txt` | The agent entrypoint at the site root. Links every doc page and the machine-readable artifacts below. | [`/llms.txt`](https://docs.walwarden.com/llms.txt), [`/llms-full.txt`](https://docs.walwarden.com/llms-full.txt) | | OpenAPI contract | The REST API v1 alpha contract — operations, scopes, and request/response schemas. | [`/openapi/walwarden.v1.json`](https://docs.walwarden.com/openapi/walwarden.v1.json) | | SDK and CLI | The dependency-free TypeScript client and the `walwarden` CLI, both generated against the same contract. | [SDK](/docs/reference/sdk), [CLI](/docs/reference/cli) | The skill artifact is the load-bearing piece: it is regenerated from `packages/core` on every release, so the commands, scopes, and "do not claim" rules an agent reads are the ones the API actually enforces. A walwarden MCP server is on the roadmap and **not yet available**. The supported agent surfaces today are the skill artifact, the SDK, and the CLI, all driven by a scoped API key. Do not configure an agent against an MCP endpoint that does not exist yet. ## Scoped credentials, not a raw credential handoff The agent never needs — and must never be given — your AWS keys, your database superuser password, or your dashboard session. It operates entirely through a **scoped API key**. - You mint an API key in the dashboard with the **minimum scopes** for the loop you want. The backup-and-evidence loop needs only `databases:read`, `destinations:read`, `backups:trigger`, and `evidence:read`. Add `restores:write` and `restores:read` only when you want the agent to drive a restore drill. - A request with a key that lacks a scope returns `403` with the exact `requiredScope` in the error body — so the agent can ask for the narrow missing scope rather than escalating to a broad key. See [API auth and scopes](/docs/reference/api-auth-scopes). - The key authorizes **inspection and backup-trigger** work. It does **not** carry your target-database write credentials. A restore still runs on a machine you control, with a target DSN that stays on that machine and is never transmitted to walwarden. See the [trust boundary](/docs/concepts/trust-boundary). The agent treats the API key like any other secret: read it from the environment or your approved secret store, never print it, never commit it. ## Observable action history maps to evidence and audit Because the agent works through the scoped API, every action it takes leaves the same auditable trail a human operator would. - A triggered backup produces a **signed manifest** (Ed25519 over the artifact checksum and metadata) recorded in the append-only [audit chain](/docs/concepts/audit-chain). The agent reads it back with `evidence list`. - A restore drill records every state transition — token issued, claimed, downloading, verifying, restoring, completed or failed — in the same chain. - The agent exports an [evidence bundle](/docs/guides/evidence/produce-an-evidence-bundle): the signed manifest plus the full audit event chain, the artifact a human or auditor reviews after the fact. This is the difference between an agent *asserting* it did something and the system *proving* it: the agent's tool calls converge on evidence outputs that exist independent of the agent's own report. An agent must not mark a backup or restore drill successful until it has read the command result **and** the evidence metadata. A completed job proves a job completed; it is not the same as proven recoverability. This rule is restated, with the exact forbidden claims, in the [agent integration recipes](/docs/reference/agent-integration). ## Start in a sandbox, against disposable data Point the agent at **test data first.** The whole loop — connect, back up, restore-drill, verify — works against a throwaway database, and that is where an agent should prove it before it touches anything you care about. 1. Stand up a disposable Postgres database with non-sensitive seed data: a fresh Neon or Supabase project, or a local cluster the agent can reach. This is the source. 2. Use a **separate, empty target** for the restore drill — `new_database` mode creates a fresh database on the target cluster, so a drill against test data never overwrites anything. See [restore modes](/docs/guides/restore/modes). 3. Mint a scoped API key bound to only that test database, so even a misbehaving agent cannot touch production. Running the loop against disposable data is the safe way to confirm the agent reports honestly — that it checks evidence, respects scopes, and refuses claims the surface does not support — before you trust it with a real database. ## Hello-world: backup → restore drill → evidence bundle The minimal end-to-end an agent should run once, against test data, to prove the loop works: 1. **Back up.** Trigger an ad-hoc backup and wait for a terminal state, then read the signed manifest it produced. Commands: [agent-assisted quickstart](/docs/getting-started/quickstart-agent). 2. **Restore-drill.** Restore that backup into the empty test target in `new_database` mode and confirm it lands. Walkthrough: [run a restore drill](/docs/guides/restore/run-a-restore-drill). 3. **Evidence bundle.** Export the bundle — signed manifest plus audit chain — and confirm the integrity evidence passed. Guide: [produce an evidence bundle](/docs/guides/evidence/produce-an-evidence-bundle). A backup you have never restored from is a backup you have not yet proven recoverable. The drill is what turns step 1 into a claim you can stand behind — and the evidence bundle is the artifact that records it. ## Read next - [Quickstart: agent-assisted backup](/docs/getting-started/quickstart-agent) — the copy-paste CLI and SDK commands. - [Agent integration recipes](/docs/reference/agent-integration) — the safe CLI pattern and the exact forbidden claims. - [Trust boundary](/docs/concepts/trust-boundary) — what walwarden holds and what stays on your side. - [What is not shipped](/docs/concepts/what-is-not-shipped) — the honest boundary, including the roadmap MCP server. # Invite teammates & roles (guides/team/invite-teammates) This guide: invite teammates into your organization, give them the right role, and manage the membership list. Multi-seat membership is part of the **Team** plan — Free and Pro are single-operator. See [Plans & billing](/docs/guides/team/plans-and-billing) for what each plan unlocks. ## Prerequisites - A **Team** subscription (or an active [temporary full-plan grant](/docs/guides/team/temporary-full-plan-grant) for Team). The Team plan covers up to 10 team members. - An **Admin** (or Owner) role in the organization. Only admins can invite, remove, or change roles. ## Roles Walwarden uses two roles you can assign, plus the Owner that the account creator holds: | Role | Can manage members, billing, API tokens, grants | Can connect databases, run backups, restore, read evidence | |---|---|---| | **Admin** (or Owner) | Yes | Yes | | **Member** | No — these surfaces are read-only or hidden | Yes | A Member sees the same operational dashboard — databases, backups, restores, evidence — but the administrative actions (inviting teammates, changing roles, editing billing, issuing API tokens, creating grants) are hidden. The organization must always keep at least one Admin: walwarden refuses to remove or demote the last remaining admin. ## Step 1: Open the Members page In the dashboard, go to **Members** (`/o//members`). The table lists every active and pending teammate with their **Email**, **Role**, **Joined** time, and **Last active** time. If you do not see the invite controls, you are signed in as a Member, not an Admin. ## Step 2: Invite a teammate 1. Click **Invite teammate**. 2. Enter the teammate's **email address**. 3. Choose a **role** — **Member** (default) or **Admin**. 4. Send the invite. The teammate receives an invitation email and appears in the table as a **pending** row until they accept. Inviting someone who already has a membership in the organization is rejected as a duplicate. Every invite is recorded in the [audit chain](/docs/concepts/audit-chain) as a `member.invited` event. A disabled invite button with a tooltip means managed invitations are not activated for this deployment. The role model and the rest of this guide still apply; contact support to enable email invitations. ## Step 3: Manage roles and removals From the same table, an Admin can: - **Change a role** — promote a Member to Admin or demote an Admin to Member. Walwarden refuses to demote the last admin. Recorded as `member.role_changed`. - **Remove a member** — revoke an active teammate's access. Walwarden refuses to remove the last admin. Recorded as `member.removed`. - **Revoke a pending invite** — withdraw an invitation before it is accepted. Recorded as `member.invite_revoked`. ## Verify it worked The Members table shows the new teammate (pending, then active once they accept) with the role you assigned. Every membership change is in the audit chain, so you can prove who was granted or removed access and when — the same evidence trail auditors expect for access control. ## Related - [Plans & billing](/docs/guides/team/plans-and-billing) — what Team unlocks over Pro - [Share a temporary full-plan grant](/docs/guides/team/temporary-full-plan-grant) — give someone time-boxed full-plan access without a subscription - [Issue a dashboard API token](/docs/reference/issue-api-tokens) — mint scoped tokens for automation (Admin-only) # Plans & billing (guides/team/plans-and-billing) This guide: understand what each plan unlocks, how to upgrade, and where your subscription state lives. Billing is read in the dashboard under **Settings → Billing**; the paid subscription record itself lives in Stripe. ## What each plan unlocks | | **Free** | **Pro — $29/mo** | **Team — $199/mo** | |---|---|---|---| | Protected Postgres databases | 1 | Multiple | Up to 25 | | Scheduled logical backups to your own S3 | Yes | Yes | Yes | | Ed25519-signed manifests + offline-verifiable audit chain | Yes | Yes | Yes | | Operator-run restore via the CLI | Yes | Yes | Yes | | Backup-failure alerting | — | Yes | Yes | | Evidence-bundle export for auditors | — | Yes | Yes | | Team members with roles | — | — | Up to 10 | | Dashboard API tokens for automation | — | — | Yes | | Audit-chain retention | Standard | Standard | 13 months — covers a full SOC 2 Type II observation window | | Support | Community (GitHub) | Email | Priority, one-business-day response SLA | Every plan runs the same backup engine: scheduled logical backups, signed manifests, and an offline-verifiable audit chain into storage you own. Higher plans add multi-database scale, alerting, evidence export, and — at Team — multi-seat membership and API tokens. The Team plan is the multi-seat tier: it is what unlocks [inviting teammates with roles](/docs/guides/team/invite-teammates) and [issuing dashboard API tokens](/docs/reference/issue-api-tokens). ## How billing works Walwarden never holds a Stripe API key. Stripe is the source of truth for the paid subscription, and a signed webhook syncs the resulting state back into walwarden. That means: - **Upgrades** open a Stripe-hosted Payment Link in your browser, pre-filled with this team's reference and your admin email so the subscription attaches to your organization. - **Cancellation and invoice history** are managed inside Stripe's hosted flow. - Your dashboard shows a **plan label** and a coarse **subscription status** badge synced from Stripe — it does not store card details. ## Step 1: Read your current plan In the dashboard, go to **Settings → Billing** (`/o//settings/billing`). This page is Admin-only. It shows: - **Current plan** — your plan label (Free, Pro, or Team). - **Subscription status** — a coarse status badge synced from Stripe. - A **Team reference** — a quiet support/Stripe-correlation handle, useful when contacting support. If your full-plan access comes from a [temporary grant](/docs/guides/team/temporary-full-plan-grant) rather than a paid subscription, the page shows a notice like *Full plan via invite grant until …* with the grant's expiry. ## Step 2: Upgrade On the Billing page, use the upgrade actions: - **Upgrade to Team — $199 / month** (the conversion target, shown first) - **Upgrade to Pro — $29 / month** Each opens a Stripe Payment Link in the same tab. Complete checkout in Stripe; the signed webhook syncs your new plan and status back to the dashboard. ## Verify it worked After checkout, the **Current plan** label and **Subscription status** on the Billing page reflect the new plan once Stripe's webhook has synced. Team-only surfaces — the Members page and Settings → API tokens — become available to admins. ## Related - [Invite teammates & roles](/docs/guides/team/invite-teammates) — multi-seat membership (Team) - [Share a temporary full-plan grant](/docs/guides/team/temporary-full-plan-grant) — time-boxed full-plan access without a subscription - [Issue a dashboard API token](/docs/reference/issue-api-tokens) — scoped tokens for automation (Team, Admin-only) # Share a temporary full-plan grant (guides/team/temporary-full-plan-grant) This guide: use the **Temporary full-plan grants** panel to hand someone time-boxed Pro or Team access without putting them on a paid subscription. This is the design-partner fast-onboarding path — a renewable invite link that turns on the full plan for a fixed window. It documents the existing grant flow; it does not invent new infrastructure. A grant is separate from billing: Stripe remains the source of truth for paid subscriptions, and grants are shown and managed separately so you can turn access on or off immediately. ## When to use this - Onboarding a **design partner** or pilot user who should experience the full product before committing to a subscription. - Giving a teammate or evaluator temporary full-plan access on a deadline. ## Prerequisites - An **Admin** (or Owner) role. The grants panel lives inside Admin-only Billing settings. ## The grant model | Property | Behavior | |---|---| | Window | A fixed window of 90 days per issue or renewal. | | Plan | **Team** (default) or **Pro**. | | Link | A one-time invite link, shown **once** at creation and not recoverable afterward. | | Status | `pending` → `accepted`; or `revoked` / `expired`. | | Renewal | Renewable for another full 90-day window at any time. | | Revocation | Revocable immediately, independent of any Stripe subscription. | Every grant action is recorded so you can prove who was granted temporary access, on what plan, and for how long. ## Step 1: Open the grants panel Go to **Settings → Billing** (`/o//settings/billing`). Scroll to **Temporary full-plan grants**. The panel describes itself as creating renewable 90-day invite links for temporary Pro or Team access, and lists existing grants with their status and local expiry time. ## Step 2: Create an invite link 1. Enter the recipient's **user email**. 2. Choose a **plan** — **Team** (default) or **Pro**. 3. Click **Create link**. Walwarden shows a **Temporary access link created** reveal with the one-time URL. Copy it now — it cannot be shown again: - Use **Copy link** to grab the URL. - The reveal notes when access expires (90 days out) unless renewed. - Click **I have copied this link** to dismiss the reveal. Share the copied link with the recipient (for example by email or your normal secure channel). ## Step 3: The recipient accepts The recipient opens the link, signs in (or signs up) if needed, and accepts. Their organization is switched to the granted plan and lands on the Billing page with the grant active. On their Billing page, the plan reads as *Full plan via invite grant until …* with the expiry date, so the temporary nature is always visible. The grant's status moves from `pending` to `accepted`. ## Step 4: Renew or revoke From the grants table, an Admin can: - **Renew** — extend the grant for another full 90-day window. Use this to keep a design partner active while an evaluation runs long. - **Revoke** — end the grant immediately. Access drops back to the org's underlying plan (which may be Free) right away, regardless of the original expiry. A grant that reaches its expiry without renewal shows as `expired` and stops conferring full-plan access. ## Verify it worked The grants table shows the new link with status `pending` (then `accepted` once the recipient accepts) and its local expiry time. The recipient's Billing page shows the *Full plan via … grant until …* notice. After a revoke, the row shows `revoked` and the recipient loses full-plan surfaces immediately. ## Related - [Plans & billing](/docs/guides/team/plans-and-billing) — what Pro and Team unlock, and how paid subscriptions work - [Invite teammates & roles](/docs/guides/team/invite-teammates) — add seats to an organization that already has the plan # Issue a dashboard API token (reference/issue-api-tokens) This page: issue and scope a dashboard API token. Tokens are bearer credentials that authenticate the [CLI](/docs/reference/cli), [SDK](/docs/reference/sdk), and any external automation against the [REST API v1](/docs/reference/api). For how a token is sent and how scope enforcement behaves, see [API auth and scopes](/docs/reference/api-auth-scopes). API tokens are a **Team**-plan capability and are managed by an **Admin** (or Owner). They live in the dashboard under **Settings → API tokens**. ## Token model | Property | Behavior | |---|---| | Format | `wal_` prefix followed by hex entropy (for example `wal_…`). The prefix makes a token unambiguous in a secret-manager listing. | | Plaintext | Shown **once** in the issue response and never recoverable afterward. The dashboard stores only a SHA-256 fingerprint, never the token. | | Authentication | Sent as `Authorization: Bearer ` against `/api/v1`. | | Scopes | A closed, least-privilege set chosen at issue time. There is no wildcard scope. | | Expiry | Optional. A token can be issued with an expiry or left non-expiring. | | Revocation | Revocable any time. Revoked tokens stay listed for audit history. | Every issue and revoke is recorded in the [audit chain](/docs/concepts/audit-chain) with the issuer, the token name, and the scopes — never the plaintext. ## Step 1: Open the API tokens page In the dashboard, go to **Settings → API tokens** (`/o//settings/api-tokens`). The page lists issued tokens with their name, scopes, creation time, last-used time, and revocation state. If you do not see the management controls, you are not an Admin, or your org is not on the Team plan. ## Step 2: Issue a token 1. Click **Issue token**. 2. Enter a **Token name** that identifies where the token will run (for example `terraform-ci` or `backup-monitor`). The name is visible on this page only. 3. Select the **scopes** the token needs. Grant the minimum required — a missing scope fails closed with `403 forbidden`. See the scope catalog below. 4. Optionally set an **expiry**. 5. Submit. The **token plaintext is shown once** — copy it into your secret manager before dismissing the reveal. It is never recoverable afterward. The reveal is the only time the token plaintext is shown. If you lose it, revoke the token and issue a new one — the dashboard stores only a fingerprint and cannot show it again. ## Step 3: Scope the token Scopes are coarse, org-wide, and least-privilege. Issue a token with exactly the scopes its automation needs: | Scope | Grants | |---|---| | `databases:read` | List and inspect protected database records. | | `databases:write` | Create, update, and delete protected database records. | | `destinations:read` | List and inspect configured backup destinations. | | `destinations:write` | Manage backup destinations and database attachments. | | `backups:trigger` | Start, cancel, and dismiss backup jobs. | | `restores:read` | List and inspect restore jobs and restore-drill state. | | `restores:write` | Start restore jobs and restore drills with explicit targets. | | `evidence:read` | Read backup, restore, and verification evidence artifacts. | | `evidence:export` | Export evidence bundles for external verification. | | `audit:read` | Read audit-chain, verification, and recoverability summaries. | | `readiness:read` | Read service readiness, health, and verification status. | A token with no scopes can still exercise read procedures that are scope-open in v1 (for example a read-only monitoring integration). For the per-endpoint scope matrix and the `401`/`403`/`409` semantics, see [API auth and scopes](/docs/reference/api-auth-scopes). ## Step 4: Use the token Send the token as a bearer credential: ```bash curl -H 'Authorization: Bearer $WALWARDEN_API_KEY' \ '$WALWARDEN_BASE_URL/api/v1/databases' ``` The same token drives the [CLI](/docs/reference/cli) and [`@walwarden/sdk`](/docs/reference/sdk). Mutation endpoints require an `Idempotency-Key` header. ## Step 5: Revoke a token When a token is no longer needed — rotation, an offboarded integration, or a suspected leak — click **Revoke** on its row. Revocation is idempotent and takes effect immediately; subsequent requests with that token fail with `401 unauthorized`. The revoked row stays on the page so the audit history remains complete. ## Related - [API auth and scopes](/docs/reference/api-auth-scopes) — bearer auth, endpoint scope matrix, idempotency - [API reference](/docs/reference/api) — REST API v1 operations - [SDK install and examples](/docs/reference/sdk) · [CLI reference](/docs/reference/cli) - [Invite teammates & roles](/docs/guides/team/invite-teammates) — only Admins manage tokens