---
title: "UploadThing Track"
description: "Convex component for tracking UploadThing files: access control, expiry, and webhooks."
slug: "uploadthingtrack"
tags: ["Convex", "UploadThing", "File Storage", "TypeScript"]
url: "https://github.com/raymond-UI/uploadthingtrack"
repo: "raymond-UI/uploadthingtrack"
embedUrl: "https://ephemera-upt-demo.vercel.app"
---


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 queries** — `listAllFiles` 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](https://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

<ToolInstall cmd="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

| 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) |

<ToolRepo href="https://github.com/raymond-UI/uploadthingtrack" title="View on GitHub" />
