Skip to content
Deployment
Skill

/pinme-r2

Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials

From plugin
pinme
3.8k7 skills

Claude Code

$ npx -y skills add glitternetwork/pinme --skill pinme-r2 --agent claude-code

SKILL.md

pinme-r2.SKILL.md
name: pinme-r2
description: Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials or manual Wrangler configuration.

PinMe Worker R2 Storage

Use the project-scoped R2 bucket that PinMe binds to every deployed Worker as `env.R2`. Do not create credentials, choose a bucket name, or edit generated Wrangler configuration.

Runtime Contract

PinMe rebuilds trusted Worker metadata on create, save, and update. Client metadata cannot replace the R2 binding.

| Binding | TypeScript type | Availability | | --- | --- | --- | | `DB` | `D1Database` | Always injected | | `R2` | `R2Bucket` | Always injected; current project's bucket | | `API_KEY` | `string` | Always injected | | `LLM_API_KEY` | `string` | Always injected | | `BASE_URL` | `string` | Always injected | | `WORKER_URL` | `string` | Always injected | | `PROJECT_NAME` | `string` | Always injected |

Payment-specific bindings such as `UNIWEB_SECRET` are conditional and unrelated to R2 access.

Declare only the bindings used by the Worker module. R2 code normally starts with:

export interface Env {
  R2: R2Bucket;
  PROJECT_NAME: string;
  WORKER_URL: string;
}

When the same module coordinates file metadata in D1, also declare `DB: D1Database` as a required field.

Choose R2 or D1

  • Use R2 for file bodies, images, attachments, media, exports, and other objects addressed by key.
  • Use D1 for searchable business metadata, ownership, relations, status, and audit fields.
  • For managed files, store the body in R2 and store only its key and business metadata in D1.
  • Never use Worker local filesystem state for persistence and never store complete files or base64 payloads in D1.

Required Security Workflow

Apply this sequence to every upload, download, metadata, delete, and list route:

authenticate request
→ authorize the project/user action
→ validate size and media policy
→ generate or normalize a scoped object key
→ call env.R2
→ return a sanitized response

Use the application's existing authentication. The examples below accept a trusted `userId` that the route must obtain from verified identity claims, never from an untrusted request body or query parameter.

Keep object keys server-controlled. Prefer opaque IDs under an owner prefix:

const FILE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

function ownerPrefix(userId: string): string {
  if (!userId) throw new Error('Authenticated user id is required');
  return `users/${encodeURIComponent(userId)}/files/`;
}

function objectKey(userId: string, fileId: string): string {
  if (!FILE_ID_RE.test(fileId)) throw new Error('Invalid file id');
  return `${ownerPrefix(userId)}${fileId}`;
}

Never accept a complete object key from the client. Reject empty identifiers, `.` or `..` segments, backslashes, control characters, and any attempt to access another user's prefix.

Shared Helpers

Use small helpers and explicit business limits. Adapt the allowlist to the product rather than accepting every client-supplied media type.

const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
const ALLOWED_CONTENT_TYPES = new Set([
  'image/jpeg',
  'image/png',
  'image/webp',
  'application/pdf',
]);

function json(data: unknown, status = 200): Response {
  return Response.json(data, { status });
}

function safeDownloadName(value: string | null): string {
  const cleaned = (value || 'download')
    .replace(/[\r\n"\\]/g, '_')
    .replace(/[\x00-\x1f\x7f]/g, '')
    .trim();
  return (cleaned || 'download').slice(0, 128);
}

function requestedFileId(request: Request): string | null {
  const url = new URL(request.url);
  const value = url.pathname.split('/').filter(Boolean).at(-1) || '';
  return FILE_ID_RE.test(value) ? value : null;
}

Client filenames and `Content-Type` are hints, not proof of content. For sensitive formats, inspect magic bytes or send the object through an asynchronous validation/scanning workflow before marking it ready.

Stream an Upload

Require authentication before calling this handler. Pass `request.body` directly to R2; do not call `arrayBuffer()`, `text()`, `json()`, `formData()`, or base64 conversion first.

async function handleUpload(
  request: Request,
  env: Env,
  userId: string,
): Promise<Response> {
  if (!request.body) return json({ error: 'File body is required' }, 400);

  const lengthHeader = request.headers.get('content-length');
  if (!lengthHeader) return json({ error: 'Content-Length is required' }, 411);

  const declaredSize = Number(lengthHeader);
  if (!Number.isSafeInteger(declaredSize) || declaredSize < 0) {
    return json({ error: 'Invalid Content-Length' }, 400);
  }
  if (declaredSize > MAX_UPLOAD_BYTES) {
    return json({ error: 'File is too large' }, 413);
  }

  const contentType = (request.headers.get('content-type') || '')
    .split(';', 1)[0]
    .trim()
    .toLowerCase();
  if (!ALLOWED_CONTENT_TYPES.has(contentType)) {
    return json({ error: 'Unsupported media type' }, 400);
  }

  const fileId = crypto.randomUUID();
  const key = objectKey(userId, fileId);
  const filename = safeDownloadName(request.headers.get('x-file-name'));

  const object = await env.R2.put(key, request.body, {
    httpMetadata: {
      contentType,
      contentDisposition: `attachment; filename="${filename}"`,
    },
    customMetadata: { ownerId: userId },
  });

  if (object === null) return json({ error: 'Upload precondition failed' }, 412);

  // Content-Length is only a precheck. Enforce the actual stored size too.
  if (object.size > MAX_UPLOAD_BYTES) {
    await env.R2.delete(key);
    return json({ error: 'File is too large' }, 413);
  }

  return json({ id: fileId, size: object.size, etag: object.httpEtag }
Read more
@glitternetwork
Ships withpinme

PinMe is a zero-config deployment CLI focused on one-command creation and deployment for full-stack projects. It lets you quickly set up and launch a complete project with an integrated frontend, Worker backend, and database, without tedious configuration.

Stars on GitHub

Momentum

stars in total
3.8k
Star history of glitternetwork/pinme on GitHub, a chart by star-history.com

Other skills on pinme.

pinme-auth

pinme-auth

Use when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying…

pinme-email

pinme-email

Use this skill when a PinMe project (Worker TypeScript) needs to integrate email sending (send_email). Guides AI to generate…

pinme-llm

pinme-llm

Use this skill when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models,…

pinme-share

pinme-share

Use this skill when the user wants to share, publish, or upload a static result through PinMe, especially by generating a static…

pinme-uniwebpay

pinme-uniwebpay

Use when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through…

pinme

pinme

Use this skill when the user mentions "pinme", or needs to upload files, store to IPFS, create/publish/deploy websites or…

More skills from other plugins.

configuring-horizon

configuring-horizon

Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing…

coolify9d66k
@coollabsio@coollabsioInstall Skill
vercel-cli

vercel-cli

Guides deployment, configuration, and troubleshooting with the Vercel CLI. Use when operating Vercel projects or resources…

vercel11d16k
@vercel@vercelInstall Skill
skypilot

skypilot

Use when launching cloud VMs, Kubernetes pods, or Slurm jobs for GPU/TPU/CPU workloads, training or fine-tuning models on cloud…

@skypilot-org@skypilot-orgInstall Skill
mlx-serve

mlx-serve

Hook an app, game or script up to the local mlx-serve server for LLM chat, embeddings, image, speech, music, sound effect, video…

mlx-serve11d2.2k
@ddalcu@ddalcuInstall Skill

Questions about pinme-r2.

Which coding agents can use pinme-r2?
pinme-r2 installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.