Studio

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

Content12 min read

static or 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. 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 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.

WhereWhat it is for
HomeWhat is waiting for you, what was published lately and by whom, and every collection with its count
InboxChanges sent for review, and agents asking before they delete or overwrite anything
A collectionA table of its entries: search, filter, sort, select many
An entryIts fields and its content, saved as you type, between its collection's list and a panel with its details and history
PublishEvery unpublished change, what changed in it, and the button that puts it live
DeveloperIndex 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:

StudioPublish buttonWhat happens
On your machineCommitA Git commit of the selected files. Nothing is pushed
On your machine, contributorCommit, turned offNothing. The drafts wait for an editor to commit them
Hosted, editorPublishOne commit on your production branch; the site redeploys. With GRAFT_STUDIO_PUBLISH=pull-request, a pull request instead
Hosted, contributorSubmit for reviewA pull request for someone to merge
Content in the database, editorPublishThe entries go live on the site straight away. Nothing to rebuild
Content in the database, contributorSubmit for reviewA 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:

ScopeAllows
studio:readBrowsing content, the schema, history and pending approvals
studio:writeSaving drafts, discarding, compiling. Publishing opens a pull request
studio:publishCommitting on your own machine, and publishing straight to the production branch
approvals:decideApproving and denying
studio:peopleInviting people and changing their roles (admins)
studio:settingsAgent 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:

AccessThe agent can
ReadRead content, never change it
Write draftsWrite drafts, which a person publishes
PublishWrite 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 StudioFrom the CLI or GitOver MCP
Find contentgraft content list, graft content searchlist_content, search_content
Save an entryEdit the file, then graft compile; or graft content writewrite_content
Delete an entryDelete the file, or graft content deletedelete_content
Publish, locallygit commit
Publish, hostedMerge the draft branch (all of its changes at once)publish_drafts
Discard a draftgit restorediscard_drafts
See an entry's historygit log -- <file>
Restore an old versiongit checkout <commit> -- <file>write_content with the old bytes
See past compilesgraft compilationslist_compilations
Approve or denygraft approve / graft denydecide_approval

The UI itself uses the Studio API, which you can call directly.

Next steps