Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Files Control

A Bijection files control component for Bijection.

Bijection source
git clone https://github.com/bijectionhq/files-control.git
cd files-control
git checkout 97f66c96257b27900fa81e5fda9024e857bfbfcc

Bijection repository access is required. Request access.

Description

A Bijection files control component for Bijection.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: bijectionhq/files-control. Based on gilhrpenner/convex-files-control. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

A convex files control component for Convex.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

Files Control provides secure file upload and download workflows with granular access control for Convex applications. It implements two-step uploads with presigned URLs, download grants with usage limits and expiration, and access-key based authorization. The component works with both Convex storage and Cloudflare R2, includes optional HTTP routes for direct uploads/downloads, and provides automated cleanup of expired files and grants.

Use cases

From the upstream package documentation.

  • Multi-tenant applications that need to isolate file access by user ID, organization ID, or other access keys without exposing files across tenants
  • Document sharing workflows where you need time-limited download links with usage caps, optional password protection, and both private and public sharing modes
  • Large file uploads that require presigned URL flows to avoid hitting serverless function limits while maintaining security and validation
  • Hybrid storage architectures that need to seamlessly transfer files between Convex storage and R2 based on usage patterns or cost optimization
  • Applications with strict file lifecycle management requiring automated cleanup of expired uploads, unused grants, and temporary files

How it works

From the upstream package documentation.

The component uses a two-step upload process where you first call generateUploadUrl to get a presigned URL and upload token, upload the file directly to storage, then call finalizeUpload with access keys to register the file. Access control is enforced through normalized access keys (user IDs, tenant IDs, etc.) that you provide during finalization.

Download security works through grants created with createDownloadGrant that specify max uses, expiration times, and optional passwords. The buildDownloadUrl function generates secure URLs, while the optional HTTP router in registerRoutes handles direct upload/download requests with customizable auth hooks like checkUploadRequest and checkDownloadRequest.

The component stores its own access control metadata while your app maintains file records (names, owners, etc.) in separate tables. Built-in queries like listFilesByAccessKeyPage and hasAccessKey let you build file management UIs, while cleanupExpired removes expired uploads, grants, and files when run in scheduled jobs.

Upstream package FAQ

How does Files Control handle file permissions in Convex?

Files Control implements permission layers that check user roles and ownership before allowing uploads or downloads. It integrates with Convex's authentication system to validate access rights and can generate temporary download grants with configurable expiration times.

Can Files Control automatically delete old files?

Yes, Files Control includes lifecycle management with configurable cleanup policies. You can set retention periods, schedule automatic cleanup jobs, and define rules for removing orphaned or expired files to control storage costs.

Does this work with existing Convex file storage?

Files Control builds on top of Convex's native file storage system, adding access control and lifecycle management without changing how files are stored. It works with existing Convex applications and can manage previously uploaded files.

How do I generate secure download links?

Files Control provides mutations that create time-limited download grants with user-specific access validation. These grants generate secure URLs that expire automatically and can be restricted to specific users or roles.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate Files Control with Bijection.

Read: https://bijection.com/components/files-control/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from gilhrpenner/convex-files-control, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

Convex Files Control

A Convex component for secure file uploads, access control, download grants, and lifecycle cleanup. Works with Convex storage and Cloudflare R2, and ships with an optional HTTP upload/download router plus a React upload hook.

Live Demo →

Features

  • Two-step uploads (presigned URL) with access keys and optional expiration.
  • Optional HTTP upload/download routes with auth hooks.
  • Download grants with max uses, expiration, optional password, and shareable links.
  • Access-key based authorization (user IDs, tenant IDs, etc.).
  • Built-in cleanup for expired uploads, grants, and files.
  • Transfer files between Convex and R2.
  • React hook for presigned or HTTP uploads.

Install

npm install @gilhrpenner/convex-files-control

Quick start

1) Add the component

// convex.config.ts
import { defineApp } from "convex/server";
import convexFilesControl from "@gilhrpenner/convex-files-control/convex.config";

const app = defineApp();
app.use(convexFilesControl);

export default app;

2) Create wrapper functions in your app

The component stores access control and download grants. Your app should store its own file metadata (name, owner, etc.) and enforce auth. The wrappers below mirror the example app in example/convex/files.ts.

