> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goosybear.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Site and project databases

> What a site database is, how a site reads it through env.DB and env.PROJECT_DB, and what a public page may read.

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.

<Note>
  **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.
</Note>

## 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`](/api/deploy-contract):

```jsonc theme={null}
{
  "bindings": {
    "database": {
      "migrations": "migrations",        // a directory of .sql files
      "tables": ["episodes", "guests"]   // the tables your code owns
    },
    "projectDatabase": "read",           // none | read | readwrite
    "submissions": "guest_applications"  // form posts land here too
  }
}
```

`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:

```js theme={null}
export default {
  async fetch(request, env) {
    const { results } = await env.DB.prepare(
      "select id, title, published_at from episodes order by published_at desc limit 10",
    ).all();
    return Response.json(results);
  },
};
```

### 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:

```astro theme={null}
---
import { env } from "cloudflare:workers";

const { results } = await env.DB.prepare(
  "select id, title, published_at from episodes order by published_at desc limit 10",
).all();
---
```

That import works in a page's frontmatter, an endpoint, and any module either of
them imports.

<Warning>
  **`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.
</Warning>

`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](/api/deploy-contract) — every field, with its exact
  constraints and its current status.
* [Pages API](/api/pages) — importing, building, previewing and publishing.
* [Recipes](/api/recipes) — a complete run from a clean directory.
