Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rjdellecese/confect/llms.txt

Use this file to discover all available pages before exploring further.

Confect provides type-safe file storage through three services: StorageReader, StorageWriter, and StorageActionWriter, built on top of Convex’s file storage system.

Overview

StorageReader

Get URLs for stored files

StorageWriter

Generate upload URLs and delete files

StorageActionWriter

Store files directly from actions

Storage Services

StorageReader

Available in queries, mutations, and actions. Use it to get URLs for files:
import { StorageReader } from "@confect/server";
import { Effect } from "effect";
import type { Id } from "convex/values";

const getFileUrl = (fileId: Id<"_storage">) =>
  Effect.gen(function* () {
    const storage = yield* StorageReader;
    
    const url = yield* storage.getUrl(fileId);
    
    return url;
  });
File URLs are temporary and expire after a short time. Always fetch fresh URLs when needed.

StorageWriter

Available in mutations. Use it to generate upload URLs and delete files:
import { StorageWriter } from "@confect/server";
import { Effect } from "effect";

const createUploadUrl = Effect.gen(function* () {
  const storage = yield* StorageWriter;
  
  const uploadUrl = yield* storage.generateUploadUrl();
  
  return uploadUrl;
});

const deleteFile = (fileId: Id<"_storage">) =>
  Effect.gen(function* () {
    const storage = yield* StorageWriter;
    
    yield* storage.delete(fileId);
  });

StorageActionWriter

Available in actions. Use it to store files directly or retrieve file contents:
import { StorageActionWriter } from "@confect/server";
import { Effect } from "effect";

const uploadFile = (blob: Blob) =>
  Effect.gen(function* () {
    const storage = yield* StorageActionWriter;
    
    const storageId = yield* storage.store(blob);
    
    return storageId;
  });

const getFileContent = (fileId: Id<"_storage">) =>
  Effect.gen(function* () {
    const storage = yield* StorageActionWriter;
    
    const blob = yield* storage.get(fileId);
    
    return blob;
  });

File Upload Flow

The typical file upload flow involves multiple steps:
1

Generate Upload URL

Call a mutation to generate a temporary upload URL
2

Upload from Client

Upload the file from the client to the generated URL
3

Store Metadata

Save the storage ID and metadata in your database
4

Retrieve URL

Get the file URL when needed using StorageReader

Step 1: Generate Upload URL

confect/files.ts
import { FunctionImpl, StorageWriter, DatabaseWriter } from "@confect/server";
import { Effect } from "effect";

const generateUploadUrl = FunctionImpl.make(
  api,
  "files",
  "generateUploadUrl",
  ({ filename, contentType }) =>
    Effect.gen(function* () {
      const storage = yield* StorageWriter;
      
      const uploadUrl = yield* storage.generateUploadUrl();
      
      return { uploadUrl: uploadUrl.toString() };
    }).pipe(Effect.orDie)
);

Step 2: Upload from Client

client/upload.ts
import { useMutation } from "@confect/react";
import refs from "../confect/_generated/refs";

function FileUpload() {
  const generateUploadUrl = useMutation(refs.public.files.generateUploadUrl);
  const saveFileMetadata = useMutation(refs.public.files.save);
  
  const handleUpload = async (file: File) => {
    // Generate upload URL
    const { uploadUrl } = await generateUploadUrl({
      filename: file.name,
      contentType: file.type,
    });
    
    // Upload file
    const response = await fetch(uploadUrl, {
      method: "POST",
      headers: { "Content-Type": file.type },
      body: file,
    });
    
    const { storageId } = await response.json();
    
    // Save metadata
    await saveFileMetadata({
      storageId,
      filename: file.name,
      contentType: file.type,
      size: file.size,
    });
  };
  
  return (
    <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
  );
}

Step 3: Store Metadata

confect/files.ts
const saveFileMetadata = FunctionImpl.make(
  api,
  "files",
  "save",
  ({ storageId, filename, contentType, size }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseWriter<typeof databaseSchema>();
      const auth = yield* Auth;
      
      const identity = yield* auth.getUserIdentity;
      
      const fileId = yield* db.table("files").insert({
        storageId,
        filename,
        contentType,
        size,
        uploadedBy: identity.subject,
        uploadedAt: Date.now(),
      });
      
      return { fileId };
    }).pipe(Effect.orDie)
);

Step 4: Retrieve File URL

confect/files.ts
const getFileUrl = FunctionImpl.make(
  api,
  "files",
  "getUrl",
  ({ fileId }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      const storage = yield* StorageReader;
      
      const file = yield* db.table("files").get(fileId);
      
      const url = yield* storage.getUrl(file.storageId);
      
      return {
        url: url.toString(),
        filename: file.filename,
        contentType: file.contentType,
      };
    }).pipe(Effect.orDie)
);

