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:
Generate Upload URL
Call a mutation to generate a temporary upload URL
Upload from Client
Upload the file from the client to the generated URL
Store Metadata
Save the storage ID and metadata in your database
Retrieve URL
Get the file URL when needed using StorageReader
Step 1: Generate Upload URL
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
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 ])} />
);
}
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
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 ;
});
Define a table to track file metadata:
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
Store Metadata
Always store file metadata in a table alongside the storage ID for easy querying.
Validate File Types
Check file types and sizes before allowing uploads.
Clean Up
Delete files from storage when removing database records.
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