← Tools

UploadThing Track

Tags

ConvexUploadThingFile StorageTypeScript

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 userId for 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 querieslistAllFiles for 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

  1. Explicit expiresAt or ttlMs on file
  2. ttlByFileType from config
  3. ttlByMimeType from config
  4. defaultTtlMs from config
  5. No expiration

#API Summary

MethodContextNotes
upsertFilemutationInsert or replace by key
getFilequeryWith access control
listFilesqueryPer-user with filters
listAllFilesqueryCross-user with access control
deleteFilesmutationBy key
setFileAccess / setFolderAccessmutationVisibility rules
getUsageStatsqueryFiles and bytes per user
cleanupExpiredactionBatch delete expired (optional remote delete)
View on GitHub·raymond-UI/uploadthingtrack
GitHub