Events overview
React when a file is created or deleted, however it got there. files-sdk/events turns each provider's bucket notifications into one event shape and routes them to your handlers.
files.upload() tells you about uploads made through the SDK. Plenty of writes aren’t: a file dropped in the console, another service writing to the bucket, a lifecycle rule expiring old objects, a browser that uploaded straight to storage and closed the tab before telling your server.
Every major provider can notify you about those writes, but each one does it differently: S3 sends to SQS, SNS, EventBridge or Lambda, MinIO to a webhook, R2 to a Queue, GCS to Pub/Sub, and Azure to Event Grid. files-sdk/events reads all of them and gives you one event:
import { createFiles } from "files-sdk";
import { events } from "files-sdk/events";
import { s3 } from "files-sdk/s3";
export const files = createFiles({
adapter: s3({ bucket: "uploads" }),
plugins: [events()],
});
files.events.on("created", "avatars/**", async (event) => {
await db.avatars.upsert({ key: event.key, size: event.size });
});
files.events.on("deleted", async (event) => {
await db.files.delete({ key: event.key });
});
Then point the provider’s notifications at it. For S3 delivering to SQS, the consumer is one line:
import { files } from "./lib/files";
export const handler = (event: unknown) => files.events.dispatch(event);
The event
interface FileEvent {
type: "created" | "deleted";
key: string; // caller-facing: URL-decoded, the instance prefix stripped
size?: number;
etag?: string;
versionId?: string; // S3 version id, GCS generation, B2 / Box / Cloudinary version
contentType?: string; // only when the delivery carries it (GCS, Azure, MinIO, Supabase, Cloudinary, Appwrite)
time: number; // ms since the epoch
source: "provider" | "gateway" | "sdk";
provider: string; // the adapter's name
id: string; // the idempotency key, see Delivery
raw: unknown; // the provider's original record, untouched
}
There are only two types because that’s what providers can honestly tell you. Most can’t tell an overwrite from a new object, so both are created. A copy is a created and a move is a deleted plus a created. Tagging, metadata, restore and storage-class changes don’t create or remove anything, so they’re skipped.
Where events come from
source |
What it is | How it arrives |
|---|---|---|
"provider" |
The bucket’s own notification. It sees every write, from anywhere. | dispatch() from a queue consumer, or webhook() for providers that push over HTTP. The memory adapter emits them natively. |
"gateway" |
A browser upload through the gateway, once it completes. | Automatic when the plugin is installed on the gateway’s files. |
"sdk" |
A successful upload / delete / copy / move made through this instance. |
Opt in with events({ sdk: true }). It duplicates provider events for the same write, so it’s off by default. Use it where the provider has no notifications. |
All three reach the same on() handlers.
Supported providers
| Format | Adapters | Delivery | Setup |
|---|---|---|---|
s3 |
s3, s3-fetch (against AWS) |
Lambda, SQS, SNS → SQS, SNS → Lambda, EventBridge | Amazon S3 |
s3 |
s3 |
SNS over HTTPS (signature-verified) | Amazon S3 |
s3 |
minio |
Webhook | MinIO |
s3 |
rustfs |
Webhook (or Kafka, NATS, …) | MinIO |
s3 |
wasabi |
Your own AWS SNS topic | Amazon S3 |
s3 |
storj |
Your own Google Pub/Sub topic (pull or push) | Amazon S3 |
r2 |
r2 |
Queue (Worker consumer or HTTP pull) | Cloudflare R2 |
gcs |
gcs, firebase-storage |
Pub/Sub (pull or push), Eventarc | Google Cloud Storage |
azure |
azure |
Event Grid webhook (either schema) | Azure Blob Storage |
b2 |
backblaze-b2 |
Signed webhook | Backblaze B2 |
tigris |
tigris |
Webhook | Tigris |
supabase |
supabase |
Database Webhook on storage.objects |
Supabase |
cloudinary |
cloudinary |
Signed webhook | Cloudinary |
appwrite |
appwrite |
Signed webhook | Appwrite |
box |
box |
Signed webhook (v2) | Box |
memory |
memory |
Built in | Testing locally |
Of the other S3-compatible services, Oracle, IBM COS, Alibaba, Tencent and Yandex use formats of their own; and DigitalOcean Spaces, Hetzner, Scaleway, Vultr, Exoscale, OVHcloud, Filebase, Akamai, Archil and Neon have no bucket notifications. An adapter whose provider isn’t verified claims no format; pass events({ format: "s3" }) to opt in when you know it sends S3’s. Calling parse() or dispatch() on an adapter with no format throws instead of guessing.
Dropbox, Google Drive, OneDrive and SharePoint webhooks only say “something changed” and expect you to call a changes API with a stored cursor, so they aren’t supported yet. Vercel Blob, Netlify Blobs, UploadThing, Convex, PocketBase, the filesystem, FTP, SFTP, WebDAV and Bunny have no notifications to read; use events({ sdk: true }) and the gateway there.
Next
- Delivery and idempotency: duplicates, ordering, and the
idto dedupe on. - Webhooks: mounting
webhook(), authenticating deliveries, and status codes. - Testing locally: the memory adapter and
settled(). - Plugin reference: every option and method.