

- A server-side write. The bytes are already on your server when the model answers, so they go up with
put. There is no browser upload here. - A new path per generation.
generations/${user.id}/${generationId}is written once and never overwritten, socache: 'immutable'is unconditionally safe. - A row per generation. It holds the prompt, the path and the URL, and it is what the gallery reads.
Storing a generation
Read the model’s response as aBlob and hand it to put. A Blob carries its own length and its own type, so nothing has to be declared.
lib/generations.ts
image/png or image/webp alike. If the response carries no Content-Type, pass contentType yourself, or the object is stored as application/octet-stream and a browser downloads it instead of rendering it.
Plenty of providers answer with JSON pointing at a temporary URL instead. Fetch that URL and store what comes back the same way:
gpt-image models answer with neither: the image comes back as base64 in data[0].b64_json, and there is no URL to fetch. Decode it and declare the type, because base64 carries none of its own:
contentType off here and the object is the application/octet-stream case above, so the gallery downloads a file instead of showing an image. response_format: "url" is a dall-e-2 and dall-e-3 parameter, and the gpt-image models reject it.
All three forms hold the image in memory for the length of the call, which is fine for an image. For a body too big for that, put takes the response stream with a size; see Writing.
The route
app/api/generate/route.ts
The gallery
The bucket cannot answer “what has this user generated”. Your table can, so read rows and render the URL stored on each one:app/gallery/page.tsx
immutable under a path that is never reused, so a browser or CDN that has seen one keeps it for a year and there is nothing to invalidate.
If the gallery is not public, create the bucket as private instead. blob.url is undefined there, so the row keeps only the path, and a route of yours checks ownership and calls signedReadUrl at click time. That shape is in Private documents.
Deleting
Delete the row first, then the object. The gallery stops showing the image immediately, and if the second step fails the leftover is an object nobody links to rather than a broken image.del treats an already missing object as success, so it is safe to retry.
app/actions.ts
Next steps
Writing
Which bodies
put accepts, and what a stream needs.Caching
immutable, revalidate, and what each one stores.Deleting
One path, an array, and what a partial delete reports.
Private documents
The same shape on a private bucket, with signed reads.