Skip to content

Support restores from backups on /intern - #2843

Merged
dwwoelfel merged 17 commits into
mainfrom
restores-from-admin
Aug 7, 2026
Merged

Support restores from backups on /intern#2843
dwwoelfel merged 17 commits into
mainfrom
restores-from-admin

Conversation

@dwwoelfel

@dwwoelfel dwwoelfel commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This adds the UI that lets server admins restore an app from a backup.

This is a follow-up to #2836.

It can be used by Instant admins (i.e. us) and admins of self-hosted instances to import apps from Instant. If someone wants to switch to self-hosted, they can turn off writes on Instant, generate a backup, then import it into their self-hosted instance from /intern/restore.

Since it's only meant to be used by administrators, I just put the zip file on disk in a temp directory and restore directly from the zip. This makes restoring a lot easier because we can just seek to different positions on the disk when we're putting files into the files queue. If it fails, you'll just have to retry from the beginning.

Screen.Recording.2026-08-06.at.4.43.27.PM.mov

If you want to restore to the same app_id, but something is already there, we'll show you an error message that tells you to delete the app from the db directly:

Screenshot 2026-08-06 at 4 46 23 PM

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an admin restore page for backup ZIP uploads. Adds asynchronous restore jobs with persisted status, progress, cancellation, cleanup, and completion links. Adds restore and backup routes, rate limiting, migration support, and shared superuser email configuration.

Changes

Admin app restore

