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

FunctionReturnsNotes
put(key, value, options?)StoredObjectMetadataOverwrites whatever is at key
get(key)StoredObject | nullnull on a miss, not a throw
head(key)StoredObjectMetadata | nullMetadata without transferring the body
del(key)voidDeleting 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

page.tsx
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

route.ts
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.

On this page

GitHubEdit this page on GitHub