Agent skill

Convex File Storage

by waynesutton in waynesutton/builder-skills

Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the storage table, and deletion.

Apache-2.0Auto-check passedBackend & APIs

Install Convex File Storage

skills CLI
$ npx skills add waynesutton/builder-skills --skill convex-file-storage -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install waynesutton/builder-skills convex-file-storage --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/waynesutton/builder-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/convex-file-storage .claude/skills/convex-file-storage && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
convex-file-storage
GitHub stars
406
Token cost
~2.7k tokens
SKILL.md length
589 words
Files
4 (incl. assets)
Skills in repo
17
Repo updated
First seen
Licence
Apache-2.0

At a glance

Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the storage table, and deletion.

  • Users upload images
  • SKILL.md covers Schema, Upload flow, Storing a blob from an action and Serving with getUrl, plus 6 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md
  • An action fetches a file from a third party

What it does

Convex File Storage is an agent skill from waynesutton/builder-skills. Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the storage table, and deletion. Use when users upload images or documents, when an action fetches a file from a third party, or when a file URL expires unexpectedly.

Its SKILL.md is about 2.7k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including assets (for example `agents/openai.yaml`).

It sits in Backend & APIs, covering File uploads and storage. The repository describes itself as: Builder skills for Convex apps. Convex patterns plus a PRD, task.md, changelog, and files.md workflow for Claude Code, Codex, Cursor, and OpenCode. The licence is Apache-2.0.

When your agent uses it

  • Users upload images
  • An action fetches a file from a third party
  • A file URL expires unexpectedly

Example prompts

  • “Use the convex-file-storage skill to handle files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action…”
  • “/convex-file-storage”

What it can do on your machine

Read from SKILL.md and the folder at commit 82d1ce2. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • docs.convex.dev

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Convex File Storage loads about 2.7k tokens when it runs. Until then it costs about 77 tokens; SKILL.md has 589 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~77
When it runs · the whole SKILL.md, loaded when a task matches
~2.7k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from waynesutton/builder-skills at commit 82d1ce2, republished under its Apache-2.0 licence (© waynesutton). 589 words, ~2,674 tokens.

Download SKILL.mdSave it as .claude/skills/convex-file-storage/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.
name
convex-file-storage
description
Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the _storage table, and deletion. Use when users upload images or documents, when an action fetches a file from a third party, or when a file URL expires unexpectedly.

Convex file storage

Produces the upload, store, serve, and delete functions for files in Convex. The one rule: persist the Id<"_storage">, never the URL string. URLs are resolved in queries with ctx.storage.getUrl at read time.

Schema

Keep your own table for app metadata (name, owner, purpose) and point at _storage by id. Convex keeps size, content type, and hash in the _storage system table.

typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  files: defineTable({
    storageId: v.id("_storage"),
    name: v.string(),
    contentType: v.string(),
    size: v.number(),
    ownerId: v.id("users"),
  }).index("by_owner", ["ownerId"]),
});

Upload flow

Three steps: a mutation hands out a short lived upload URL, the client POSTs the file to it, then a second mutation saves the returned storageId. Upload URLs expire after one hour, so generate a fresh one per upload and never store it.

typescript
// convex/files.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";

const MAX_BYTES = 10 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "application/pdf"];

export const generateUploadUrl = mutation({
  args: {},
  returns: v.string(),
  handler: async (ctx) => {
    await getCurrentUser(ctx); // only signed in users get an upload slot
    return await ctx.storage.generateUploadUrl();
  },
});

export const saveFile = mutation({
  args: { storageId: v.id("_storage"), name: v.string() },
  returns: v.id("files"),
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    // Server side validation reads the real size and type from _storage.
    // Client checks are for UX only; anyone can POST to the upload URL.
    const meta = await ctx.db.system.get(args.storageId);
    if (!meta) throw new Error("Upload not found");
    if (meta.size > MAX_BYTES || !ALLOWED_TYPES.includes(meta.contentType ?? "")) {
      await ctx.storage.delete(args.storageId);
      throw new Error("File type or size not allowed");
    }

    return await ctx.db.insert("files", {
      storageId: args.storageId,
      name: args.name,
      contentType: meta.contentType ?? "application/octet-stream",
      size: meta.size,
      ownerId: user._id,
    });
  },
});
tsx
// src/FileUploader.tsx
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
import { useState } from "react";