Layer / File(s) Summary
Restore schema and API contracts
server/resources/migrations/*, server/src/instant/config.clj, server/src/instant/tasks.clj, server/src/instant/core.clj, server/src/instant/dash/routes.clj
Adds the restore-job table, shared superuser email configuration, upload body parsing bypass, admin authorization, restore handlers, and route registration.
Backup job API and rate limits
server/src/instant/dash/routes.clj
Adds per-app and per-client-IP backup rate limits and backup job creation, retrieval, listing, cancellation, and deletion handlers.
Restore progress instrumentation
server/src/instant/restore.clj
Adds callbacks for restore totals, copied triples, and uploaded files.
Asynchronous restore execution
server/src/instant/db/app_restore_jobs.clj
Adds ZIP staging, job persistence, status transitions, progress updates, cancellation checks, background execution, error handling, and staged-file cleanup.
Admin restore interface
client/www/app/intern/content.tsx, client/www/app/intern/restore/*
Adds the internal restore tool, upload dialog, authentication states, job polling, status display, stale-job warnings, cancellation, and completion links.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant RestorePage
  participant DashboardRoutes
  participant AppRestoreJobs
  participant Restore
  Admin->>RestorePage: Select ZIP and submit restore
  RestorePage->>DashboardRoutes: POST /dash/restores/zip
  DashboardRoutes->>AppRestoreJobs: Start waiting restore job
  AppRestoreJobs->>Restore: Run restore with progress callbacks
  Restore-->>AppRestoreJobs: Report totals and progress
  RestorePage->>DashboardRoutes: Poll restore job
  DashboardRoutes-->>RestorePage: Return status and progress
Loading

Possibly related PRs

Suggested reviewers: nezaj, stopachka

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding backup restore support to /intern.
Description check ✅ Passed The description directly explains the administrator restore UI, backup import workflow, temporary ZIP storage, and existing-app handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch restores-from-admin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/instant/db/app_restore_jobs.clj`:
- Line 212: Update the restore dispatch around run-restore! so restore jobs
cannot execute concurrently process-wide. Replace the per-upload unbounded
background launch with a single bounded worker or database-backed lease/queue,
ensuring new jobs are queued or rejected while one restore is active and
preserving the one-restore-at-a-time contract.
- Around line 163-185: Update the restore worker around mark-processing! and
mark-completed! to honor failed status transitions: verify mark-processing!
successfully claims the waiting job before starting restore/restore-from-zip,
and exit without launching work when it updates zero rows. Add a cancellation
checkpoint during active processing before ownership transfer, and if
mark-completed! updates zero rows because cancellation won, remove the restored
app before returning while preserving the cancelled status.
- Around line 198-213: Implement startup reconciliation for persisted
nonterminal restore jobs, and initialize it from the application startup path in
core. Ensure waiting jobs are resumed safely, while processing jobs are marked
failed or otherwise finalized without rerunning restore work; clean up each
job’s staged ZIP, including orphaned staged files where applicable. Anchor the
changes around start-restore!, run-restore!, and the core initialization flow.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fb407cc3-88d6-457f-bd5e-1f36f2b96070

📥 Commits

Reviewing files that changed from the base of the PR and between c496390 and 3869ddd.

📒 Files selected for processing (9)
  • client/www/app/intern/content.tsx
  • client/www/app/intern/restore/content.tsx
  • client/www/app/intern/restore/page.tsx
  • server/resources/migrations/123_app_restore_jobs.down.sql
  • server/resources/migrations/123_app_restore_jobs.up.sql
  • server/src/instant/core.clj
  • server/src/instant/dash/routes.clj
  • server/src/instant/db/app_restore_jobs.clj
  • server/src/instant/restore.clj

Comment thread server/src/instant/db/app_restore_jobs.clj Outdated
Comment thread server/src/instant/db/app_restore_jobs.clj
Comment thread server/src/instant/db/app_restore_jobs.clj
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

View Vercel preview at instant-www-js-restores-from-admin-jsv.vercel.app.

@dwwoelfel
dwwoelfel marked this pull request as ready for review August 6, 2026 23:50
@dwwoelfel dwwoelfel changed the title [WIP] restores from /intern Support restores from backups on /intern Aug 6, 2026

@stopachka stopachka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Niice!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/instant/dash/routes.clj (1)

449-449: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat an empty title as absent.

get-in returns "" when the optional query parameter is submitted empty. In Clojure, "" is truthy, so the restore can save a blank override instead of using the title from the backup.

Use ex/get-optional-param! with string-util/coerce-non-blank-str.

Proposed fix
-        title (get-in req [:params :title])
+        title (ex/get-optional-param! req
+                                      [:params :title]
+                                      string-util/coerce-non-blank-str)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/instant/dash/routes.clj` at line 449, Update the title binding in
the restore request flow to use ex/get-optional-param! with
string-util/coerce-non-blank-str, so an empty submitted title becomes absent and
the backup title is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/instant/config.clj`:
- Around line 59-65: Update superuser-email to match bootstrap validation for
INSTANT_SUPERUSER_EMAIL: trim surrounding whitespace, treat blank values as
unset, and reject invalid emails consistently instead of silently converting
them to nil. Reuse the existing shared normalization/validation helper if
available, while preserving nil for an unset variable.

---

Outside diff comments:
In `@server/src/instant/dash/routes.clj`:
- Line 449: Update the title binding in the restore request flow to use
ex/get-optional-param! with string-util/coerce-non-blank-str, so an empty
submitted title becomes absent and the backup title is preserved.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71acae2a-6874-451a-b54b-3a532761dcac

📥 Commits

Reviewing files that changed from the base of the PR and between b2e9881 and ee33d01.

📒 Files selected for processing (2)
  • server/src/instant/config.clj
  • server/src/instant/dash/routes.clj

Comment thread server/src/instant/config.clj
Base automatically changed from on-demand-backup to main August 7, 2026 15:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/instant/dash/routes.clj (1)

2049-2064: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make multi-bucket rate-limit consumption atomic.

Lines 2053-2056 peek all buckets, but Lines 2062-2064 consume them one at a time. Under contention, the app token can be consumed successfully, then the IP bucket can reject the request. The request fails, but the app quota is lost. Repeated contention can create false app-level rate limits.

Use an atomic reservation for both buckets, or refund every token consumed when a later bucket rejects the request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/instant/dash/routes.clj` around lines 2049 - 2064, Make the
multi-bucket flow around retry-at and the consume-user-rate-limit-retry-at calls
atomic: reserve capacity for all bucket-params as one operation, or track
successful consumptions and refund every earlier bucket if a later consumption
rejects. Ensure a failed request never leaves any bucket token consumed, while
preserving the existing retry-at reporting behavior.
🧹 Nitpick comments (1)
server/src/instant/dash/routes.clj (1)

456-461: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Enforce a restore ZIP size limit.

server/src/instant/core.clj merges :max-entity-size -1, and the restore route streams that body directly into server/src/instant/db/app_restore_jobs.clj::store-zip!, which copies the stream to instant-restore-uploads/<job-id>.zip without a bound. Add an endpoint/request-size cap for restore uploads; delete the partial staged zip if the limit is exceeded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/instant/dash/routes.clj` around lines 456 - 461, Enforce a maximum
request size for the restore upload before or while the body reaches the restore
route’s app-restore-jobs/start-restore! and store-zip! flow, rather than
inheriting the unlimited :max-entity-size setting. Ensure oversized uploads are
rejected and any partially written instant-restore-uploads/<job-id>.zip is
deleted when the limit is exceeded, while preserving normal restore behavior for
uploads within the cap.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@server/src/instant/dash/routes.clj`:
- Around line 2049-2064: Make the multi-bucket flow around retry-at and the
consume-user-rate-limit-retry-at calls atomic: reserve capacity for all
bucket-params as one operation, or track successful consumptions and refund
every earlier bucket if a later consumption rejects. Ensure a failed request
never leaves any bucket token consumed, while preserving the existing retry-at
reporting behavior.

---

Nitpick comments:
In `@server/src/instant/dash/routes.clj`:
- Around line 456-461: Enforce a maximum request size for the restore upload
before or while the body reaches the restore route’s
app-restore-jobs/start-restore! and store-zip! flow, rather than inheriting the
unlimited :max-entity-size setting. Ensure oversized uploads are rejected and
any partially written instant-restore-uploads/<job-id>.zip is deleted when the
limit is exceeded, while preserving normal restore behavior for uploads within
the cap.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7de575a1-16b6-4291-a35e-bf6ef516f5ed

📥 Commits

Reviewing files that changed from the base of the PR and between ee33d01 and a03b695.

📒 Files selected for processing (3)
  • server/src/instant/config.clj
  • server/src/instant/dash/routes.clj
  • server/src/tasks.clj

@dwwoelfel
dwwoelfel merged commit 4c19871 into main Aug 7, 2026
34 checks passed
@dwwoelfel
dwwoelfel deleted the restores-from-admin branch August 7, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants