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.