Direct Upload in Actions

For server-side file generation or processing, use StorageActionWriter to store files directly:
import { StorageActionWriter } from "@confect/server";
import { Effect } from "effect";

const generateReport = FunctionImpl.make(
  api,
  "reports",
  "generate",
  ({ userId }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      const storage = yield* StorageActionWriter;
      
      // Fetch data
      const data = yield* db
        .table("analytics")
        .index("by_user", (q) => q.eq("userId", userId))
        .collect();
      
      // Generate report
      const reportContent = JSON.stringify(data, null, 2);
      const blob = new Blob([reportContent], { type: "application/json" });
      
      // Store directly
      const storageId = yield* storage.store(blob);
      
      return { storageId };
    }).pipe(Effect.orDie)
);

Image Processing Example

Process images using Node actions:
import { FileSystem, Path } from "@effect/platform-node";
import { StorageActionWriter } from "@confect/server";
import { Effect } from "effect";

const resizeImage = FunctionImpl.make(
  api,
  "images",
  "resize",
  ({ imageId, width, height }) =>
    Effect.gen(function* () {
      const storage = yield* StorageActionWriter;
      
      // Download original image
      const originalBlob = yield* storage.get(imageId);
      const buffer = Buffer.from(await originalBlob.arrayBuffer());
      
      // Resize using sharp
      const sharp = require("sharp");
      const resized = yield* Effect.tryPromise(() =>
        sharp(buffer)
          .resize(width, height, { fit: "cover" })
          .jpeg({ quality: 90 })
          .toBuffer()
      );
      
      // Upload resized image
      const resizedBlob = new Blob([resized], { type: "image/jpeg" });
      const storageId = yield* storage.store(resizedBlob);
      
      return { storageId };
    }).pipe(Effect.orDie)
);

File Download

Download files in actions:
import { StorageActionWriter } from "@confect/server";
import { Effect } from "effect";

const downloadAndProcess = FunctionImpl.make(
  api,
  "files",
  "process",
  ({ fileId }) =>
    Effect.gen(function* () {
      const storage = yield* StorageActionWriter;
      
      // Download file
      const blob = yield* storage.get(fileId);
      
      // Process based on type
      const text = yield* Effect.promise(() => blob.text());
      
      // Parse and process...
      const lines = text.split("\n");
      
      return { lineCount: lines.length };
    }).pipe(Effect.orDie)
);

Deleting Files

Delete files when no longer needed:
import { StorageWriter, DatabaseWriter } from "@confect/server";
import { Effect } from "effect";

const deleteFile = FunctionImpl.make(
  api,
  "files",
  "delete",
  ({ fileId }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      const dbWrite = yield* DatabaseWriter<typeof databaseSchema>();
      const storage = yield* StorageWriter;
      
      // Get file metadata
      const file = yield* db.table("files").get(fileId);
      
      // Delete from storage
      yield* storage.delete(file.storageId);
      
      // Delete metadata
      yield* dbWrite.table("files").delete(fileId);
    }).pipe(Effect.orDie)
);

Error Handling

Handle storage errors gracefully:
import { BlobNotFoundError } from "@confect/server";
import { Effect } from "effect";

const getFileSafe = (fileId: Id<"_storage">) =>
  Effect.gen(function* () {
    const storage = yield* StorageReader;
    
    const url = yield* storage
      .getUrl(fileId)
      .pipe(
        Effect.catchTag("BlobNotFoundError", () =>
          Effect.succeed(null)
        )
      );
    
    return url;
  });

File Metadata Schema

Define a table to track file metadata:
schema.ts
import { Table } from "@confect/server";
import { Schema } from "effect";

const filesTable = Table.make(
  "files",
  Schema.Struct({
    storageId: Schema.String,
    filename: Schema.String,
    contentType: Schema.String,
    size: Schema.Number,
    uploadedBy: Schema.String,
    uploadedAt: Schema.Number,
  })
)
  .index("by_user", ["uploadedBy", "uploadedAt"])
  .index("by_storage_id", ["storageId"]);

Best Practices

1

Store Metadata

Always store file metadata in a table alongside the storage ID for easy querying.
2

Validate File Types

Check file types and sizes before allowing uploads.
3

Clean Up

Delete files from storage when removing database records.
4

Use Content Types

Set appropriate content types when storing files.
Files stored in Convex storage are immutable. To update a file, upload a new version and delete the old one.
Storage IDs are references to the _storage system table. You can query this table to see all stored files.

Next Steps

Node Actions

Process files with Node.js

HTTP API

Upload files via HTTP

Functions

Learn about mutations and actions

Database

Store file metadata