# Studio

> An editor for people who would rather not edit files: find, create, edit and publish content in a browser, with every change kept in Git or the database.

*Give writers and merchandisers an editor, without giving up Git.*

> Works on both storage engines, static and Postgres.

Not everyone on your team wants to edit Markdown files and run Git commands.
Studio is an editor for them. They find an entry, change it in a form built from
your schema, and publish. In a project that keeps its content in Git, every
change ends up as files there, exactly as if a developer had edited them. In
one that keeps it in the database, every change is a revision in Postgres.
Nothing an editor can do in Studio is unavailable to an agent or a developer.

Studio runs on both [storage engines](/docs/tiers). Editing, saving and
committing content need no database. The views that keep their data in
Postgres (approvals, the compile history, branches, accounts, agents, the media
library and site settings) need one, and say so when it is missing. Your
content stays where your project keeps it either way.

## Open Studio

```sh
graft studio
```

This starts Studio on your machine, at port 4983, and opens your browser. Only
your machine can reach it, and saves go to the files in your checkout.

To give a team a Studio URL, with sign-in and saves that land on GitHub, see
[Host Studio for your team](/docs/studio-hosting).

### Without a database

`graft studio` starts without `DATABASE_URL`. You can find, edit, create and
delete entries, and commit them, exactly as with one.

- **In a static project** (`export const index = "static"`), each save also
  updates the static index, `.graft/index.db`, so your local site shows it at
  once, and Developer, Health compares your files with it. A static project
  never uses a database, even when a `DATABASE_URL` is set.
- **In a Postgres project with no `DATABASE_URL`**, saves go to your files and
  the index waits for your next `graft compile` with a database.

Approvals, the compile history, branches, people, agents, the media library and
site settings keep their data in Postgres. Each of those views says it needs a
database and how to add one. The Studio API answers their routes with
`NEEDS_DATABASE` (HTTP 503) and the same steps in its `fix`.

### Who signs in

A Studio started with `graft studio` is bound to your computer: it listens on
`127.0.0.1` only, and anyone who can reach that address is you. It asks nobody
to sign in, with a database or without one. To reach it from another machine,
start it with `--host` and set `GRAFT_DEV_TOKEN`; requests then need that token
as a bearer header, which is checked without a database.

Studio accounts are kept in Postgres, and so are the sessions of the people who
sign in with them, so accounts need a database. A Studio for a team runs under
`graft serve --studio`, which needs `DATABASE_URL` anyway. Without a database
there is no sign-in to offer, and none is needed, because only this computer
can open Studio.

## Find your way around

Content comes first.

| Where | What it is for |
| --- | --- |
| Home | What is waiting for you, what was published lately and by whom, and every collection with its count |
| Inbox | Changes sent for review, and agents asking before they delete or overwrite anything |
| A collection | A table of its entries: search, filter, sort, select many |
| An entry | Its fields and its content, saved as you type, between its collection's list and a panel with its details and history |
| Publish | Every unpublished change, what changed in it, and the button that puts it live |
| Developer | Index health, schema, compile history, branches and settings |

Press <kbd>⌘K</kbd> anywhere to find an entry by name, create one, or jump to a
collection. Search runs on the server, so it is as quick on five thousand
products as on fifty.

In a list, <kbd>J</kbd> and <kbd>K</kbd> move down and up from anywhere on the
page, and once you are in it so do the arrow keys, <kbd>Home</kbd> and
<kbd>End</kbd>. <kbd>Enter</kbd> opens the entry.

## Find content

A collection opens as a table. The columns are the first few short fields of
its schema, so a product list shows price, status and category without any
configuration.

- **Search** matches names, URL names and text fields.
- **Filter** by status (all, unpublished, published) and by any choice,
  yes/no or reference field.
- **Sort** by name, last edited or any column.
- **Select** several entries to publish, discard or delete them together.

The table loads a page at a time as you scroll. Search, filters and sort are
kept in the address, so a reload, the back button or a link you send shows the
same rows. The count leaves out entries you deleted: they stay in the list,
marked Deleted, until you publish the deletion.

### Change a field on many entries

Select the entries, then choose **Set a field** in the bar at the bottom of the
list. Pick the field and its value, with the same control the entry editor
uses, and Studio saves each entry as a draft. Nothing changes on your site
until you publish (or commit) them. To take an optional field off every selected
entry instead, tick **Remove … from these entries instead**. An empty value
without that box ticked is refused, so one press of Enter can't wipe a field.

Each entry is checked like any save, so a value the schema refuses is refused
here too. If some entries can't be saved, the dialog lists them with the
reason, and they stay selected so you can fix them and try again. The others
are saved. You can set a choice, a number or amount, a yes/no field, a
reference or a line of text. Lists, groups and images are edited one entry at
a time.

