Skip to main content
A site on Goosy can keep files as well as rows. When your build says it needs an object store, a bucket is created for it automatically and bound into your code — you do not provision anything, there are no access keys to hold, and there is no URL to sign.
What is live today: a declared store is created and bound. env.BUCKET is real in your Worker, env.IMAGES is real when you ask for it, and both are removed with the site — an empty bucket is. A bucket that still holds objects is kept, and the deletion receipt says so — see “It goes when the site goes” below.

What you get, and what we never touch

One bucket belongs to one site. It is created the first time you publish a build that declares it, and it is bound into that site’s Worker. Your code is the only thing that ever writes to it or reads from it — nothing on our side opens it, lists it, or hands its contents to anyone. It is private. There is no public URL for an object in your bucket. A visitor sees a file because your own route read it and returned it, which means the rules about who may see what are yours and are written in your code. It goes when the site goes — once it is empty. Deleting the site removes the bucket. A bucket that still holds objects is not removed: R2 refuses to delete a non-empty bucket, and we deliberately do not enumerate and erase your bytes on our own initiative. Such a bucket is retained and reported in the deletion receipt rather than silently dropped, so you can see exactly what is left. Emptying it is your code’s job today. If you want the bytes gone with the site, delete the objects through env.BUCKET before you delete the site. A platform-side drain — one that empties the bucket for you as part of deletion — is not built yet; when it ships, this page says so and the sentence above becomes unconditional.

Declaring it

Two lines in pages.deploy.json:
name is the only part you choose. Which bucket it points at is ours and is not settable: it is derived from your site’s id, it is what our own teardown finds the bucket by, and it is checked on every publish before your Worker goes up. That is what makes it impossible for one site’s binding to be pointed at another site’s files. Five names are already taken and are refused by name if you ask for one: DB (your site’s database), ASSETS (your own static files), IMAGES (the transform binding), and GOOSY_API_KEY / GOOSY_API_BASE (the key your site calls our API with, and the API origin it calls). They share one namespace with your binding, so a collision would not fail — it would quietly give your code the wrong handle.

Writing and reading — a complete Astro route

An upload endpoint and the route that serves the file back:
Astro.locals.runtime.env does not work. It was removed in Astro 6 and throws on Astro 7, so a route reaching for it answers 500 on every request — a real error, not a warning you can ignore. import { env } from "cloudflare:workers" is the supported access for every binding on this page, and it is what the Cloudflare adapter’s own documentation shows.
Decide who may read a file before you return it. There is no public URL, so the route above IS the access rule — and as written it lets anyone who knows a key fetch the object. Check your session, your own tables, or whatever your product’s rule is, the same way you would before returning a database row.

Resizing on the way out — env.IMAGES

With "images": true your Worker can transform bytes it already holds, without them being reachable at a URL first:
Now /media/guests/abc?w=320 serves a 320-pixel WebP of the same object, and the original stays where you put it.

Limits and cost