npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mzedstudio/uploadthingtrack

v0.4.1

Published

UploadThing file tracking, access control, and cleanup for Convex.

Downloads

682

Readme

@mzedstudio/uploadthingtrack

A Convex component for tracking UploadThing files with access control, expiration, and webhook verification.

UploadThing handles file storage. This component adds the metadata layer: who uploaded what, who can see it, and when it expires.

Features

  • File tracking -- stores URL, key, name, size, MIME type, and upload time for every file
  • User association -- ties each file to a userId for ownership and dashboards
  • Access control -- per-file and per-folder visibility rules (public / private / restricted)
  • Expiration -- configurable TTL by file, MIME type, file type, or a global default
  • Replacement -- re-uploading with the same key updates the record in place
  • Tags and filters -- tag files and query by user, folder, tag, or MIME type
  • Cross-user queries -- list files across all users for galleries, feeds, and shared boards
  • On-demand deletion -- delete specific file records by key
  • Remote cleanup -- optionally delete files from UploadThing servers when they expire
  • Webhook verification -- HMAC SHA-256 signature validation for UploadThing callbacks
  • Cleanup -- batch deletion of expired file records
  • Custom metadata -- store and retrieve arbitrary metadata on file records
  • Usage stats -- total files and bytes per user

Installation

npm install @mzedstudio/uploadthingtrack

Setup

1. Register the component

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

// convex/uploadthing.ts
import { UploadThingFiles } from "@mzedstudio/uploadthingtrack";
import { components } from "./_generated/api";

const uploadthing = new UploadThingFiles(components.uploadthingFileTracker);

3. Mount the webhook route

// 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 as an environment variable in the Convex dashboard. The webhook handler reads it automatically.

4. Configure the component (optional)

export const setup = mutation({
  handler: async (ctx) => {
    await uploadthing.setConfig(ctx, {
      config: {
        uploadthingApiKey: process.env.UPLOADTHING_API_KEY,
        defaultTtlMs: 30 * 24 * 60 * 60 * 1000, // 30 days
        ttlByMimeType: { "image/png": 90 * 24 * 60 * 60 * 1000 },
        ttlByFileType: { avatar: 365 * 24 * 60 * 60 * 1000 },
        deleteRemoteOnExpire: true, // also delete from UploadThing servers
      },
    });
  },
});

Usage

Querying files

import { query } from "./_generated/server";
import { v } from "convex/values";

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 file listing

List files across all users -- useful for galleries, public feeds, and shared boards:

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,
    });
  },
});

listAllFiles applies the same access control as listFiles -- viewers only see files they have permission to access. All filters (folder, tag, mimeType, includeExpired) are supported.

Inserting files manually

import { mutation } from "./_generated/server";

export const trackFile = mutation({
  args: { /* ... */ },
  handler: async (ctx, args) => {
    await uploadthing.upsertFile(ctx, {
      file: {
        key: args.key,
        url: args.url,
        name: args.name,
        size: args.size,
        mimeType: args.mimeType,
      },
      userId: args.userId,
      options: {
        folder: "uploads",
        tags: ["document"],
        metadata: { uploaderName: args.displayName },
      },
    });
  },
});

Deleting files

Delete specific file records by key:

export const removeFiles = mutation({
  args: { keys: v.array(v.string()) },
  handler: async (ctx, args) => {
    const count = await uploadthing.deleteFiles(ctx, { keys: args.keys });
    // count = number of records actually deleted
  },
});

Access control

// Make a file public
await uploadthing.setFileAccess(ctx, {
  key: "file_abc",
  access: { visibility: "public" },
});

// Restrict a folder to specific users
await uploadthing.setFolderAccess(ctx, {
  folder: "team-docs",
  access: {
    visibility: "restricted",
    allowUserIds: ["user_1", "user_2"],
  },
});

// Remove a file-level rule (falls back to folder rule)
await uploadthing.setFileAccess(ctx, { key: "file_abc", access: null });

File-level rules always override folder-level rules. Deny lists take precedence over allow lists.

Filtering

