Cornerstone managed off-site backup (Backblaze B2)
The Cornerstone Vault keeps a family's sealed data in Supabase (Postgres rows + private Storage blobs). Managed off-site backup holds an encrypted second copy on a genuinely separate provider so that losing our primary store — account termination, provider outage, corruption — doesn't lose the family's data.
Because every vault item is already ciphertext, the second copy is sealed bytes too. The backend reads ciphertext (fine) and never a key — honest custody holds even though we run the backup.
What it does when enabled
- Back up off-site now (
POST /api/cornerstone/backup/run) — assembles the user's whole vault into the samecornerstone-vault-backuparchive the client-side "Download backup" produces, and uploads it as one object per user to your B2 bucket. - Restore from off-site (
POST /api/cornerstone/backup/restore) — pulls that archive and re-creates the items (adds them; restore into a fresh vault). - Status (
GET /api/cornerstone/backup/status) — last backup time, item count, bytes, last error. Surfaced in the vault's Cold-backup panel.
A managed backup and a family-held "Download backup" file are the same format, so either can restore the other.
Enable it
Set all four env vars on the backend (Railway). Missing any one → the feature reads
as unconfigured: every route returns BACKUP_NOT_CONFIGURED and the UI shows
"not set up on this deployment yet." Nothing runs until it's fully configured.
CORNERSTONE_B2_KEY_ID=<B2 applicationKeyId>
CORNERSTONE_B2_APP_KEY=<B2 applicationKey>
CORNERSTONE_B2_BUCKET_ID=<the bucket's id (not the name)>
CORNERSTONE_B2_BUCKET=<the bucket's name (for the download URL)>
Create these in the Backblaze console:
- Create a bucket — set it Private (never public; it holds sealed archives).
- Create an application key scoped to that bucket with read + write. Copy the
keyIDandapplicationKey(shown once). - The bucket id is on the bucket's detail page; the bucket name is what you named it.
Apply migration 607 (cornerstone_backup_status) — status only, no vault content.
⚠️ Smoke-test before trusting it
The B2 network wire (apps/backend/src/services/b2-client.ts, native B2 v2 API via
fetch, no SDK) is code-complete but was not testable without live credentials.
Before relying on it for real family data:
- Configure the four env vars against a throwaway private bucket.
- Seal a note and a small photo in the vault.
- Back up off-site now → confirm an object appears in the B2 bucket and the status row shows the item count.
- Delete an item locally, Restore from off-site → confirm it comes back and opens with its passphrase.
- Only then point it at the real bucket.
Not yet (follow-ons)
- Scheduled auto-backup (a cron) — deliberately not built yet, so there's no
recurring-task heartbeat/toggle obligation. It's manual ("Back up now") for now.
When added, wire the heartbeat +
CRON_REGISTRYtoggle per the recurring-tasks doc. - Incremental per-item diff (currently a full-vault archive each run — fine at family scale).
- S3 Glacier Deep Archive tier as a cheaper cold option (the client is B2-native today; the interface is small enough to add an S3 backend behind the same config).
See docs/product/CORNERSTONE_DATA_LAYER.md and the honest-custody standard.