feat(backup): account-level backup and restore behind the Backup add-on (BUNYIP-353) #356

Merged
longjacksonle merged 1 commit from feat/BUNYIP-353-account-backup-restore into main 2026-07-10 19:36:24 +02:00

Summary

Backs the seeded "Backup" add-on tile with an account-level Backup and Restore surface (BUNYIP-353). A backup captures the account's Bunyip-side state (owner profile + entitled app slugs) plus a per-app section for each entitled app, as one JSON bundle the admin downloads; restore re-applies the profile, re-grants the listed entitlements, and dispatches each app's bundle to that app, gated on entitlement.

Reality-check that shaped the design

The ticket assumed bunyip relays a backup call to each app backend. It does not: bunyip is the OIDC issuer, apps are relying parties, and the only outbound-to-app path is the fire-and-forget webhook push (which cannot return a bundle). Mokosh's tenant-scoped backup API is also not shipped yet. So per-app backup/restore is expressed behind a pluggable AppBackupAdapter trait: the account orchestration is built and tested now, and the real Mokosh HTTP client drops into MokoshBackupAdapter (currently a pending stub that reports "unavailable") once its endpoint exists. New adapters register in one place and never touch the orchestration.

What's in it

  • Domain (crates/bunyip-domain/src/services/backup.rs): the AccountBackup bundle format (format_version 1), AppBackupAdapter + AppBackupOutcome, the MokoshBackupAdapter pending stub, an AccountBackupStore data seam (PgAccountBackupStore over the existing user/entitlement/application repos) so the round-trip is unit-tested with no Postgres, and BackupService with create_backup / restore_backup returning a RestoreReport.
  • API: GET /v1/account/backup streams the bundle as a download; POST /v1/account/restore applies an uploaded bundle and returns the report. Both AdminUser-gated and audited (AccountBackupCreated / AccountRestored, logging user_id + counts, never PII per BUNYIP-265).
  • Web: a Backup & Restore page at /integrations/backup (admin-guarded), linked from the Backup add-on tile on /applications, with a one-click download and a multipart restore upload that renders the resulting report.

Decisions / scope

  • "Account" is the acting admin's own account (single-tenant JIT reality). Surfaces are admin-guarded, matching the Email/Auto-ban settings pattern.
  • Export captures profile + entitlements only; system-global singletons (email/auto-ban/stripe) are excluded as platform-wide, not per-account.
  • Restore re-grants entitlements with the existing admin source (the application_entitlements.source CHECK allows admin/stripe/backfill), so no migration is needed. Email is left unchanged (its verified-change flow owns it).
  • No .sqlx regen (all data access is runtime query_as; audit actions are strings).

Tests

Five pure domain tests (run in the standard container check, no DB): the backup->restore round-trip with Mokosh wired, entitlement gating both directions, the Mokosh-unavailable case, no-adapter skip, and format-version rejection. just check-container is green (fmt + clippy -D warnings + full suite: domain 271 passed, web 94 passed).

Follow-ups (out of scope)

  • Real Mokosh HTTP backup client, once the Mokosh tenant backup endpoint ships (drops into MokoshBackupAdapter).
  • Server-side backup storage/history, scheduled backups, tar/gzip archive format.

🤖 Generated with Claude Code

## Summary Backs the seeded "Backup" add-on tile with an account-level Backup and Restore surface (BUNYIP-353). A backup captures the account's Bunyip-side state (owner profile + entitled app slugs) plus a per-app section for each entitled app, as one JSON bundle the admin downloads; restore re-applies the profile, re-grants the listed entitlements, and dispatches each app's bundle to that app, gated on entitlement. ## Reality-check that shaped the design The ticket assumed bunyip relays a backup call to each app backend. It does not: bunyip is the OIDC issuer, apps are relying parties, and the only outbound-to-app path is the fire-and-forget webhook push (which cannot return a bundle). Mokosh's tenant-scoped backup API is also not shipped yet. So per-app backup/restore is expressed behind a pluggable `AppBackupAdapter` trait: the account orchestration is built and tested now, and the real Mokosh HTTP client drops into `MokoshBackupAdapter` (currently a pending stub that reports "unavailable") once its endpoint exists. New adapters register in one place and never touch the orchestration. ## What's in it - **Domain** (`crates/bunyip-domain/src/services/backup.rs`): the `AccountBackup` bundle format (`format_version` 1), `AppBackupAdapter` + `AppBackupOutcome`, the `MokoshBackupAdapter` pending stub, an `AccountBackupStore` data seam (`PgAccountBackupStore` over the existing user/entitlement/application repos) so the round-trip is unit-tested with no Postgres, and `BackupService` with `create_backup` / `restore_backup` returning a `RestoreReport`. - **API**: `GET /v1/account/backup` streams the bundle as a download; `POST /v1/account/restore` applies an uploaded bundle and returns the report. Both `AdminUser`-gated and audited (`AccountBackupCreated` / `AccountRestored`, logging `user_id` + counts, never PII per BUNYIP-265). - **Web**: a Backup & Restore page at `/integrations/backup` (admin-guarded), linked from the Backup add-on tile on `/applications`, with a one-click download and a multipart restore upload that renders the resulting report. ## Decisions / scope - "Account" is the acting admin's own account (single-tenant JIT reality). Surfaces are admin-guarded, matching the Email/Auto-ban settings pattern. - Export captures profile + entitlements only; system-global singletons (email/auto-ban/stripe) are excluded as platform-wide, not per-account. - Restore re-grants entitlements with the existing `admin` source (the `application_entitlements.source` CHECK allows `admin`/`stripe`/`backfill`), so **no migration** is needed. Email is left unchanged (its verified-change flow owns it). - No `.sqlx` regen (all data access is runtime `query_as`; audit actions are strings). ## Tests Five pure domain tests (run in the standard container check, no DB): the backup->restore round-trip with Mokosh wired, entitlement gating both directions, the Mokosh-unavailable case, no-adapter skip, and format-version rejection. `just check-container` is green (fmt + clippy `-D warnings` + full suite: domain 271 passed, web 94 passed). ## Follow-ups (out of scope) - Real Mokosh HTTP backup client, once the Mokosh tenant backup endpoint ships (drops into `MokoshBackupAdapter`). - Server-side backup storage/history, scheduled backups, tar/gzip archive format. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(backup): account-level backup and restore behind the Backup add-on (BUNYIP-353)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 6s
Check / fmt + clippy + build + tests (pull_request) Successful in 8m42s
Create release / Create release from merged PR (pull_request) Has been skipped
c2a033a216
Back the seeded "Backup" add-on tile with an account-level Backup and Restore surface. A backup captures the account's Bunyip-side state (owner profile + entitled app slugs) plus a per-app section for each entitled app, as one JSON bundle the admin downloads; restore re-applies the profile, re-grants the listed entitlements, and dispatches each app's bundle to that app, gated on entitlement.

The ticket's premise that bunyip relays a backup call to each app backend does not hold: bunyip is the OIDC issuer, apps are relying parties, and the only outbound-to-app path is the fire-and-forget webhook push (which cannot return a bundle). Mokosh's tenant-scoped backup API is also not shipped yet. So per-app backup/restore is expressed behind a pluggable AppBackupAdapter trait: the account orchestration is built and tested now, and the real Mokosh HTTP client drops into MokoshBackupAdapter (currently a pending stub that reports "unavailable") once its endpoint exists. No new adapter touches the orchestration.

Domain (crates/bunyip-domain/src/services/backup.rs): the AccountBackup bundle format (format_version 1), AppBackupAdapter + AppBackupOutcome, the MokoshBackupAdapter pending stub, an AccountBackupStore data seam (PgAccountBackupStore over the existing user/entitlement/application repos) so the backup->restore round-trip is unit-tested with no Postgres, and BackupService with create_backup/restore_backup returning a RestoreReport. Restore re-grants entitlements with the existing "admin" source (the source CHECK allows admin/stripe/backfill) so no migration is needed, and leaves email unchanged (verified-change flow owns it). Five pure tests cover the round-trip with Mokosh wired, entitlement gating both directions, the Mokosh-unavailable case, no-adapter skip, and format-version rejection.

API: GET /v1/account/backup streams the bundle as a download; POST /v1/account/restore applies an uploaded bundle and returns the report. Both are AdminUser-gated and audited (AccountBackupCreated / AccountRestored, logging user_id and counts, never PII). BackupService is wired in main.rs with the Mokosh adapter registered.

Web: a Backup & Restore page at /integrations/backup (admin-guarded), linked from the Backup add-on tile on /applications, with a one-click download and a multipart restore upload that renders the resulting report. Adds an "upload" icon.

Scope note: "account" is the acting admin's own account (single-tenant JIT reality); system-global singletons (email/auto-ban/stripe) are intentionally excluded as they are platform-wide, not per-account.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gTWMeFYgEAiRKCV3L9jAb
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-10 19:35:15 +02:00
longjacksonle deleted branch feat/BUNYIP-353-account-backup-restore 2026-07-10 19:36:24 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!356
No description provided.