UploadThing Track
Convex component for tracking UploadThing files—access control, expiration, webhook verification, and metadata.
UploadThing handles storage. This component adds the metadata layer: who uploaded what, who can see it, and when it expires.
#Features
- File tracking — URL, key, name, size, MIME type, upload time
- User association — Ties each file to
userIdfor ownership - Access control — Per-file and per-folder visibility (public / private / restricted)
- Expiration — TTL by file, MIME type, file type, or global default
- Replacement — Re-upload with same key updates the record in place
- Tags and filters — Query by user, folder, tag, MIME type
- Cross-user queries —
listAllFilesfor galleries, feeds, shared boards - Webhook verification — HMAC SHA-256 for UploadThing callbacks
- Remote cleanup — Optionally delete from UploadThing servers when expired
#Demo
ephemera-upt-demo.vercel.app — Ephemera, an ephemeral image board built with this component. Posts auto-expire (1h, 24h, 7d), live countdowns, anonymous auth.
#Installation
npm install @mzedstudio/uploadthingtrack#Setup
#1. Register the component
ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import uploadthingFileTracker from "@mzedstudio/uploadthingtrack/convex.config.js";
const app = defineApp();
app.use(uploadthingFileTracker, { name: "uploadthingFileTracker" });
export default app;#2. Create the client
ts
// convex/uploadthing.ts
import { UploadThingFiles } from "@mzedstudio/uploadthingtrack";
import { components } from "./_generated/api";
const uploadthing = new UploadThingFiles(components.uploadthingFileTracker);#3. Mount webhook route
ts
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "@mzedstudio/uploadthingtrack";
import { components } from "./_generated/api";
const http = httpRouter();
registerRoutes(http, components.uploadthingFileTracker);
export default http;Set UPLOADTHING_API_KEY in the Convex dashboard. Webhook path: /webhooks/uploadthing.
#4. Configure (optional)
ts
await uploadthing.setConfig(ctx, {
config: {
defaultTtlMs: 30 * 24 * 60 * 60 * 1000,
ttlByMimeType: { "image/png": 90 * 24 * 60 * 60 * 1000 },
ttlByFileType: { avatar: 365 * 24 * 60 * 60 * 1000 },
deleteRemoteOnExpire: true,
},
});#Usage
#Querying files
ts
export const listMyFiles = query({
args: { userId: v.string() },
handler: async (ctx, args) => {
return await uploadthing.listFiles(ctx, {
ownerUserId: args.userId,
viewerUserId: args.userId,
});
},
});
export const getFile = query({
args: { key: v.string(), viewerUserId: v.optional(v.string()) },
handler: async (ctx, args) => {
return await uploadthing.getFile(ctx, args);
},
});#Cross-user listing
ts
export const publicGallery = query({
args: { viewerUserId: v.optional(v.string()) },
handler: async (ctx, args) => {
return await uploadthing.listAllFiles(ctx, {
viewerUserId: args.viewerUserId,
folder: "gallery",
limit: 20,
});
},
});#Inserting files
ts
await uploadthing.upsertFile(ctx, {
file: { key, url, name, size, mimeType },
userId: args.userId,
options: { folder: "uploads", tags: ["document"], metadata: { uploaderName } },
});#Access control
ts
await uploadthing.setFileAccess(ctx, {
key: "file_abc",
access: { visibility: "public" },
});
await uploadthing.setFolderAccess(ctx, {
folder: "team-docs",
access: {
visibility: "restricted",
allowUserIds: ["user_1", "user_2"],
},
});File-level rules override folder rules. Deny lists beat allow lists.
#Filtering
ts
await uploadthing.listFiles(ctx, { ownerUserId, tag: "avatar" });
await uploadthing.listFiles(ctx, { ownerUserId, mimeType: "image/png" });
await uploadthing.listFiles(ctx, { ownerUserId, folder: "documents" });#Usage stats and cleanup
ts
const stats = await uploadthing.getUsageStats(ctx, { userId });
// { totalFiles: 42, totalBytes: 1048576 }
const preview = await uploadthing.cleanupExpired(ctx, { dryRun: true });
const result = await uploadthing.cleanupExpired(ctx, { batchSize: 100 });
// { deletedCount, keys, hasMore, remoteDeleteFailed?, remoteDeleteError? }#TTL precedence
- Explicit
expiresAtorttlMson file ttlByFileTypefrom configttlByMimeTypefrom configdefaultTtlMsfrom config- No expiration
#API Summary
| Method | Context | Notes |
|---|---|---|
upsertFile | mutation | Insert or replace by key |
getFile | query | With access control |
listFiles | query | Per-user with filters |
listAllFiles | query | Cross-user with access control |
deleteFiles | mutation | By key |
setFileAccess / setFolderAccess | mutation | Visibility rules |
getUsageStats | query | Files and bytes per user |
cleanupExpired | action | Batch delete expired (optional remote delete) |