Skip to content

Backups

Every AppMecca app gets a single canonical backup primitive that covers both its database and every drive it declares. A backup is a single atomic artifact — half-backups are never offered for restore.

One primitive. mecca app backup/restore/backups/download replaces the earlier database-only mecca db * commands (which have been retired entirely). Apps without drives still get a valid backup — the drive section of the bundle is just empty.


What's in a backup

Each backup is a bundle stored under one prefix in AppMecca object storage:

backups/{env}/{app}/{branch}/{type}/{timestamp-desc}/
    database.sql.gz             (pg_dump output)
    drives/
        media.tar.gz            (one tar per declared drive)
        exports.tar.gz
        …
    manifest.json               (written LAST — acts as the commit marker)

The manifest records the schema version, per-component SHA-256 checksums, byte counts, file counts per drive, and the image tag the backup was taken against. A reader that finds manifest.json knows every referenced object has already been written.


Creating a backup

mecca app backup <app>
Flag Effect
-d, --description <text> Free-form label. Shown in mecca app backups.
-b, --branch <name> Which branch to back up. Omit to resolve automatically — your production branch (or main if it exists). Pass -b dev to back up a specific branch.
--quiesce Advisory only — records the bundle as quiesced in its manifest. It does not stop services; the snapshot is still a hot copy.

Every backup is a hot copy — the app keeps serving throughout. The database half is always consistent (pg_dump takes a snapshot-isolated view regardless). The drive half is a live tar, so for a guaranteed-consistent snapshot of a high-write drive you must stop writes yourself first (mecca app restart/scale the app to zero, take the backup, scale back up). --quiesce records your intent in the manifest but performs no stop on its own.

Example — take a labelled backup before a migration:

mecca app backup recipe-cookoff -d "pre-migration-2026-04-19"

The backup runs asynchronously. The CLI returns an action ID immediately; use mecca queue status <id> to follow progress.


Listing backups

mecca app backups <app>
Flag Effect
-b, --branch Filter by branch.
--limit <N> Max bundles to show. Default 20.

Returns newest first. The automatic database-only safety copies — taken before every deploy, before a database-only restore, and before a delete drops the database — are marked database-only (with their class, e.g. database-only · pre-deploy (may expire 14d after it was taken)). Their Prefix is the .sql.gz object itself, and they restore and download like a bundle.


Restoring a backup

mecca app restore <app> <bundle-prefix>

<bundle-prefix> is the value of the Prefix column from mecca app backups.

Flag Effect
-b, --branch <name> Which branch to restore into. Omit for a single-branch app (resolved automatically). A restore is destructive, so a multi-branch app requires -b — an omitted branch is refused (409) rather than guessing which branch's database to overwrite.
--drives <names> Comma-separated subset of drives to restore (default: all).
--drives-only Restore drives but leave the database untouched.
--db-only Restore the database but leave drives untouched.
--in-place Skip the atomic-swap; extract directly over the live drive (old behaviour).
--force Skip the destructive-action confirmation prompt.

DESTRUCTIVE. Restore overwrites the current database AND every drive it touches. Confirm twice unless --force is passed. The agent restores drives first (safer — DB restore is atomic via psql -v ON_ERROR_STOP=1; a partial drive restore is recoverable by re-running).

Atomic-swap (default)