const MAX_BYTES = 10 * 1024 * 1024;

export function FileUploader() {
  const generateUploadUrl = useMutation(api.files.generateUploadUrl);
  const saveFile = useMutation(api.files.saveFile);
  const [status, setStatus] = useState<"idle" | "uploading" | "error">("idle");

  async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    if (file.size > MAX_BYTES) return setStatus("error");

    setStatus("uploading");
    try {
      const uploadUrl = await generateUploadUrl();
      const res = await fetch(uploadUrl, {
        method: "POST",
        headers: { "Content-Type": file.type }, // recorded as the file's contentType
        body: file,
      });
      if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
      const { storageId } = (await res.json()) as { storageId: Id<"_storage"> };
      await saveFile({ storageId, name: file.name });
      setStatus("idle");
    } catch {
      setStatus("error");
    }
  }

  return <input type="file" accept="image/*,.pdf" onChange={handleChange} disabled={status === "uploading"} />;
}

Storing a blob from an action

ctx.storage.store is available in actions and HTTP actions, not in mutations. Plain fetch works in the default runtime, so "use node" is only needed when a Node library produces the bytes. When building bytes yourself, wrap them first: new Blob([bytes], { type: "application/pdf" }). Without a type the file serves as application/octet-stream.

typescript
// convex/importFile.ts
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const importFromUrl = action({
  args: { url: v.string(), name: v.string(), ownerId: v.id("users") },
  returns: v.id("files"),
  handler: async (ctx, args) => {
    const response = await fetch(args.url);
    if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);

    // response.blob() carries the Content-Type header into blob.type
    const blob = await response.blob();
    const storageId = await ctx.storage.store(blob);

    return await ctx.runMutation(internal.files.saveImported, {
      storageId,
      name: args.name,
      ownerId: args.ownerId,
    });
  },
});

Serving with getUrl

Resolve the URL inside the query that returns the file. The client renders url directly in img, iframe, or a download link.

typescript
// convex/files.ts
export const listMine = query({
  args: {},
  returns: v.array(
    v.object({
      _id: v.id("files"),
      name: v.string(),
      contentType: v.string(),
      size: v.number(),
      url: v.union(v.string(), v.null()),
    }),
  ),
  handler: async (ctx) => {
    const user = await getCurrentUser(ctx);
    const files = await ctx.db
      .query("files")
      .withIndex("by_owner", (q) => q.eq("ownerId", user._id))
      .order("desc")
      .collect();
    return await Promise.all(
      files.map(async (f) => ({
        _id: f._id,
        name: f.name,
        contentType: f.contentType,
        size: f.size,
        url: await ctx.storage.getUrl(f.storageId),
      })),
    );
  },
});

getUrl returns null when the file was deleted. Handle that in the UI instead of assuming a string.

Serving through an HTTP action

Use this when the file needs an auth check, a custom Content-Disposition, or a stable path at https://<deployment>.convex.site/files/<storageId>. ctx.storage.get returns the Blob. Add the route to the router in convex/http.ts.

typescript
// convex/http.ts
import { Id } from "./_generated/dataModel";

http.route({
  pathPrefix: "/files/",
  method: "GET",
  handler: httpAction(async (ctx, request) => {
    const storageId = new URL(request.url).pathname.slice("/files/".length) as Id<"_storage">;
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return new Response("Unauthorized", { status: 401 });

    const blob = await ctx.storage.get(storageId);
    if (!blob) return new Response("Not found", { status: 404 });
    return new Response(blob, {
      headers: { "Content-Type": blob.type || "application/octet-stream" },
    });
  }),
});

Metadata from the _storage table

Read metadata with ctx.db.system.get(storageId) in any query or mutation, as saveFile does above. ctx.storage.getMetadata is deprecated and should not appear in new code. The document shape:

typescript
type StorageDoc = {
  _id: Id<"_storage">;
  _creationTime: number;
  contentType?: string; // from the upload's Content-Type header or blob.type
  sha256: string;
  size: number; // bytes
};

The matching validator is v.object({ _id: v.id("_storage"), _creationTime: v.number(), contentType: v.optional(v.string()), sha256: v.string(), size: v.number() }). ctx.db.system.query("_storage") lists every stored file, useful for finding orphans that no files row points to.

