Multiple buckets
Serve more than one bucket — a public images bucket and a private files bucket, say — through the gateway and the UI components.
An app often has more than one storage domain: public profile images in one bucket, private personal files in another, each with its own access policy. Every layer of the stack — createFilesRouter, createFilesClient, useFiles, and the UI components — is parameterized, so multi-bucket needs no special API. There are two patterns.
One route per bucket (recommended)
Mount one gateway per bucket. Each gets its own Files instance and — the real win — its own authorize policy:
import { createFiles } from "files-sdk";
import { createFilesRouter } from "files-sdk/api";
import { minio } from "files-sdk/minio";
import { createRouteHandler } from "files-sdk/next";
const router = createFilesRouter({
files: createFiles({
adapter: minio({
bucket: process.env.S3_FILES_BUCKET!,
endpoint: process.env.MINIO_ENDPOINT!,
}),
}),
authorize: async ({ req }) => {
const userId = await requireUser(req); // throw to deny
return { keyPrefix: `users/${userId}/` }; // private, per-user
},
});
export const { GET, POST, PUT } = createRouteHandler(router);
const router = createFilesRouter({
files: createFiles({
adapter: minio({
bucket: process.env.S3_IMAGES_BUCKET!,
endpoint: process.env.MINIO_ENDPOINT!,
}),
}),
// `operations` is a hard gate that runs before `authorize`, so it must list
// every verb the route serves, writes included.
operations: ["download", "url", "head", "list", "upload", "delete"],
authorize: async ({ operation, req }) => {
if (operation === "upload" || operation === "delete") {
await requireUser(req);
}
},
});
export const { GET, POST, PUT } = createRouteHandler(router);
On the client, point each hook (or createFilesClient) at its route and pass it to the components:
const files = useFiles({ endpoint: "/api/files" });
const images = useFiles({ endpoint: "/api/images" });
<FileBrowser files={files} />
<Dropzone files={images} />
Bucket selection is a component prop, so “which bucket” is decided wherever you render — including at runtime.
FileList and FilePreview build their download-proxy fallback URL (used for thumbnails and previews when the adapter can’t sign) from their own endpoint prop, which defaults to /api/files. Pass them the same endpoint as their hook (including any ?bucket= selector, see below), e.g. <FileList endpoint="/api/images" files={images} />.
One route, selected per request
The gateway also accepts a per-request factory — files: (req) => Files — so a single route can serve several buckets. Put the selector in the endpoint’s query string; the client threads it through every call (JSON verbs, downloads, and the presign/proxy upload round-trip):
const filesInstance = createFiles({
adapter: minio({
bucket: process.env.S3_FILES_BUCKET!,
endpoint: process.env.MINIO_ENDPOINT!,
}),
});
const imagesInstance = createFiles({
adapter: minio({
bucket: process.env.S3_IMAGES_BUCKET!,
endpoint: process.env.MINIO_ENDPOINT!,
}),
});
const router = createFilesRouter({
files: (req) =>
new URL(req.url).searchParams.get("bucket") === "images"
? imagesInstance
: filesInstance,
authorize: async ({ req }) => {
// `req` carries the same query — vary policy by bucket here.
const bucket = new URL(req.url).searchParams.get("bucket");
/* … */
},
});
const images = useFiles({ endpoint: "/api/files?bucket=images" });
Prefer separate routes when the buckets have genuinely different policies — two small routers are easier to audit than one branching authorize. The factory shines when the instances share a policy shape (per-tenant buckets, region routing) or when adding a route per bucket doesn’t scale.