Skip to main content
A site on Goosy can have a real backend. When your build says it needs one, a database is created for it automatically and bound into your code — you do not provision anything, and there is no connection string to hold.
What is live today: a declared database is created, migrated and bound. env.DB is real in your Worker, your migrations are applied when a revision is staged, and a form post to /__lead is mirrored into the table you name in bindings.submissions. Write code that expects env.DB — it is there.env.PROJECT_DB is the exception and is declared but not yet bound: "projectDatabase" is validated and recorded, and the binding arrives with the project-database stage. Until then a site’s own tables are the source of truth.

Two databases, two different jobs

A site database belongs to one site. It is created the first time a build declares a backend, and it is bound into that site’s Worker as env.DB. Your code owns its schema and its migrations — nothing on our side writes to it unless your code does. A project database belongs to a project: the folder that holds a group of sites and their boards. It is created the first time anyone adds a Data table to a board in that project, and every site inside the project may ask for env.PROJECT_DB, read-only or read-write. It is the one a price list, a prospect list or a shared content table lives in, because more than one site needs the same rows. The unit is the project, not the account and not the workspace, so one site’s schema can never collide with another’s.

Declaring what you need

Everything is one block in pages.deploy.json:
migrations names a directory of .sql files applied in filename order. Name the directory whatever suits your project; we move the files to the reserved _migrations/ path when we stage the bundle, so your schema is never served to a visitor. Your own field comes back as you wrote it. In a source project the path is relative to public/, exactly as entry is — a directory outside public/ is not part of what your toolchain copies into the build output, so it is neither built nor staged.

Reading it from your code

Inside your Worker the bindings are ordinary Cloudflare D1 bindings:

From an Astro project

An Astro project has no fetch(request, env) of its own to read env from, so it imports the bindings directly:
That import works in a page’s frontmatter, an endpoint, and any module either of them imports.
Astro.locals.runtime.env does not work. It was removed in Astro 6 and throws on Astro 7, so a page 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, and it is what the Cloudflare adapter’s own documentation shows.
env.PROJECT_DB is the same interface against the project database, and it refuses writes unless you declared "projectDatabase": "readwrite" — it arrives with the project-database stage (see the note at the top of this page). Inside your own Worker you see your whole schema. There is no column policy between your code and your tables — the site’s code is your code.

What a public page may read

The policy applies at the platform’s read of a Data table, not at your Worker’s. When a table is managed as a Goosy Data table, its owner chooses which columns a public page may read, and the public read returns those columns and nothing else. A column that was never published is not omitted from a response; it is not in the answer at all. That is why a table can hold a contact’s email for your own automation and still back a public directory page safely.

A row written now is served now

This is the point of the whole arrangement. A row written by a form, by an automation, by Goosy in chat or by a person in the grid is read by the next request. There is no rebuild, no publish, and no cache to wait out. The static side of your site — the bytes you built — is unchanged and still moves only when you publish a revision.

Limits worth knowing before you design

  • SQLite semantics. Types, functions and transaction behaviour are SQLite’s.
  • 10 GB per database, and there are no cross-database joins — a site database and a project database cannot be joined in one query.
  • A project that needs more than one database’s ceiling is a conversation to have with us, not something to split quietly.

Forms

A site with no backend at all still captures leads: a form posting to the platform handler is recorded against the site regardless. Declaring bindings.submissions adds a second write — the same submission also lands in that table in your project database, so a static site gets “my form entries, in my own database” with no code.

Where to go next

  • The deploy contract — every field, with its exact constraints and its current status.
  • Pages API — importing, building, previewing and publishing.
  • Recipes — a complete run from a clean directory.