The default restore is rollback-safe: the agent extracts the tar into a sibling staging directory inside the mount, then atomically renames staging/ → live and old-live → outgoing/ per entry. If the extraction or verification fails, nothing moved — the current drive is untouched. Only pass --in-place if you need the old direct-extract behaviour (e.g. to preserve unwritten sibling files that aren't in the bundle).

SHA-256 verification

Every restore validates the manifest's per-component checksums before touching the drive or database. A corrupted bundle (bit-rot, a truncated upload) is refused with a clear error; the app stays running on its existing data. There is no way to skip verification — a bundle that fails its own checksum is never applied.

Example — restore just the media drive after a data-loss incident, leaving the DB intact:

mecca app restore recipe-cookoff \
    backups/dev/recipe-cookoff/main/manual/20260419-143000-pre-migration \
    --drives-only --drives media

Downloading a bundle off-platform

mecca app download <app> <bundle-prefix>

Prints one pre-signed download URL per object (database + each drive + manifest), one per line. It does not write any files itself — pipe the output to xargs wget or iterate in a shell for off-platform archival.

A database-only backup prints exactly one URL, for its .sql.gz. A pre-deploy or pre-restore safety copy may have been removed since it was listed (they expire); the download then answers backup not found: <key> (it may have expired).

Flag Effect
--expires-in <seconds> URL TTL. Default 3600 (1 hour). Max 86400.

Automatic backups

Two moments the platform takes a backup for you:

  1. Pre-deploy. Every mecca app deploy triggers a full-bundle backup before the new code runs. Labelled pre_deploy.
  2. Pre-drop. mecca app delete takes a safety backup before dropping the database. Labelled pre_drop.

Both are listed by mecca app backups alongside your manual backups.


Consistency model

Mode When to use
Hot copy (every backup) Apps with sporadic, whole-file writes. Tiny risk of capturing one in-flight upload mid-write.
Stop writes yourself Apps with continuous writes or strict consistency needs (e.g. a ticketing app under load). Scale the app to zero, back up, scale back — --quiesce only records intent in the manifest, it does not stop anything.

The database side is always consistent — pg_dump takes a snapshot-isolated view of the DB regardless of hot-copy/quiesce.


What if my app has no drives?

The backup still runs — you just get a bundle with an empty drives list in the manifest and no drives/ subdirectory in the bundle. Restore works the same way. No flag-juggling to opt out of drives.


Seeding a fresh app from another app's backup

Branch previews and new environments can seed their initial state directly from a bundle — no manual restore step, no blank database to populate.

mecca register <git-url> --seed-from <app>:<branch>:<selector>

The selector is one of:

Selector Resolves to
latest The newest bundle for that app and branch
YYYY-MM-DD The newest bundle taken on or before that day (UTC)
anything else A substring of the bundle's name. Exactly one match resolves; more than one is refused rather than guessed

The short form :main:latest means this same app, on main.

The seed is a one-shot: after the first successful deploy the agent stamps seeded_at and later deploys skip the seed entirely.

The seed is refused — and that deploy fails — when:

  • The source app is not in this app's own group. You can only seed from apps your group owns. To move data between groups, download a backup from the source app with mecca app download and load the database dump into the target app from its Database tab in the dashboard.
  • This app already holds data. A seed resets the database and replaces the drives, so it only runs against an app whose database has no tables yet and whose drives are empty — in practice, before the app's first deploy has run its migrations. To load a backup into an app that already has data, use mecca app restore.
  • The bundle's manifest version is one this platform cannot read.
  • The backup doesn't provably belong to your group. Backups taken since this check shipped record their owning group; an older backup is only accepted when it was taken after the source app was registered. So an app you deleted and registered again cannot seed from its pre-deletion backups — download a backup before you delete an app instead.

An app without a database cannot be seeded at all; seeding restores a database first.

If a seed is refused, clear the reference and redeploy (the app then starts empty), or fix the reference:

# clear it — the API accepts an explicit null for this one field
curl -X PATCH "$API/api/apps/<app>?branch=<branch>&group=<your-group>" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"seed_from_reference": null}'

Setting a seed reference on an app that is already running, or has already been seeded, is refused with a 409.

Example — spin up a feature-login branch of recipe-cookoff seeded from the main branch's latest bundle:

mecca register git@github.com:you/recipe-cookoff.git \
    --seed-from recipe-cookoff:main:latest

While the seed is pending, the retention job does not prune the source bundles it points at.


Retention

Bundles don't accumulate forever. The platform's scheduler prunes them daily at 05:00 UTC:

Rule Default Meaning
Keep most-recent 10 The newest 10 bundles per (app, branch) are kept
Keep newer than 30 days Any bundle younger than 30 days is kept

The two rules are a union, not an intersection — a bundle is deleted only when it fails both (it is outside the newest 10 and older than 30 days). So an app that backs up rarely keeps its bundles well past 30 days, and an app that backs up hourly keeps a full month of them.

Pinned bundles (pinned by an active seed-from reference) are never pruned regardless of age or count. If you want to keep a specific bundle forever, ask an admin to pin it.


Point-in-time recovery (PITR)

When the platform administrator has enabled continuous snapshots for your environment (backup.enabled: true in the environment config), you can restore to an arbitrary second within the configured window rather than picking a discrete bundle. Useful when:

  • The last scheduled bundle is too old to cover what you're trying to undo.
  • You need the state "90 seconds before the bad migration ran", not "last night at 03:00".

Listing available recovery points

Platform operators only

Recovery points are snapshots of the environment's shared managed PostgreSQL database and shared filesystem — environment-level infrastructure, not per-app resources. Listing them (and the PITR restore they feed) is a platform-operator operation; a tenant call returns 403.

