Blob tiering — R2 hot, OCI cold
How the storage/ layer moves infrequently used files from Cloudflare R2 (hot) to OCI MinIO (cold) when the hot quota fills up.
Parent plan: ../plans/completed/migration-plan.md. Layer overview: README.md.
Tiers
| Tier | Backend | Default quota | Role |
|---|---|---|---|
| hot | Cloudflare R2 | 10 GB free | All new uploads; fast global reads |
| cold | MinIO on OCI block volume | ~100 GB | Evicted blobs; slower first read |
wireStorage() returns TieredBlobStore when BLOB_PROVIDER=tiered. Registry rows live in blob_objects (migration 0023).
Registry (blob_objects)
| Column | Purpose |
|---|---|
bucket, path |
Logical key (paper-images, experiment-artifacts, …) |
tier |
hot | cold |
size_bytes |
Quota math |
access_count |
Incremented on signed URL request |
last_accessed_at |
Updated on read |
priority |
0–100 business weight |
created_at |
Upload time |
Eviction score
When hot storage exceeds the high watermark, sort by descending score (migrate highest first).
score = α × days_since_last_access
+ β / log₂(access_count + 2)
+ γ × (100 − priority)
Defaults: α=1.0, β=30.0, γ=0.5 — configured via readStorageConfig() in apps/web/src/storage/config.ts.
Priority map
| Source | Condition | Priority |
|---|---|---|
experiment-artifacts |
experiment done |
20 |
paper-images |
paper status read |
30 |
| (default) | — | 50 |
experiment-artifacts |
experiment running |
70 |
vault-assets |
section drafting |
80 |
Tier job behaviour
- Measure R2 usage
- If ≥ 85% of
BLOB_HOT_QUOTA_GB, migrate highest-score hot objects - Stop at 75% or
BLOB_TIER_BATCH_SIZE - Optional promote cold → hot on repeated access (
BLOB_PROMOTE_ON_ACCESS)
Configuration
Set in .env.local or host secrets — see apps/web/src/storage/config.ts for all keys:
BLOB_PROVIDER=tiered # server: supabase | tiered
NEXT_PUBLIC_BLOB_PROVIDER=tiered # browser: supabase | tiered
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=weaveforge-hot
BLOB_COLD_ENDPOINT=
BLOB_COLD_ACCESS_KEY_ID=
BLOB_COLD_SECRET_ACCESS_KEY=
BLOB_COLD_BUCKET=weaveforge-cold
Code layout
apps/web/src/storage/
├── config.ts
├── wire-storage.ts
└── providers/
├── supabase/blob-store.ts ← today
├── s3/blob-store.ts ← Phase 1
└── tiered/tiered-blob-store.ts