General-purpose image/file hosting on Cloudflare R2. Stores files via authenticated upload, serves them at public URLs. Currently used for embedding diagrams in Capacities notes, but designed to work for any use case (Obsidian assets, blog images, etc).
We capture learning notes from AI coding sessions and push them to Capacities as a personal knowledge base. These notes often include diagrams, architecture sketches, and screenshots that make the content useful months later. Capacities renders images via URL, but it doesn't host files. We needed somewhere to put them.
The obvious options are S3, GCS, or just throwing images into Google Drive. Google Drive doesn't give you a direct file URL that renders inline. It wraps everything in a viewer, which breaks markdown image embeds. S3 and GCS work, but they charge egress fees every time someone loads an image. For assets that get viewed repeatedly (notes you revisit, shared links), egress adds up.
| R2 | S3 | GCS | Google Drive | |
|---|---|---|---|---|
| Egress | Free | $0.09/GB | $0.12/GB | Free |
| Direct URL | Yes | Yes | Yes | No (viewer only) |
| Free storage | 10 GB | 5 GB | 5 GB | 15 GB (shared) |
| Inline embed | Yes | Yes | Yes | No |
| Auth for reads | None needed | Presigned URLs or public | Presigned URLs or public | Share link required |
Cloudflare R2 is S3-compatible object storage with zero egress fees and a generous free tier. Upload a file, get a permanent public URL, embed it in a note. No viewer wrappers, no expiring links, no surprise bills.
Any client (Claude, curl, script)
|
v
POST /upload (auth'd, base64 or binary)
|
v
Cloudflare R2 (assets bucket)
|
v
GET /i/2026/03/diagram-name.png (public, cached forever)
npm install
wrangler login
# Create R2 bucket
wrangler r2 bucket create assets
# Generate upload token
export UPLOAD_TOKEN=$(openssl rand -hex 32)
echo "Save this: $UPLOAD_TOKEN"
# Set the secret
wrangler secret put UPLOAD_TOKEN
# Deploy
wrangler deployLive at: https://<your-worker>.workers.dev
Upload an image. Returns a public URL.
JSON body (base64):
curl -X POST https://<your-worker>.workers.dev/upload \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": "iVBORw0KGgo...", "mime": "image/png", "filename": "tcp-handshake"}'Binary body:
curl -X POST https://<your-worker>.workers.dev/upload \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: image/png" \
-H "X-Filename: tcp-handshake" \
--data-binary @diagram.pngResponse:
{
"url": "https://<your-worker>.workers.dev/i/2026/03/tcp-handshake.png",
"key": "2026/03/tcp-handshake.png",
"size": 45231,
"mime": "image/png"
}Serve a file. Public, no auth. Cached 1 year (immutable content).
Returns {"status": "ok"}.
| Category | Formats |
|---|---|
| Images | PNG, JPEG, SVG, WebP, GIF |
| Documents | |
| Audio | MP3, OGG, WAV |
| Video | MP4, WebM |
| Text | JSON, TXT, Markdown |
Max 5MB per file.
R2 free tier: 10GB storage, 10M reads/month. Worker free tier: 100k req/day. Total: $0.
- knowledge-capture skill -- uploads diagrams from Claude sessions, embeds public URLs in Capacities notes
- (future) Obsidian vault image hosting
- (future) Blog/static site assets