Studio
Give writers and merchandisers an editor, without giving up Git.
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. 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
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.
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 aDATABASE_URLis set. - In a Postgres project with no
DATABASE_URL, saves go to your files and the index waits for your nextgraft compilewith 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 ⌘K 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, J and K move down and up from anywhere on the page, and once you are in it so do the arrow keys, Home and End. Enter 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 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 ⌘\ (Ctrl+\) 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), 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.
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.
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:
{ "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.
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, which you can call directly.
Next steps
- Host Studio for your team puts Studio online with sign-in.
- Schema declares the fields Studio builds its forms from.
- MDX safety explains what a save refuses.