Deleting

Delete the blob and the row in the same mutation so neither is orphaned. Check ownership on the row, not by trusting the client. If several rows can share one storageId, only delete the blob when the last reference goes.

typescript
// convex/files.ts
export const remove = mutation({
  args: { fileId: v.id("files") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    const file = await ctx.db.get(args.fileId);
    if (!file) return null; // idempotent: already gone
    if (file.ownerId !== user._id) throw new Error("Not allowed");

    await ctx.storage.delete(file.storageId);
    await ctx.db.delete(args.fileId);
    return null;
  },
});
Show full SKILL.md (244 more words)Show less

Common mistakes

MistakeWhy it breaksDo instead
Saving the URL from getUrl or the upload URL in a documentUpload URLs expire in an hour; stored URLs go stale after deletesStore Id<"_storage">, resolve with getUrl in the query
v.string() for the storage idLoses type safety, accepts garbagev.id("_storage")
Calling ctx.storage.store in a mutationNot available thereUse an action or HTTP action, then runMutation to save
Skipping Content-Type on the upload POSTFile serves as application/octet-stream, images will not render inlineSet headers: { "Content-Type": file.type }
Validating size and type only in the browserAnyone can POST to the upload URL directlyCheck again with ctx.db.system.get in the save mutation
ctx.storage.getMetadata(id)Deprecatedctx.db.system.get(id)
Deleting the files row but not the blobStorage bill keeps growingDelete both in one mutation
Uploading large files through an HTTP action20MB request capUse the upload URL flow

Checklist

  • Schema stores v.id("_storage") plus app level name, type, size, and owner
  • generateUploadUrl requires a signed in user, and the client POSTs with Content-Type: file.type
  • Save mutation validates size and content type again via ctx.db.system.get and deletes rejected uploads
  • Queries return url from ctx.storage.getUrl and the UI handles null
  • Actions use ctx.storage.store(blob) with a correct type, then runMutation to save the reference
  • HTTP serving route checks auth before ctx.storage.get
  • Delete mutation removes the blob and the row together and is idempotent
  • No ctx.storage.getMetadata calls and no URL strings persisted in the database

Docs

© waynesutton, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 3 other files (assets) in skills/convex-file-storage of waynesutton/builder-skills.

  • SKILL.md
  • agents/openai.yaml
  • assets/large-logo.png
  • assets/small-logo.svg

Open the folder on GitHubat commit 82d1ce2

Compare with similar skills

Convex File Storage next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Convex File Storage compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Convex File Storage this skillwaynesutton/builder-skills406—~2.7kAutomated safety check: PassApache-2.0
Stripe Projectsfossasia/eventyay1.7k5 repos~2kAutomated safety check: NotesApache-2.0
FoundatioFoundatioFx/Foundatio2.1k—~3.9kAutomated safety check: PassApache-2.0
Spatialduckdb/duckdb-skills6041 repos~1kAutomated safety check: NotesMIT
R Oopab604/claude-code-r-skills2062 repos~1.9kAutomated safety check: PassMIT
Edgestore Setupedgestorejs/edgestore454—~1.8kAutomated safety check: PassMIT

Similar skills

  • Stripe Projects

    fossasia/eventyay

    A skill your agent uses when the user wants to provision infrastructure or third-party services using Stripe Projects.

    1.7k GitHub starsUsed in 5 repos~2k tokens
    Backend & APIsAuto-check: notes
  • Foundatio

    FoundatioFx/Foundatio

    A skill your agent uses when working with Foundatio infrastructure abstractions for .NET -- caching, queuing, messaging, file storage, distributed locking, or background jobs.

    2.1k GitHub stars~3.9k tokensUpdated today
    Backend & APIsAuto-check passed
  • Spatial

    duckdb/duckdb-skills

    Official

    Answer questions about spatial data using DuckDB. An agent skill from duckdb/duckdb-skills.

    604 GitHub starsUsed in 1 repo~1k tokens
    Backend & APIsAuto-check: notes
  • R Oop

    ab604/claude-code-r-skills

    R object-oriented programming guide for S7, S3, S4, and vctrs.

    206 GitHub starsUsed in 2 repos~1.9k tokens
    Backend & APIsAuto-check passed
  • Edgestore Setup

    edgestorejs/edgestore

    A skill your agent uses when adding, extending, or troubleshooting EdgeStore file uploads, upload UI, or bucket access policies in a TypeScript/React application.

    454 GitHub stars~1.8k tokensUpdated 3 days ago
    Backend & APIsAuto-check passed
  • S3 Explore

    duckdb/duckdb-skills

    Official

    Explore and query data on S3, Cloudflare R2, GCS, MinIO, or any S3-compatible storage.

    604 GitHub starsUsed in 1 repo~848 tokens
    Backend & APIsAuto-check: notes