## Edit an entry

An entry opens as a page: its title at the top, its fields below, and its
content last.

Fields are built from your schema. Each has its label, says if it is optional,
and shows its help text. Money is typed as money, choices are buttons or a
list, references open a search over the other collection, lists of images are a
gallery, and repeated groups such as product variants fold into one line each.
See [Schema](/docs/schema) for how to declare them.

Problems are shown on the field, in words, before anything is saved: "Keep the
summary to 240 characters", "Price can't be less than $0.00". Studio does not
save an entry until it is valid, which is the same rule every agent write
follows.

The content is edited in a rich editor that saves back to Markdown. Studio only
offers it when it can save the content without changing anything you did not
touch. When it can't, the content opens as Markdown beside the same form. This
matters for MDX: a change as small as escaping `{` can break a page that looks
identical. Use **Edit source** in the entry's menu to edit the whole file.

### The list and the panel

An open entry keeps its collection's list on the left, with the entry marked,
so the next one is a click away. The arrow keys move through the list and
Enter opens an entry.

The panel on the right has three tabs:

- **Details**: whether the entry is published, where it is saved, its URL
  name, file and version.
- **Changes**: what your draft changes, field by field.
- **History**: every published version, who made it and when. Open one to see
  how the entry differs from it now, and press **Restore this version** to go
  back to it. A restore is a draft like any other edit: nothing on the site
  changes until you publish it.

History comes from wherever the content lives: the published revisions when
content is in the database, the production branch's commits on GitHub, and
your checkout's commits on your machine.

Press <kbd>⌘&#92;</kbd> (<kbd>Ctrl+&#92;</kbd>) to hide the panel for a full-width
page. On a narrower window it opens over the entry instead of beside it.

### Saving

Studio saves automatically, and never rewrites an entry you only opened. Every
save carries the version you started from. If the entry changed somewhere else
in the meantime (another tab, a teammate, an agent), Studio does not overwrite
it. It shows you, and you choose to load the newer version or keep yours.

On your machine, a save also updates the index for that entry, so your local
site shows it straight away. Studio checks and projects only the file it
saved, which keeps a save fast on a large catalog. A file you change outside
Studio reaches the index on your next `graft compile`, or at once while
`graft dev` is running.

A save rewrites only the fields you changed. The rest of the frontmatter keeps
its bytes, comments included, and an edited one-line string stays on one line,
in the quotes it had, so the diff shows your change and nothing else.

## Publish

Saved changes are drafts until you publish them. The Publish button in the top
bar shows how many are waiting, and opens a sheet that lists them as entries
("Wool Hat, Edited") with the fields that changed ("Price: $25.00 → $29.00").
Choose what to include and press the button. What it does depends on where
Studio runs:

| Studio | Publish button | What happens |
| --- | --- | --- |
| On your machine | Commit | A Git commit of the selected files. Nothing is pushed |
| On your machine, contributor | Commit, turned off | Nothing. The drafts wait for an editor to commit them |
| Hosted, editor | Publish | One commit on your production branch; the site redeploys. With `GRAFT_STUDIO_PUBLISH=pull-request`, a pull request instead |
| Hosted, contributor | Submit for review | A pull request for someone to merge |
| Content in the database, editor | Publish | The entries go live on the site straight away. Nothing to rebuild |
| Content in the database, contributor | Submit for review | A request someone who can publish approves |

