# R2

> Write Cloudflare R2 object-storage code for uploads, downloads, metadata, streaming, signed access patterns, large files, generated exports, public assets, and D1/R2 metadata separation. Use when Workers handle blobs or egress-sensitive storage.

- Skill: `zllovesuki/r2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add zllovesuki/r2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zllovesuki/r2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: zllovesuki (https://skillmd.com/u/zllovesuki)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/zllovesuki/r2

---

# R2

Use this skill when code needs object/blob storage. R2 stores bytes; keep queryable metadata in D1 or Durable Object state.

## Best uses

- User uploads.
- Images, video, audio, attachments.
- Generated exports/reports.
- Backups and archives.
- Parsed document chunks for RAG pipelines.
- Public or signed downloads.

## Key design

Use predictable, tenant-safe keys that avoid collisions and make cleanup possible.

```text
tenants/{tenantId}/uploads/{yyyy}/{mm}/{objectId}-{safeFilename}
tenants/{tenantId}/exports/{jobId}.csv
documents/{documentId}/chunks/{chunkIndex}.txt
```

Do not expose raw keys to users unless the key contains no sensitive information and authorization is still enforced.

## Upload pattern

```ts
export interface Env {
  BUCKET: R2Bucket;
  DB: D1Database;
}

export async function upload(request: Request, env: Env, tenantId: string) {
  const contentType = request.headers.get("content-type") ?? "application/octet-stream";
  const objectId = crypto.randomUUID();
  const key = `tenants/${tenantId}/uploads/${objectId}`;

  await env.BUCKET.put(key, request.body, {
    httpMetadata: { contentType },
    customMetadata: { tenantId }
  });

  await env.DB.prepare(
    `INSERT INTO files (id, tenant_id, r2_key, content_type, created_at)
     VALUES (?, ?, ?, ?, ?)`
  ).bind(objectId, tenantId, key, contentType, new Date().toISOString()).run();

  return Response.json({ id: objectId });
}
```

## Download pattern

```ts
export async function download(env: Env, tenantId: string, fileId: string) {
  const row = await env.DB.prepare(
    "SELECT r2_key, content_type FROM files WHERE tenant_id = ? AND id = ?"
  ).bind(tenantId, fileId).first<{ r2_key: string; content_type: string }>();

  if (!row) return new Response("Not found", { status: 404 });

  const object = await env.BUCKET.get(row.r2_key);
  if (!object) return new Response("Not found", { status: 404 });

  return new Response(object.body, {
    headers: {
      "Content-Type": object.httpMetadata?.contentType ?? row.content_type,
      "ETag": object.httpEtag
    }
  });
}
```

## Metadata rules

- R2 custom metadata is useful for object-local hints.
- D1 is better for querying, ownership, ACLs, status, and indexes.
- Durable Objects are better for live coordination around an upload/session.
- KV can cache public metadata but should not be the source of truth.

## Safety checks

- Enforce auth before reading/writing objects.
- Limit accepted content types and sizes before processing.
- Stream large data; do not buffer entire files in memory.
- Generate canonical keys server-side.
- Consider malware scanning or content moderation for user uploads where appropriate.
- Delete metadata and object bytes together through a workflow or idempotent cleanup job.

## Anti-patterns

- Listing R2 keys as the only authorization mechanism.
- Keeping all file metadata only in object custom metadata.
- Reading large objects into memory before returning them.
- Using D1 or KV for large file contents.

