mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
- /docs/audit/ moved out of the public docs surface (23 per-release forensic notes). They were linked from the README but read as internal traceability artefacts, not user-facing documentation. Kept local for cross-release lookup; added to .gitignore so new ones don't re-track. - /docs/api/openapi-v1422-legacy.yaml removed — superseded by the current openapi.yaml on the v1.5 line. - /docs/api/openapi.yaml info.version bumped 1.4.23 → 1.5.0 so the spec matches package.json (the doc claims the version is mirrored from the package; previously the mirror had drifted six versions). - /docs/ops/deploy.md: image name corrected from `ghcr.io/marc-bombeck/healthlog` → `ghcr.io/mbombeck/healthlog` (the previous command would have 404'd at GHCR); release-process framing dropped from a release-pair note. - /docs/ops/backup-restore.md + /docs/self-hosting/scaling.md: trailing phase tags (`v1.4 G1`, `v1.4 G3`, `v1.4.40 W-POOL`) stripped from section headers + body. Replaced an internal-notes pointer with a concrete `getPoolMax()` reference. The README pointer to /docs/audit/ is removed in the next commit along with the rest of the README accuracy pass.
88 lines
3.1 KiB
Markdown
88 lines
3.1 KiB
Markdown
# Off-host backup & restore
|
|
|
|
HealthLog ships with an optional daily off-host backup that ships every
|
|
user's JSON dump, encrypted with AES-256-GCM under a SEPARATE key
|
|
(`BACKUP_ENCRYPTION_KEY`), to any S3-compatible bucket — Cloudflare R2,
|
|
AWS S3, Backblaze B2, MinIO, etc.
|
|
|
|
The backup runs at **02:30 Europe/Berlin** every day from the worker
|
|
container (queue `data-backup-offhost`). Object key layout:
|
|
|
|
```
|
|
<bucket>/YYYY-MM-DD/user-<userId>.json.enc
|
|
```
|
|
|
|
## Wire format (binary)
|
|
|
|
```
|
|
magic = "HLBK" (4 bytes, ASCII)
|
|
version = 0x01 (1 byte)
|
|
iv = 12 random bytes (AES-GCM nonce)
|
|
authTag = 16 bytes (AES-GCM tag)
|
|
ciphertext = N bytes (AES-256-GCM, key = BACKUP_ENCRYPTION_KEY)
|
|
plaintext = JSON dump (UTF-8)
|
|
```
|
|
|
|
## Required env vars
|
|
|
|
| Var | Required | Notes |
|
|
| ----------------------- | -------- | -------------------------------------------------------------------- |
|
|
| `BACKUP_ENCRYPTION_KEY` | yes | 64 hex chars or 32-byte base64. **Different from `ENCRYPTION_KEY`.** |
|
|
| `BACKUP_S3_ENDPOINT` | yes | e.g. `https://<account>.r2.cloudflarestorage.com` |
|
|
| `BACKUP_S3_BUCKET` | yes | |
|
|
| `BACKUP_S3_ACCESS_KEY` | yes | |
|
|
| `BACKUP_S3_SECRET_KEY` | yes | |
|
|
| `BACKUP_S3_REGION` | no | defaults to `auto` (Cloudflare R2) |
|
|
| `BACKUP_RETENTION_DAYS` | no | defaults to `30` |
|
|
|
|
## Bucket lifecycle (recommended)
|
|
|
|
The worker prunes objects older than `BACKUP_RETENTION_DAYS`, but the
|
|
storage provider's lifecycle rule is the canonical safety net:
|
|
|
|
```
|
|
Filter: "" (all objects)
|
|
Action: Expire after 30 days
|
|
```
|
|
|
|
For Cloudflare R2 add this from the bucket's **Settings → Lifecycle**.
|
|
|
|
## Smoke test
|
|
|
|
After deploying, hit `POST /api/admin/backup/test` (admin-only). It
|
|
performs a 1-byte PUT + GET round-trip and returns:
|
|
|
|
```json
|
|
{
|
|
"endpoint": "https://...r2.cloudflarestorage.com",
|
|
"bucket": "healthlog-backups",
|
|
"region": "auto",
|
|
"putLatencyMs": 142,
|
|
"getLatencyMs": 38,
|
|
"ok": true
|
|
}
|
|
```
|
|
|
|
The credentials are never returned.
|
|
|
|
## Restore
|
|
|
|
Pick a key (e.g. `2026-05-08/user-clx123.json.enc`) from the bucket
|
|
and run:
|
|
|
|
```bash
|
|
BACKUP_S3_ENDPOINT=https://...r2.cloudflarestorage.com \
|
|
BACKUP_S3_BUCKET=healthlog-backups \
|
|
BACKUP_S3_ACCESS_KEY=... \
|
|
BACKUP_S3_SECRET_KEY=... \
|
|
BACKUP_S3_REGION=auto \
|
|
BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32) \
|
|
pnpm tsx scripts/restore-backup.ts \
|
|
2026-05-08/user-clx123.json.enc \
|
|
/tmp/restored.json
|
|
```
|
|
|
|
The script downloads the object, decrypts it, and writes the JSON dump
|
|
to disk. Importing the JSON back into a HealthLog instance is left to
|
|
the operator (use `prisma db seed` or a custom script).
|