When your project keeps its content in the database
(`content = { source: "database" }`, see
[Configuration](/docs/configuration#content)), there is one draft per entry,
shared by everyone who edits it, and Studio works the same on your machine and
hosted.

A commit on your machine is authored by the person signed in, when Studio has
sign-in, and committed by whoever runs Studio. Leave **Describe the change**
empty and the message names the entries, such as `Update “Wool Hat”`, or counts
them past three.

**Discard** throws a draft away and leaves the published version as it is.
Deleting an entry is a draft too: it stays live until you publish the deletion,
and the toast that follows a delete has an Undo. Until then the entry stays in
its collection marked Deleted; open it to read it or press Restore.

## Inbox

The inbox holds what is waiting on a decision:

- **Changes for review.** A contributor's Submit for review lands here. When
  content is in the database, someone who can publish presses **Publish** on
  the request. On GitHub, the request is a pull request, and **Open on
  GitHub** takes you to it.
- **Agents asking first.** A destructive call from an agent waits until a
  person approves or denies it. Nothing runs until then. The card says what it
  would do, such as **Delete projects/welup-digital**, and **Approve** asks once
  more before the agent may run it. See
  [Audit & approvals](/docs/audit-and-approvals).

The sidebar shows how many things are waiting.

## Media

**Media** in the sidebar lists every file in the asset store that Studio
uploaded or an entry uses. Search by name or alt text, and press **Upload** to
add images or PDFs (10 MB each at most). Open a file to copy its key, write
its alt text, and see which entries use it. A file can be deleted only when
nothing uses it, by someone who can publish.

An image field has **Choose…**, which opens the same library: pick a file or
upload one. The field takes the file's alt text when it has none of its own,
and you can still word it differently for that entry.

Media needs an asset store: set `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`
and `S3_BUCKET` (any S3-compatible store) where Studio runs, and
`S3_PUBLIC_URL` for public links. Without them the library says so, and image
fields still take a key you type.

## Give people access

On your own machine Studio asks for no login. A hosted Studio has sign-in: Studio
accounts with a password, GitHub, Google, your own single sign-on provider, and
invite links. An admin adds people in **Settings, People**, or lets a whole email
domain join on its own. There are four roles: viewer, contributor, editor and
admin. See [Host Studio for your team](/docs/studio-hosting#2-let-people-sign-in).

The roles name scopes, which is also what bearer tokens carry:

| Scope | Allows |
| --- | --- |
| `studio:read` | Browsing content, the schema, history and pending approvals |
| `studio:write` | Saving drafts, discarding, compiling. Publishing opens a pull request |
| `studio:publish` | Committing on your own machine, and publishing straight to the production branch |
| `approvals:decide` | Approving and denying |
| `studio:people` | Inviting people and changing their roles (admins) |
| `studio:settings` | Agent tokens, the site's address and deploy hooks (admins) |

## Agents

**Developer, Agents** gives an agent its own token, for the MCP endpoint and
the Studio API. Name the token after the agent and pick how far it may go:

| Access | The agent can |
| --- | --- |
| Read | Read content, never change it |
| Write drafts | Write drafts, which a person publishes |
| Publish | Write and publish |

The token is shown once, so copy it then. Studio keeps only its hash, so a lost
token is revoked and replaced, never shown again. Revoking takes effect on the
agent's next request. No token can approve: destructive calls still wait in the
inbox for a person. Only admins see this page.

Tokens work at `/api/mcp` wherever the site runs `graft serve`. `graft studio`
on your own machine has no MCP endpoint, so tokens made there are for the
hosted site on the same database. An agent on your machine needs no token: it
starts `npx graft mcp` itself.

## Site and deploy hooks

**Settings, Site** holds the site's name and address. Studio shows them, and
agents can read them.

**Settings, Deploy hooks** lists URLs called with a `POST` after every publish:
a host's build hook, so a static site rebuilds, or the site's own revalidate
route, so cached pages refresh. A publish is a commit Studio makes to your
production branch on GitHub, or a publish of content kept in the database. A
commit on your own machine reaches no site, so it calls no hook: your host
builds when you push it. Add a secret if the URL checks one; it is sent
as `Authorization: Bearer <secret>`. Each hook shows what it answered last, and
**Send a test** calls it now. A hook that fails never undoes the publish.

The body names what went live:

```json title="Deploy hook body"
{ "event": "publish", "branch": "main", "paths": ["posts/hello-world.mdx"], "at": "2026-10-12T09:00:00.000Z" }
```

Hooks need https, or http to the same machine. They are called when content is
in the database, and when an agent's `publish_drafts` commits to your
production branch on GitHub. A pull request calls none until it is merged,
when your host builds from the commit.

## Developer views

Under **Developer**: index health (each document's state against the compiled
index), the schema, the compile history with restore, branches, agents, and
settings. The index states are explained in [How Graft works](/docs/the-model).

## Do it without Studio

| In Studio | From the CLI or Git | Over MCP |
| --- | --- | --- |
| Find content | `graft content list`, `graft content search` | `list_content`, `search_content` |
| Save an entry | Edit the file, then `graft compile`; or `graft content write` | `write_content` |
| Delete an entry | Delete the file, or `graft content delete` | `delete_content` |
| Publish, locally | `git commit` | |
| Publish, hosted | Merge the draft branch (all of its changes at once) | `publish_drafts` |
| Discard a draft | `git restore` | `discard_drafts` |
| See an entry's history | `git log -- <file>` | |
| Restore an old version | `git checkout <commit> -- <file>` | `write_content` with the old bytes |
| See past compiles | `graft compilations` | `list_compilations` |
| Approve or deny | `graft approve` / `graft deny` | `decide_approval` |

The UI itself uses the [Studio API](/docs/studio-api), which you can call
directly.

## Next steps

- [Host Studio for your team](/docs/studio-hosting) puts Studio online with sign-in.
- [Schema](/docs/schema) declares the fields Studio builds its forms from.
- [MDX safety](/docs/mdx-safety) explains what a save refuses.