// convex/files.ts
import { ConvexError, v } from "convex/values";
import { mutation } from "./_generated/server";
import { components } from "./_generated/api";

export const generateUploadUrl = mutation({
  args: {
    provider: v.union(v.literal("convex"), v.literal("r2")),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("Unauthorized");

    return await ctx.runMutation(
      components.convexFilesControl.upload.generateUploadUrl,
      {
        provider: args.provider,
        // r2Config: { accountId, accessKeyId, secretAccessKey, bucketName },
      },
    );
  },
});

export const finalizeUpload = mutation({
  args: {
    uploadToken: v.string(),
    storageId: v.string(),
    fileName: v.string(),
    expiresAt: v.optional(v.union(v.null(), v.number())),
    metadata: v.optional(
      v.object({
        size: v.number(),
        sha256: v.string(),
        contentType: v.union(v.string(), v.null()),
      }),
    ),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("Unauthorized");

    const { fileName, ...componentArgs } = args;
    const result = await ctx.runMutation(
      components.convexFilesControl.upload.finalizeUpload,
      {
        ...componentArgs,
        accessKeys: [identity.subject],
      },
    );

    // Store your own file record (name, owner, etc.) here.
    // await ctx.db.insert("files", { ... });

    return result;
  },
});

3) Optional HTTP routes

If you want /files/upload and /files/download, register the router in convex/http.ts. Access keys are provided by your hook (not via the form).

// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "@gilhrpenner/convex-files-control";
import { components } from "./_generated/api";

const http = httpRouter();

registerRoutes(http, components.convexFilesControl, {
  pathPrefix: "files",
  enableUploadRoute: true,

  // Required when enableUploadRoute is true
  checkUploadRequest: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      return new Response(JSON.stringify({ error: "Unauthorized" }), {
        status: 401,
        headers: { "Content-Type": "application/json" },
      });
    }

    return { accessKeys: [identity.subject] };
  },

  // Optional: persist file metadata after a successful HTTP upload
  onUploadComplete: async (ctx, { result, file, formData }) => {
    const fileNameFromForm = formData.get("fileName");
    const fileName =
      typeof fileNameFromForm === "string"
        ? fileNameFromForm
        : (file as File).name ?? "untitled";
    // await ctx.runMutation(api.files.recordUpload, { ...result, fileName });
  },

  // Optional: provide accessKey for downloads
  checkDownloadRequest: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (identity) return { accessKey: identity.subject };
  },
});

export default http;

HTTP upload requires multipart/form-data with fields:

  • file (required)
  • provider (optional, "convex" | "r2")
  • expiresAt (optional, timestamp or null)

Access keys are not accepted via the form; they must come from checkUploadRequest. Additional form fields are available on onUploadComplete via formData.