More from waynesutton/builder-skills

All 17 skills in this repo
  • Convex Agents

    waynesutton/builder-skills

    Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with vector search, and workflows for multi step jobs.

    406 GitHub stars~2.2k tokensUpdated 11 days ago
    Auto-check passed
  • Convex Best Practices

    waynesutton/builder-skills

    Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent mutations, avoiding OCC conflicts, thin function wrappers, error handling.

    406 GitHub stars~2.6k tokensUpdated 11 days ago
    Auto-check passed
  • Convex Component Authoring

    waynesutton/builder-skills

    Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup.

    406 GitHub stars~2.6k tokensUpdated 11 days ago
    Auto-check passed
  • Convex Cron Jobs

    waynesutton/builder-skills

    Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs, and cancelling or inspecting the queue.

    406 GitHub stars~2k tokensUpdated 11 days ago
    Auto-check passed
  • Convex HTTP Actions

    waynesutton/builder-skills

    Adds HTTP endpoints in convex/http.ts: webhook receivers with signature checks, REST style routes, CORS, auth headers, streaming responses, and file uploads over HTTP.

    406 GitHub stars~2.6k tokensUpdated 11 days ago
    Auto-check passed
  • Convex Migrations

    waynesutton/builder-skills

    Changes a live Convex schema without downtime: make a field optional, backfill in batches, flip the validator, then clean up.

    406 GitHub stars~2.1k tokensUpdated 11 days ago
    Auto-check passed

Categories

Questions about Convex File Storage

What does Convex File Storage do?

Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the storage table, and deletion. Convex File Storage is an agent skill from waynesutton/builder-skills. Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the storage table, and deletion.

When should I use Convex File Storage?

Convex File Storage fits situations like: users upload images; an action fetches a file from a third party; A file URL expires unexpectedly.

How do I install Convex File Storage in Claude Code?

Run `npx skills add waynesutton/builder-skills --skill convex-file-storage -a claude-code`. Or copy the skill folder (skills/convex-file-storage in waynesutton/builder-skills) into .claude/skills/convex-file-storage in your project. Claude Code loads it when a task matches its description.

How do I install Convex File Storage in Codex?

Run `npx skills add waynesutton/builder-skills --skill convex-file-storage -a codex`. Or copy the skill folder (skills/convex-file-storage in waynesutton/builder-skills) into .agents/skills/convex-file-storage in your project. Codex loads it when a task matches its description.

Can I use Convex File Storage in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add waynesutton/builder-skills --skill convex-file-storage -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/convex-file-storage, .gemini/skills/convex-file-storage, .github/skills/convex-file-storage and .opencode/skills/convex-file-storage in your project.

What does Convex File Storage need to run?

SKILL.md names no scripts, command-line tools or credentials: Convex File Storage is instructions for the agent only.

Does Convex File Storage access the network?

SKILL.md names 1 domain. As links in the text: docs.convex.dev. This is read from the text; nothing was executed.

Is Convex File Storage safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Convex File Storage use?

Convex File Storage is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Convex File Storage use?

About 2.7k tokens (SKILL.md is roughly 11k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Convex File Storage?

Skills that share tags, products or a category with Convex File Storage: Stripe Projects (fossasia/eventyay, 1.7k stars), Foundatio (FoundatioFx/Foundatio, 2.1k stars), Spatial (duckdb/duckdb-skills, 604 stars) and R Oop (ab604/claude-code-r-skills, 206 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Convex File Storage?

waynesutton (a GitHub user) maintains it in waynesutton/builder-skills, which has 406 GitHub stars. The repository holds 17 skills in this directory. The repository was last updated on September 28, 2026.

Source: waynesutton/builder-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.