mecca app recovery-points myapp
mecca app recovery-points myapp --from 2026-04-20T00:00:00Z
mecca app recovery-points myapp --from 2026-04-20T00:00:00Z --until 2026-04-21T00:00:00Z
mecca app recovery-points myapp --json

--from / --until take ISO-8601 instants and bound the recovery-point window. The flag is --from, not --since: everywhere else in the CLI --since means a relative window like 24h, so it is refused here rather than silently misread as an instant. A value that is not an instant at all (--from 24h, --until yesterday) is a usage error naming the flag, and nothing is sent.

Output is a table of recovery points newest-first, showing creation date, resource type — AWS's own RDS (the database) or EFS (drive storage) — status, and the recovery-point identifier. Every row belongs to the environment's shared database instance or filesystem (shared: true in --json), so the list is the same for every app on the environment that uses those resources. An empty table means AWS Backup holds no points for them yet — the first scheduled snapshot typically lands within an hour of the environment being provisioned. When the platform could not look at all, the command says so instead: see When the listing cannot answer.

Restoring to a specific instant

Platform-operator only (for now)

A point-in-time restore clones the environment's shared database instance and filesystem into new platform resources, so this command is currently gated to platform operators — ask your operator to run it for you. (Per-tenant quotas that will re-open self-service are tracked.) Bundle restores (mecca app restore <bundle>) and uploaded-dump restores are unaffected and remain self-service.

mecca app restore myapp --to-timestamp 2026-04-21T14:30:00Z

Flags:

  • --db-only — restore the database, leave drive storage alone.
  • --drives-only — restore drive storage, leave the database alone.
  • --force — skip the confirmation prompt.

The timestamp is ISO-8601 UTC. The platform finds the recovery point whose creation time most closely precedes your target instant and starts a restore job for each resource (the database plus any shared filesystem the app mounts).

What actually happens on disk

  • Database: the platform creates a new database instance from the snapshot. The original stays online for a grace window (admin-configurable, default 24h) so you can diff the two before cutting over.
  • Drive storage: the platform creates a new filesystem with the restored contents. The original also stays for the grace window.

You must explicitly repoint the app at the restored resources — PITR doesn't auto-swap environment variables or remount the drive. Check mecca queue status <action_id> for job progress; once terminal, coordinate with the admin to cut the app over or discard the restored copy.

Bundle vs PITR — when to use which

Scenario Use
Planned cutover (pre-migration snapshot) Bundle (mecca app backup → mecca app restore <prefix>)
Branch-preview seed Bundle (portable, works cross-environment)
Download the data locally Bundle (mecca app download)
Undo an accidental DELETE from 90 seconds ago PITR (--to-timestamp)
Roll back to "moments before the outage at 09:17" PITR
Cross-region DR Neither today — future feature

When the listing cannot answer

mecca app recovery-points distinguishes "there are none" from "I could not look", because the two need different remedies. It prints the platform's note in each case, and exits 1 whenever it could not look, so a script never reads a refusal as an empty list — with one exception, the last row: an environment whose API predates this listing cannot tell the two apart.

What you see What it means What to do Exit
Couldn't check recovery points. AWS refused backup:ListRecoveryPointsByBackupVault … The environment's agent has not been granted the read yet. The operator runs appmecca infra setup -e <env>; IAM changes take about a minute to apply. 1
Couldn't check recovery points. backup: ThrottlingException — nothing is concluded … The question could not be asked, usually transiently. Nothing is concluded about whether any points exist. Retry; if it persists, report it, quoting the code shown. 1
Couldn't check recovery points. The asset index records no rds_instance / efs_file_system … The platform's own record of the environment is incomplete. Retrying will not change this. The operator runs appmecca infra setup -e <env>, which re-records the existing resource. 1
AWS Backup is not configured for environment … The environment's backup feature is off, so there is no vault. Ask your platform administrator — enabling it is a one-line environment config change. 0
myapp declares no database and no drives … There is nothing AWS Backup would hold for this app. Nothing. 0
AWS Backup is not configured, from an environment whose API predates this listing An older API reports a REFUSED read this way, so this line may hide a missing grant. Ask your platform administrator whether the environment's API is current; once it is, the refusal reads as the first row. 0

--json prints the same answer as a document: notice is aws_backup_read_refused, aws_backup_check_failed or aws_backup_not_configured (absent when the listing succeeded), and note carries the sentence above.


  • Domain routing — custom domains work independently of backups.
  • Service management — how to stop/restart an app's services (to take writes offline before a consistent backup).
  • Drive file management — a drive's contents are what the drives/ half of a bundle contains.