> ## Documentation Index
> Fetch the complete documentation index at: https://help.plannotator.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Back Up and Recover Workspaces on Cloudflare

> Back up Cloudflare data, recover D1 metadata, update both Workers, and understand rollback limits.

Back up three parts of a Cloudflare deployment: D1 metadata, each workspace's
Git history, and both R2 buckets. The tested restore rebuilds D1 and reconnects
it to the existing Artifacts and R2 data.

<Warning>
  The tested restore did not rebuild lost Artifacts or R2 resources. Keep the
  Git and R2 backups, but do not treat this procedure as a tested full-account
  restore. A Workspaces API key can clone only the workspaces its owner may
  read, so private workspaces may need separate owner keys.
</Warning>

## Back up the deployment

Use a protected backup directory outside the source checkout. Do not place API
keys, resource IDs, or customer data in the repository or a support request.

### Export D1

From `apps/rooms`, export data without the schema:

```bash theme={null}
CFG=wrangler.self-host.jsonc
./node_modules/.bin/wrangler d1 export workspaces-db \
  --remote --no-schema --output d1-backup.sql -c "$CFG"
```

Keep the source tag, D1 row counts, and export file together. The schema comes
from the source migrations during restore.

### Clone every workspace's Git history

Read the workspace IDs from the D1 export. Clone each one through the public
API origin:

```bash theme={null}
git clone https://backup@api.example.com/git/<workspace-id>.git
```

When Git asks for a password, enter a Workspaces API key. The username is
ignored. Each clone contains that workspace's full version history.

The key owner must have read access to the workspace. One organization member
cannot clone another member's private workspace. Confirm that your backup list
contains every workspace ID from D1.

### Copy both R2 buckets

The usercontent bucket stores text objects at
`text/<workspace-id>/<blob-sha>`. The asset bucket stores objects at
`<workspace-id>/<content-hash>`. Read those values from D1, then fetch each
object:

```bash theme={null}
./node_modules/.bin/wrangler r2 object get <bucket>/<key> \
  --remote --file <backup-path> -c "$CFG"
```

Current `wrangler` has no `r2 object list` command. For many objects, use the
Cloudflare dashboard or an S3-compatible client instead of one command per
key.

## Restore D1 metadata

Do not replay a raw `wrangler d1 export` file. The table order can break
foreign-key checks. Current `wrangler` also has no `d1 import` command.

<Steps>
  <Step title="Create a fresh D1 database">
    ```bash theme={null}
    ./node_modules/.bin/wrangler d1 create workspaces-db
    ```

    Put the new database ID in both Worker configs. Keep the app and
    usercontent `database_name` and `database_id` values equal.
  </Step>

  <Step title="Apply the source migrations">
    From `apps/rooms`, run:

    ```bash theme={null}
    CFG=wrangler.self-host.jsonc
    ./node_modules/.bin/wrangler d1 migrations apply workspaces-db \
      --remote -c "$CFG"
    ```
  </Step>

  <Step title="Prepare the data-only import">
    Keep only `INSERT` statements from the backup. Remove every
    `d1_migrations` row. Order the inserts by table:

    ```text theme={null}
    workspaces
    documents
    api_keys
    assets
    asset_versions
    commits
    annotations
    annotation_replies
    all remaining tables
    ```

    Save the result as `data-only-parent-first.sql` outside the repository.
  </Step>

  <Step title="Import the rows">
    ```bash theme={null}
    ./node_modules/.bin/wrangler d1 execute workspaces-db \
      --remote --file data-only-parent-first.sql -c "$CFG"
    ```
  </Step>

  <Step title="Check row counts">
    ```bash theme={null}
    ./node_modules/.bin/wrangler d1 execute workspaces-db --remote -c "$CFG" \
      --command "SELECT 'documents' t, count(*) n FROM documents UNION ALL SELECT 'commits', count(*) FROM commits UNION ALL SELECT 'annotations', count(*) FROM annotations;"
    ```

    Compare the result with the source backup. `d1 execute --file` can cancel
    at a confirmation prompt while exiting with status `0`, so do not skip this
    check.
  </Step>
</Steps>

Deploy both Workers after the database IDs change. Then open a known workspace,
check its current and older versions, open a comment and reply, make a new edit,
and compare a living raw URL, an exact-version raw URL, and an uploaded asset
with the backup.

## Update both Workers

Back up the deployment before any update that adds a D1 migration.

1. Check out the new named source tag and read its migration notes.
2. Install the locked dependencies and build the browser app.
3. Copy your resource values into the new app and usercontent config templates.
4. Apply pending D1 migrations.
5. Deploy the app Worker and the usercontent Worker from the same source tag.

```bash theme={null}
pnpm install --frozen-lockfile
pnpm build:web:rooms

(
  cd apps/rooms
  ./node_modules/.bin/wrangler d1 migrations apply workspaces-db \
    --remote -c wrangler.self-host.jsonc
  ./node_modules/.bin/wrangler deploy -c wrangler.self-host.jsonc
)

(
  cd apps/usercontent
  ./node_modules/.bin/wrangler deploy -c wrangler.self-host.jsonc
)
```

Check `/healthz` on every origin, sign in through Access, open and edit a known
document, check living and exact-version raw links, and test one API-key request
and Git fetch.

## Roll back safely

Cloudflare can roll a Worker back to a prior registered version. Roll back both
Workers to versions from the same source commit, then repeat the update checks.

```bash theme={null}
(
  cd apps/rooms
  ./node_modules/.bin/wrangler rollback -c wrangler.self-host.jsonc
)

(
  cd apps/usercontent
  ./node_modules/.bin/wrangler rollback -c wrangler.self-host.jsonc
)
```

Each command asks you to choose a prior deployed version. Choose versions that
carry the same source commit.

The recovery drill tested rollback only when both versions used the same D1
schema. It did not test older code against a newer migrated schema. Before a
migrating update, take a D1 backup. If you must return to older code, restore
that pre-update D1 backup instead of running the older code on the changed
database.

*Reviewed July 22, 2026. Maintained by the Plannotator documentation team.*