Useful route options:

  • pathPrefix (default: /files)
  • defaultUploadProvider (\"convex\" or \"r2\")
  • enableDownloadRoute (default: true)
  • requireAccessKey (force checkDownloadRequest to return an access key)
  • passwordHeader / passwordQueryParam (override or disable password inputs)

Uploading files

Presigned URL flow

// Client-side
const { uploadUrl, uploadToken } = await generateUploadUrl({
  provider: "convex",
});

const uploadResponse = await fetch(uploadUrl, {
  method: "POST",
  body: file,
  headers: { "Content-Type": file.type || "application/octet-stream" },
});

const { storageId } = await uploadResponse.json();

const result = await finalizeUpload({
  uploadToken,
  storageId,
  fileName: file.name,
  expiresAt: Date.now() + 60 * 60 * 1000,
});

React hook

import { useUploadFile } from "@gilhrpenner/convex-files-control/react";
import { api } from "../convex/_generated/api";

const convexSiteUrl = import.meta.env.VITE_CONVEX_URL.replace(
  ".cloud",
  ".site",
);

const { uploadFile } = useUploadFile(api.files, {
  method: "presigned",
  http: { baseUrl: convexSiteUrl },
});

// Presigned
await uploadFile({ file, provider: "convex" });

// HTTP route
await uploadFile({
  file,
  method: "http",
  provider: "convex",
  http: {
    baseUrl: convexSiteUrl,
    // authToken: useAuthToken() from @convex-dev/auth/react
  },
});

uploadFile accepts:

  • file (required)
  • provider ("convex" | "r2")
  • expiresAt (timestamp or null)
  • method ("presigned" | "http")

Downloading files

Create a grant + build a URL

import { buildDownloadUrl } from "@gilhrpenner/convex-files-control";

const grant = await ctx.runMutation(
  components.convexFilesControl.download.createDownloadGrant,
  {
    storageId,
    maxUses: 1,
    expiresAt: Date.now() + 10 * 60 * 1000,
    shareableLink: false,
  },
);

const url = buildDownloadUrl({
  baseUrl: "https://<your-convex-site>",
  downloadToken: grant.downloadToken,
  filename: "report.pdf",
  // pathPrefix: "/files", // Optional if you changed the HTTP route prefix
});

Access keys are not placed in the URL. For private grants, supply them via checkDownloadRequest (HTTP route) or pass accessKey when calling consumeDownloadGrantForUrl.

Shareable links

Set shareableLink: true to allow unauthenticated downloads (no access key required). This is how the example app generates public links. If you enable requireAccessKey on the HTTP route, shareable links will still require checkDownloadRequest to return an access key.

Password-protected grants

const grant = await ctx.runMutation(
  components.convexFilesControl.download.createDownloadGrant,
  { storageId, password: "secret-passphrase" },
);

To consume a password-protected grant, pass password to consumeDownloadGrantForUrl, or send it to the HTTP route via the x-download-password header (preferred) or the password query param. Query params can leak into logs, so headers or POST flows are safer.

Access control & queries

Access keys are normalized (trimmed) and must contain at least one non-empty value.

  • accessControl.addAccessKey(storageId, accessKey)
  • accessControl.removeAccessKey(storageId, accessKey)
  • accessControl.updateFileExpiration(storageId, expiresAt)
  • queries.hasAccessKey(storageId, accessKey)
  • queries.listAccessKeysPage(storageId, paginationOpts)
  • queries.listFilesPage(paginationOpts)
  • queries.listFilesByAccessKeyPage(accessKey, paginationOpts)
  • queries.listDownloadGrantsPage(paginationOpts)
  • queries.getFile({ storageId })

Pagination uses { numItems: number, cursor: string | null }.

Cleanup

Use cleanUp.cleanupExpired to delete expired uploads, grants, and files. The example app wraps this in a mutation and runs it in a cron job.

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();
crons.hourly(
  "cleanup-expired-files",
  { minuteUTC: 0 },
  internal.files.cleanupExpiredFiles,
  {},
);
export default crons;

Server-side helper (FilesControl)

If you prefer a class wrapper around component calls, use FilesControl:

import { FilesControl } from "@gilhrpenner/convex-files-control";
import { components } from "./_generated/api";

const files = new FilesControl(components.convexFilesControl, {
  // r2: { accountId, accessKeyId, secretAccessKey, bucketName },
});

await files.generateUploadUrl(ctx, { provider: "convex" });

FilesControl.clientApi() also returns a ready-to-export API surface with optional hooks if you want the component to generate your Convex mutations and queries for you.

R2 configuration

Provide R2 credentials when you use R2 for uploads, downloads, deletes, or transfers. You can pass r2Config to the component calls or supply env vars for the HTTP routes:

  • R2_ACCOUNT_ID
  • R2_ACCESS_KEY_ID
  • R2_SECRET_ACCESS_KEY
  • R2_BUCKET_NAME

Transfer between providers

const result = await ctx.runAction(
  components.convexFilesControl.transfer.transferFile,
  { storageId, targetProvider: "r2", r2Config },
);

The transfer preserves access keys and download grants, updates the file record, and deletes the original storage object.

Testing helper

import { convexTest } from "convex-test";
import { register } from "@gilhrpenner/convex-files-control/test";

const t = convexTest(schema, modules);
register(t, "convexFilesControl");

Example app

Live Demo →

A full Convex + React + Convex Auth implementation lives in example/. It demonstrates:

  • presigned and HTTP uploads
  • authenticated downloads and shareable links
  • access key management
  • transfer between Convex and R2
  • scheduled cleanup