convex-file-system

ConvexFS — File Storage for Convex

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "convex-file-system" with this command: npx skills add imfa-solutions/skills/imfa-solutions-skills-convex-file-system

ConvexFS — File Storage for Convex

convex-fs — Path-based file storage with global CDN delivery via bunny.net.

Installation & Setup

  1. Install

npm install convex-fs

  1. Register component

// convex/convex.config.ts import { defineApp } from "convex/server"; import fs from "convex-fs/convex.config.js";

const app = defineApp(); app.use(fs); export default app;

  1. Create ConvexFS instance

// convex/fs.ts import { ConvexFS } from "convex-fs"; import { components } from "./_generated/api";

export const fs = new ConvexFS(components.fs, { storage: { type: "bunny", apiKey: process.env.BUNNY_API_KEY!, storageZoneName: process.env.BUNNY_STORAGE_ZONE!, cdnHostname: process.env.BUNNY_CDN_HOSTNAME!, tokenKey: process.env.BUNNY_TOKEN_KEY, // recommended for signed URLs }, });

  1. Register HTTP routes

// convex/http.ts import { httpRouter } from "convex/server"; import { registerRoutes } from "convex-fs"; import { components } from "./_generated/api"; import { fs } from "./fs";

const http = httpRouter();

registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", uploadAuth: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); return identity !== null; }, downloadAuth: async (ctx, blobId, path) => { const identity = await ctx.auth.getUserIdentity(); return identity !== null; }, });

export default http;

Creates: POST /fs/upload (upload proxy) and GET /fs/blobs/{blobId} (302 redirect to CDN).

  1. Environment variables

Set in Convex dashboard (Settings → Environment Variables):

BUNNY_API_KEY=your-api-key BUNNY_STORAGE_ZONE=your-storage-zone-name BUNNY_CDN_HOSTNAME=your-zone.b-cdn.net BUNNY_TOKEN_KEY=your-token-auth-key BUNNY_REGION=ny # Optional: ny, la, sg, uk, se, br, jh, syd (default: Frankfurt)

Core Concepts

Architecture

React Client ──▶ Convex Backend (ConvexFS) ──▶ bunny.net CDN (File Storage + Edge)

  • File metadata (paths, content types, sizes) → Convex tables

  • File contents (blobs) → bunny.net Edge Storage

Paths

Any UTF-8 string. list() uses prefix matching (not directory listing):

  • prefix: "/users" matches /users/alice.txt AND /users-backup/data.bin

  • prefix: "/users/" matches only under /users/

Blob lifecycle

Upload → Pending (4h TTL) → Committed (refCount=1) → Deleted (refCount=0) → GC cleanup

  • Reference counting: copy increments refCount (zero-copy), delete decrements it.

  • Garbage collection: 3 automatic jobs — upload GC (hourly), blob GC (hourly), file expiration GC (every 15s).

  • Grace period: orphaned blobs retained for blobGracePeriod (default 24h) before permanent deletion.

Attributes

interface FileAttributes { expiresAt?: number; // Unix timestamp — auto-deleted by FGC }

Attributes are path-specific: cleared on move, not inherited on copy, removed on overwrite.

Core API

Query methods

// Get file metadata by path const file = await fs.stat(ctx, "/uploads/photo.jpg"); // Returns: { path, blobId, contentType, size, attributes } | null

// List files with pagination const result = await fs.list(ctx, { prefix: "/uploads/", paginationOpts: { numItems: 50, cursor: null }, }); // Returns: { page: FileMetadata[], continueCursor, isDone }

Mutation methods

// Commit uploaded blobs to paths await fs.commitFiles(ctx, [ { path: "/file.txt", blobId: "uuid-here" }, // overwrite if exists { path: "/new.txt", blobId: "uuid", basis: null }, // FAIL if exists { path: "/update.txt", blobId: "new-uuid", basis: "old-uuid" }, // CAS: fail if changed ]);

// Atomic multi-operation transaction await fs.transact(ctx, [ { op: "move", source: file, dest: { path: "/new/path.txt" } }, { op: "copy", source: file, dest: { path: "/backup.txt", basis: null } }, { op: "delete", source: file }, { op: "setAttributes", source: file, attributes: { expiresAt: Date.now() + 3600000 } }, ]);

// Convenience methods await fs.move(ctx, "/old.txt", "/new.txt"); // throws if source missing or dest exists await fs.copy(ctx, "/a.txt", "/b.txt"); // throws if source missing or dest exists await fs.delete(ctx, "/file.txt"); // idempotent (no-op if missing)

Basis values (for commitFiles and transact destinations):

Value Meaning

undefined

No check — overwrite if exists

null

File must NOT exist

"blobId"

File's current blobId must match (compare-and-swap)

Action methods

// Generate signed download URL const url = await fs.getDownloadUrl(ctx, blobId, { extraParams: { filename: "doc.pdf" } });

// Download blob data const data = await fs.getBlob(ctx, blobId); // ArrayBuffer | null

// Download file contents + metadata const result = await fs.getFile(ctx, "/file.txt"); // { data, contentType, size } | null

// Upload data and get blobId const blobId = await fs.writeBlob(ctx, imageData, "image/webp");

// Upload + commit in one call await fs.writeFile(ctx, "/report.pdf", pdfData, "application/pdf");

Client utilities

import { buildDownloadUrl } from "convex-fs";

const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path, { filename: "doc.pdf" });

Upload & Serve Flow

Upload (React)

const handleUpload = async (file: File) => { const siteUrl = (import.meta.env.VITE_CONVEX_URL ?? "").replace(/.cloud$/, ".site");

// 1. Upload blob const res = await fetch(${siteUrl}/fs/upload, { method: "POST", headers: { "Content-Type": file.type }, body: file, }); const { blobId } = await res.json();

// 2. Commit to path (via mutation) await commitFile({ blobId, filename: file.name }); };

Serve (React)

import { buildDownloadUrl } from "convex-fs";

function Image({ path }: { path: string }) { const file = useQuery(api.files.getFile, { path }); const siteUrl = (import.meta.env.VITE_CONVEX_URL ?? "").replace(/.cloud$/, ".site");

if (!file) return <div>Loading...</div>; const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path); return <img src={url} alt={path} />; }

Security Rules

  • Always authenticate uploads — open upload endpoints are a serious risk.

  • Use path-based authorization — validate path ownership in downloadAuth .

  • Enable token authentication on bunny.net — prevents URL tampering.

  • Set appropriate URL TTLs: sensitive content 60–300s, general 3600s (default), streaming 3600s+.

Conflict Handling

import { ConvexError } from "convex/values"; import { isConflictError } from "convex-fs";

try { await fs.commitFiles(ctx, files); } catch (e) { if (e instanceof ConvexError && isConflictError(e.data)) { // e.data: { code, path, expected, found } // Codes: SOURCE_NOT_FOUND, SOURCE_CHANGED, DEST_EXISTS, DEST_NOT_FOUND, DEST_CHANGED, CAS_CONFLICT } }

Constructor Options

Option Type Default Description

storage

StorageConfig

required Bunny.net backend config

downloadUrlTtl

number

3600

Signed URL expiration (seconds)

blobGracePeriod

number

86400

Orphaned blob retention (seconds)

Reference Files

  • Patterns & examples: User files, temp files, atomic ops, CAS updates, retry, pagination, React hooks → See references/examples.md

  • Advanced topics: Multiple filesystems, disaster recovery, testing, GC details, Bunny.net setup, TypeScript types, troubleshooting → See references/advanced.md

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

rn-skia

No summary provided by upstream source.

Repository SourceNeeds Review
General

rn-reanimated

No summary provided by upstream source.

Repository SourceNeeds Review
General

rn-heroui

No summary provided by upstream source.

Repository SourceNeeds Review