Storage
How to store files in your application.
@repo/storage stores files in Cloudflare R2. It is the
intended home for Artifact bytes once that table exists.
The package used to be a bare re-export of a vendor SDK, which meant every consumer imported that
SDK directly and the package had no surface of its own. It now owns a small API over the BLOB
binding declared in each app's wrangler.jsonc. There is no token
to configure: the bucket is handed to the Worker by the runtime.
API
| Function | Returns | Notes |
|---|---|---|
put(key, value, options?) | StoredObjectMetadata | Overwrites whatever is at key |
get(key) | StoredObject | null | null on a miss, not a throw |
head(key) | StoredObjectMetadata | null | Metadata without transferring the body |
del(key) | void | Deleting an absent key is not an error |
list(options?) | StoredObjectMetadata[] | prefix and limit supported |
A miss is a value, not an exception. Callers routinely ask for objects that may not exist, and
making that path a throw pushes control flow into try/catch at every call site.
Usage
Server uploads
import { put } from '@repo/storage';
import { revalidatePath } from 'next/cache';
export function Form() {
async function uploadImage(formData: FormData) {
'use server';
const imageFile = formData.get('image') as File;
const stored = await put(imageFile.name, await imageFile.arrayBuffer(), {
contentType: imageFile.type,
});
revalidatePath('/');
return stored;
}
return (
<form action={uploadImage}>
<label htmlFor="image">Image</label>
<input type="file" id="image" name="image" required />
<button type="submit">Upload</button>
</form>
);
}Reading an object back
import { get } from '@repo/storage';
export const GET = async (
_request: Request,
{ params }: { params: Promise<{ key: string }> }
): Promise<Response> => {
const { key } = await params;
const object = await get(key);
if (!object) {
return new Response('Not found', { status: 404 });
}
return new Response(object.body, {
headers: object.contentType ? { 'content-type': object.contentType } : {},
});
};Client uploads
Send the file to a route handler you own, and have that route call put. There is deliberately no
direct browser-to-bucket helper here: a signed-upload route keeps authorization on the server,
where the workspace of the caller is already known.
Local development
initOpenNextCloudflareForDev() in each app's next.config.ts makes the BLOB binding available
to next dev, backed by a local simulation. Point it at the real bucket with the
experimental_remote flag on the binding when you need production data.