#418

Feature request: support key prefixes (folders) in Convex File Storage

Requested

Submitted October 14, 2026

Summary

Today, every file in Convex File Storage lives in one flat namespace and is identified only by an opaque Id<"_storage">. I’d like the option to provide a prefix (path-like key, e.g. users/123/avatars/) when storing a file, so files can be organized, listed and managed by prefix, similar to S3/R2/GCS object keys.

Current behavior

ctx.storage.generateUploadUrl() returns an upload URL, and the POST response returns a new storageId. ctx.storage.store(blob) in actions returns a storageId. Neither accepts any naming or path information. The only way to group files is to keep a separate table that maps storageId → logical path, and do all listing, cleanup and access checks through that table.

Proposed API (illustrative)

ts // Actions / HTTP actions const storageId = await ctx.storage.store(blob, { prefix: users/${userId}/avatars/, });

// Upload URL flow const uploadUrl = await ctx.storage.generateUploadUrl({ prefix: projects/${projectId}/attachments/, });

// Querying the _storage system table by prefix const files = await ctx.db.system .query("_storage") .withIndex("by_prefix", (q) => q.eq("prefix", users/${userId}/avatars/)) .collect();

The prefix would be stored as a field on the _storage system document (next to sha256, size, contentType), so it’s visible in the dashboard and queryable.

Use cases

Multi-tenant apps: keep each tenant’s or user’s files grouped (orgs/{orgId}/…), which makes auditing, export and deletion requests (e.g. GDPR) much simpler. Bulk cleanup: delete everything under a prefix when a user, project or temporary upload session is removed, without maintaining a parallel index. Dashboard browsing: navigate files as folders in the Convex dashboard instead of a single flat list. Migration / interoperability: teams coming from S3, R2 or Supabase Storage already organize files by key prefix; matching that model eases migration and backup/export to external buckets. Per-prefix limits or policies (future): a natural hook for things like per-prefix size quotas or lifecycle rules (auto-delete after N days).

Current workaround and its cost

A custom files table with { storageId, path } plus an index on path. It works, but every app reimplements it, it can drift out of sync with _storage (orphaned files when a mutation fails midway), and the dashboard’s file browser can’t use it.

Nice-to-haves (optional)

ctx.storage.list({ prefix, paginationOpts }) helper Prefix filter / folder view in the dashboard Files tab Prefix included in npx convex export / import so structure survives backups

Backward compatibility

Fully additive: the prefix is optional, and existing files simply have no prefix. storageId remains the primary identifier, so nothing changes for current code.

Development Updates

No updates yet