// By tag
await uploadthing.listFiles(ctx, {
  ownerUserId: userId,
  tag: "avatar",
});

// By MIME type
await uploadthing.listFiles(ctx, {
  ownerUserId: userId,
  mimeType: "image/png",
});

// By folder
await uploadthing.listFiles(ctx, {
  ownerUserId: userId,
  folder: "documents",
});

Usage stats

const stats = await uploadthing.getUsageStats(ctx, { userId });
// { totalFiles: 42, totalBytes: 1048576 }

Cleanup

import { action } from "./_generated/server";

export const cleanup = action({
  handler: async (ctx) => {
    // Preview what would be deleted
    const preview = await uploadthing.cleanupExpired(ctx, { dryRun: true });

    // Actually delete expired records
    const result = await uploadthing.cleanupExpired(ctx, { batchSize: 100 });
    // { deletedCount: 12, keys: [...], hasMore: false }
  },
});

When deleteRemoteOnExpire is enabled in config, cleanupExpired also calls the UploadThing API to delete files from their servers before removing local records. If remote deletion fails, local records are preserved so the next run can retry. Check remoteDeleteFailed and remoteDeleteError in the return value for details.

TTL Precedence

When determining a file's expiration, the first match wins:

  1. Explicit expiresAt timestamp
  2. Per-file ttlMs
  3. ttlByFileType from config
  4. ttlByMimeType from config
  5. defaultTtlMs from config
  6. No expiration

API Reference

UploadThingFiles class

| Method | Context | Description | |---|---|---| | upsertFile(ctx, args) | mutation | Insert or replace a file record by key | | getFile(ctx, args) | query | Get a file by key with access control | | listFiles(ctx, args) | query | List files for a specific user with filters | | listAllFiles(ctx, args) | query | List files across all users with access control | | deleteFiles(ctx, args) | mutation | Delete specific file records by key | | setFileAccess(ctx, args) | mutation | Set or clear file-level access rules | | setFolderAccess(ctx, args) | mutation | Set or clear folder-level access rules | | getFolderRule(ctx, args) | query | Get access rule for a folder | | listFolderRules(ctx, args) | query | List all folder access rules | | setConfig(ctx, args) | mutation | Update component configuration | | getConfig(ctx) | query | Read current configuration | | getUsageStats(ctx, args) | query | Get total files and bytes for a user | | cleanupExpired(ctx, args) | action | Delete expired file records (and optionally remote files) | | handleCallback(ctx, args) | action | Handle an UploadThing webhook |

Configuration options

| Option | Type | Description | |---|---|---| | uploadthingApiKey | string | API key for webhook verification and remote deletion | | defaultTtlMs | number | Default TTL in milliseconds for all files | | ttlByMimeType | Record<string, number> | TTL overrides by MIME type | | ttlByFileType | Record<string, number> | TTL overrides by custom file type | | deleteRemoteOnExpire | boolean | Delete files from UploadThing servers on expiration | | deleteBatchSize | number | Max files per cleanup batch (default: 100) |

registerRoutes(http, component, options?)

Mounts the UploadThing webhook at /webhooks/uploadthing (configurable via options.path).

Exported types

  • AccessRule -- { visibility, allowUserIds?, denyUserIds? }
  • FileInfo -- { key, url, name, size, mimeType, ... }
  • FileUpsertOptions -- { tags?, folder?, access?, metadata?, expiresAt?, ttlMs?, fileType? }
  • ConfigUpdate -- { uploadthingApiKey?, defaultTtlMs?, ttlByMimeType?, ... }

Validators (accessRuleValidator, fileInfoValidator, etc.) are also exported for use in your own function definitions.

Testing

This component exports a test helper for use with convex-test:

import { convexTest } from "convex-test";
import { register } from "@mzedstudio/uploadthingtrack/test";
import schema from "./schema";

const modules = import.meta.glob("./**/*.ts");

test("my test", async () => {
  const t = convexTest(schema, modules);
  register(t, "uploadthingFileTracker");
  // ... test your functions that use the component
});

License

Apache-2.0