Authn & Authz
ConvexFS relies on the same conventions as any other Convex project for
authentication and authorization. This means it’s composable with whatever your
app is already doing—including middleware patterns like
customFunction
from convex-helpers.
Access control for the ConvexFS object
Section titled “Access control for the ConvexFS object”When you call fs.stat(), fs.list(), fs.transact(), or any other ConvexFS
method from your own queries, mutations, or actions, you’re in full control.
Just perform your auth checks before calling the fs method—exactly like you
would for any other Convex function.
import { query } from "./_generated/server";import { paginationOptsValidator } from "convex/server";import { v } from "convex/values";import { fs } from "./fs";
export const listMyFiles = query({ args: { prefix: v.optional(v.string()), paginationOpts: paginationOptsValidator, }, handler: async (ctx, args) => { // Standard Convex auth check const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); }
// Check any other access privileges specific to your app here // e.g., verify user has permission to access this prefix
// Now call ConvexFS - user is authenticated return await fs.list(ctx, { prefix: args.prefix, paginationOpts: args.paginationOpts, }); },});This pattern works for all ConvexFS methods. For more sophisticated patterns
like role-based access control or per-file permissions, add your logic before
calling the fs method.
Access control for the component’s HTTP routes
Section titled “Access control for the component’s HTTP routes”In the App setup guide, you registered ConvexFS’s HTTP routes with placeholder auth callbacks:
registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", uploadAuth: async () => { // TODO: Add real auth check return true; }, downloadAuth: async () => { // TODO: Add real auth check return true; },});These callbacks are your gatekeepers for the upload and download endpoints. Let’s look at each one.
Upload authentication
Section titled “Upload authentication”The uploadAuth callback is called before any upload is accepted. This is
critical to secure properly—returning true commits your app to accepting
arbitrary content from anyone on the internet.
At minimum, you should verify the user is authenticated:
registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", uploadAuth: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); return identity !== null; }, // ...});You can also implement more granular checks—for example, verifying the user has a specific role or checking rate limits.
Download authentication
Section titled “Download authentication”The downloadAuth callback is called before redirecting to a signed download
URL. The callback receives both the context and the blobId being requested.
Whether you need download authentication depends on your use case:
-
Public assets (marketing images, public documents): You may want to allow unauthenticated access. The simple
return trueis fine here. -
Private user content (personal files, confidential documents): You’ll want to verify the user has permission to access this specific blob.
For private content, you’ll typically need to look up which file(s) reference this blob and check if the user has access:
registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", // ... downloadAuth: async (ctx, blobId) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return false; }
// Look up the file and verify the user can access it // This depends on your app's data model const canAccess = await ctx.runQuery(api.files.canUserAccessBlob, { userId: identity.subject, blobId, }); return canAccess; },});Signed CDN URL security considerations
Section titled “Signed CDN URL security considerations”The download component endpoint (hosted on Convex) doesn’t serve file data directly. Instead, it returns a 302 redirect to a time-limited signed URL on the Bunny.net CDN. This has important security implications:
Once a user has the signed URL, they can access the blob directly from the CDN—no further auth checks occur. The URL remains valid until it expires, even if you revoke the user’s access in your app.
This is a deliberate tradeoff. Serving files through Convex would be slower and
more expensive than serving directly from a global CDN. But it means your
downloadAuth callback is a gate, not a continuous guard.
In practice, when a user’s access is revoked, your app’s queries and reactivity will stop providing them with the CDN URL. However, it’s technically possible for the user to have copied the URL or retained it in their browser cache. In that case, they can continue to access the asset directly from the CDN until the signed token expires—even if your app no longer displays it.
Configuring URL expiration
Section titled “Configuring URL expiration”You can control how long signed URLs remain valid with the downloadUrlTtl
option (in seconds) when creating your ConvexFS instance:
const fs = new ConvexFS(components.fs, { storage: { /* ... */ }, downloadUrlTtl: 300, // URLs expire after 5 minutes (default: 3600 = 1 hour)});Shorter TTLs (e.g., 60–300 seconds):
- Reduce the window where a revoked user can still access content
- Better for highly sensitive content
- Users may need to re-request URLs more often for long viewing sessions
Longer TTLs (e.g., 3600+ seconds):
- Better user experience for media playback and large downloads
- Fewer requests to your Convex backend
- Larger window where revoked access still works
For most apps, the default of 1 hour is a reasonable balance. If you’re building something with strict access control requirements (e.g., paid content, confidential documents), consider shorter TTLs and accept the UX tradeoff.