HermesBackup

Reference

Setup & pairing

The entire UX is one instruction from the user to their agent:

# user tells Hermes Agent:
Set up cloud backups for yourself using hermesbackup.com
  1. The agent fetches https://hermesbackup.com/agent (mirrored at /llms.txt) for exact install instructions.
  2. The skill's setup calls POST /v1/pair/start (unauthenticated) with the device hostname → receives a pairing code (FROG-8241) and poll token.
  3. The agent tells the user: “Go to hermesbackup.com/pair and enter code FROG-8241” (sign-up via Clerk happens in under a minute if no account exists).
  4. The script polls POST /v1/pair/poll every 5 seconds (10-minute expiry). On approval it receives a device token, stored chmod 600.
  5. Setup generates the age keypair, prints the recovery kit, asks about schedule (default: daily --quick at 03:00 local + weekly full on Sundays) and on_full policy, writes cron entries, runs a first backup immediately, and confirms end-to-end success.
Agents authenticate with Authorization: Bearer <de...de>. The dashboard uses Clerk JWT. Agents never touch Clerk.

API reference

Base URL: https://api.hermesbackup.com/v1. JSON everywhere. Errors as {"error": {"code", "message"}}.

Pairing

POST
/pair/start
Auth: none. Body: {device_name}. Returns {pair_code, poll_token, expires_at}.
POST
/pair/poll
Auth: none. Body: {poll_token}. Returns 202 pending or {device_token, device_id} once approved.
POST
/pair/approve
Auth: Clerk. Dashboard-only — agent never calls this. Approves {pair_code} for the signed-in user.

Backups

POST
/backups/init
Auth: device. Body: {size_bytes, sha256, label, kind} (kind: full|quick). Checks quota + rate limits. Returns {backup_id, upload_url} or 507.
POST
/backups/{id}/complete
Auth: device. Server HEADs the R2 object, verifies size + SHA-256 checksum, marks the row stored, updates usage.
GET
/backups
Auth: device or Clerk. List backups: id, label, kind, size, sha256, created_at, device.
GET
/backups/{id}/download
Auth: device or Clerk. Returns {download_url} (presigned GET, 5-min expiry).
DEL
/backups/{id}
Auth: device or Clerk. Delete object + row; tombstone retained 7 days.

Account

GET
/quota
Auth: device or Clerk. Returns {plan, used_bytes, limit_bytes, object_count}.
GET
/devices
Auth: Clerk. List devices: name, id, paired-date, last-backup, health, revoked.
DEL
/devices/{id}
Auth: Clerk. Revoke device. Existing backups are untouched.
POST
/webhooks/stripe
Auth: signature. Billing state changes.
POST
/webhooks/emailit
Auth: signature. Email bounce handling.
Incomplete uploads (init with no complete within 24 hours) are garbage-collected by a cron trigger.

D1 schema (abridged)

users(id TEXT PK, clerk_id TEXT UNIQUE, email TEXT, plan TEXT DEFAULT 'free',
      stripe_customer_id TEXT, used_bytes INTEGER DEFAULT 0,
      limit_bytes INTEGER DEFAULT 2147483648, created_at INTEGER);

devices(id TEXT PK, user_id TEXT REFS users, name TEXT, token_hash TEXT UNIQUE,
        last_backup_at INTEGER, expected_interval_hours INTEGER DEFAULT 24,
        revoked INTEGER DEFAULT 0, created_at INTEGER);

backups(id TEXT PK, user_id TEXT, device_id TEXT, r2_key TEXT UNIQUE,
        label TEXT, kind TEXT, size_bytes INTEGER, sha256 TEXT,
        status TEXT DEFAULT 'pending',  -- pending|stored|deleted
        created_at INTEGER, deleted_at INTEGER);

pairings(pair_code TEXT PK, poll_token_hash TEXT UNIQUE, device_name TEXT,
         user_id TEXT, status TEXT DEFAULT 'pending', expires_at INTEGER);

Dashboard

Served from Worker static assets at hermesbackup.com. Auth: Clerk JWT via middleware. Built as a small React app.

  • Backups — list, download (presigned GET), delete, usage meter with 4 states (normal / warning / critical / full).
  • Devices — pairing approval inline, revoke with confirmation, last-seen and health badges (healthy / silent / overdue / revoked).
  • Billing — plan status, Stripe Customer Portal deeplink (we never render a card-number field), planned tiers as read-only placeholders.
  • Settings — email prefs, delete account (purges all objects).

Full dashboard UX spec lives at design/dashboard-ux-spec.md in the repo. Interactive prototype at design/dashboard-prototype.html.

Restore on a new machine

# on the new machine, after installing Hermes Agent:
hermesbackup.sh restore

# flow:
No device paired. Starting pairing flow...
Pair code: OTTER-3091
…
Select backup to restore [1-3]: 1
Downloading ciphertext via presigned URL...
Decrypting with age identity...
  Enter AGE-SECRET-KEY-1: **********
  ✓ SHA-256 verified: 7f2a...e910 matches server record
  ✓ Plaintext written to /tmp/hermes-restore-XXXX/
Importing to ~/.hermes/...
  ✓ 4,579 files imported. SOUL.md, MEMORY.md, skills/, sessions/ restored.
# done. Your agent remembers everything.

Retention & quota

Backups accumulate until the account hits its quota (2 GB free / 10 GB Pro). When an init would exceed quota, the API returns 507 INSUFFICIENT_STORAGE with current usage. The skill then follows its configured policy:

  • on_full = "fail" (default): abort, log, server emails the user.
  • on_full = "rotate": skill deletes the oldest backups via the API until the new one fits, then retries. The user opts into this at setup; it's automatic pruning by client choice, so the server never silently destroys data.

Deletes are immediate (object + row) with a 7-day tombstone record for support disputes. Quota warnings email at 80% and 95%. The server never silently deletes user data — invariant.

Email plan

Transactional only, via Emailit REST v2 with webhook-driven bounce suppression. Pointedly: the recovery-kit reminder does not contain the key.

  • Welcome + recovery-kit reminder
  • Device-paired confirmation
  • First-backup success
  • Backup-failure alert
  • Missed-schedule nudge (device silent > 2× expected interval)
  • Quota 80% / 95%
  • Payment failed (dunning ×3) / renewal receipt
  • Account-deletion confirmation

Estimated volume: ~15 emails per user per year — inside the free 1,000/month until roughly 800 active users.

Unit economics

Per user per year:

ItemFree userPro (avg 4 GB)Pro (10 GB worst case)
Revenue$0$10.00$10.00
Stripe fee (2.9% + $0.30)−$0.59−$0.59
R2 storage ($0.015/GB-mo)−$0.18−$0.72−$1.80
R2 egress$0$0$0
Workers / D1 / KV marginal~$0.01~$0.01~$0.01
Net contribution−$0.20+$8.67+$7.59

R2's zero egress is why this business works at $10/year. A 10 GB restore on S3 would cost ~$0.90 — nearly 10% of annual revenue gone in one click. On R2 it costs us nothing.

Fixed cost floor: ~$75–100/year (Workers Paid $60, domain ~$10, Emailit ~$0–20). Break-even at roughly 12 Pro subscribers. Full scenario projections are in the spec at files/hermesbackup-spec.md §11.