Upgrading

What changed between versions, and what you need to change.

Reference11 min read

Graft is in beta. Most releases need nothing from you, but some change a default to make it safer, and those can need a change in your project. This page lists every change that can break an existing project, newest first, with what to do about each one. For every change in a release, including fixes and new features, see the Changelog.

Upgrade your packages

All @usegraft/* packages share one version number. Upgrade them together, naming every @usegraft/* package your project already uses and no others. For a Next.js project on Postgres, that might be:

npm i @usegraft/core @usegraft/db @usegraft/sdk-next
npm i -D @usegraft/cli

On another framework, swap @usegraft/sdk-next for your adapter, such as @usegraft/sdk-astro. Mixing versions is not supported. Then run graft compile. If anything in your project relies on an old behavior, the error's fix says what to change.

1.0.0-beta.10

Restricted trust refuses innerHTML, outerHTML and dangerouslySetInnerHTML attributes. Vue renders <div innerHTML="<img src=x onerror=…>"> as raw HTML, so a literal value was a working script with no {} and no on… attribute for the existing checks to see. The check runs in graft compile, MCP write_content and Studio saves, on every framework, in any letter case. A body that uses one of these attributes under the default mdxTrust = "restricted" now fails with INPUT_VALIDATION_FAILED. React ignored the string form, so on a React site the attribute rendered nothing anyway.

What to do: run graft compile. If it names a document, replace the attribute with the markup itself, written as Markdown or a component. If every author commits through code review, mdxTrust = "full" lets graft compile accept the body, with trust: "full" at the render site. MCP write_content and Studio saves check every body whatever mdxTrust says, so such a body has to be committed to the repository. See MDX safety.

An endpoint-backed createGraft in @usegraft/sdk-react refuses two configurations it used to ignore. Any branch passed through graft.client, "main" included, got the endpoint's own branch back, because each content API endpoint serves one branch. Passing both endpoint and index used endpoint and ignored index. Both now throw CONFIG_INVALID.

What to do: to read a preview branch, point a second createGraft at the preview deployment's endpoint instead of passing branch. Pass endpoint or index, not both. See React.

1.0.0-beta.9

A usage mistake exits with code 2, and a flag the command does not take is refused. Before, every failure exited 1, and a flag meant for another command was ignored, so graft init --apply succeeded and did nothing. Now an unknown command or flag, or a missing argument, stops the command before it runs and exits 2. An action that waits for a person's approval exits 3, and a missing or refused credential exits 4. An unknown command no longer prints the whole help on stdout. It prints a short message on stderr, with the closest command when there is one:

graft: unknown command "complie".
Did you mean `graft compile`? Run `graft --help` to list the commands.

What to do: if a script reads exit code 1 as "called wrong", check for 2 instead. If a script passes a flag the command does not take, remove it. The command now stops instead of ignoring it. See Act on the exit code for every code.

graft content lists your content files, not the Postgres index. It used to read the index, so it needed DATABASE_URL and showed what the last graft compile wrote. It now lists the documents in your content files (or in the database, for a project that keeps its content there), including files you have not compiled yet. It works on the static engine with no database. Each document's line keeps its format, and the header no longer names the branch.

What to do: nothing, unless you used graft content to see what the index holds. To see what each compile changed, run graft compilations.

@usegraft/sdk-next no longer supports Next.js 14. Its peer range is next@^15.0.0 || ^16.0.0, and CI no longer tests 14. Next.js 14 gets no security fixes, and its last release has two critical remote code execution advisories. The adapter still runs on 14.2, so a project that cannot upgrade yet keeps working. Installing shows a peer warning, and npm refuses unless you pass --legacy-peer-deps.

What to do: upgrade to Next.js 15.5.24 or 16.3.3 or later. Until you can, follow Run Graft on Next.js 14.

1.0.0-beta.8

Frontmatter written in JavaScript is refused. A document whose frontmatter opens with ---js or ---javascript used to have that block run as code wherever the file was read, including the Studio server and CI. graft compile, Studio saves and MCP writes now refuse it with INPUT_VALIDATION_FAILED, and read YAML or JSON frontmatter only. See MDX safety.

What to do: if a file opens with ---js, rewrite its frontmatter as YAML.

Committing and reverting through the Studio API needs studio:publish. POST /changes/commit and POST /compilations/:id/revert asked for studio:write, so a contributor could commit to the checkout or roll it back without the publish right that Studio's own Commit button checks. Both now answer UNAUTHORIZED without studio:publish. Studio's screens need no change.

What to do: if your own code calls either route, give its token the studio:publish scope.

graft compile refuses a reference that points at nothing. A field.reference whose slug names no document, such as a product whose category was deleted, stops the compile with CONTENT_REFERENCE_NOT_FOUND, naming the file, the field and the slug. Studio and MCP writes refuse the same thing before they write. See Choices and references.

What to do: run graft compile (or graft check). For each reference it names, create the missing document or point the field at one that exists.

If you call checkRemoval from @usegraft/compiler yourself, pass contentDir and drop db and branchId. It checks against the files in contentDir, which stay right when the index is behind them.

Contributors can't commit on a local Studio. On your own machine a commit lands on the checkout at once, with no review, so it now needs the editor role. A contributor's drafts wait, and the Commit button says why. A local commit is also authored by the person signed in, not by whoever runs Studio.

What to do: nothing, unless a contributor used to commit locally. Give them the editor role, or have an editor commit their drafts.

1.0.0-beta.7

Postgres projects need graft db migrate. This release adds tables for Studio accounts and sessions, the media library, agent tokens, site settings, deploy hooks, content kept in the database, and the GitHub mirror. Until you migrate, a Studio page that needs one of them answers MIGRATION_REQUIRED with the fix, and the rest of Studio keeps working.

What to do: run graft db migrate against each database, then graft harden <role> again if your app connects as a hardened runtime role. A static project has nothing to migrate.

Embedding @usegraft/studio/panels needs lucide-react. Studio's icons moved from Phosphor to Lucide, and the panels import it as an optional peer.

What to do: if you embed the panels, run npm i lucide-react and remove @phosphor-icons/react if nothing else uses it. graft studio and graft serve --studio need nothing.

1.0.0-beta.6

Drafts made through an invite link move to a new branch name. On a hosted Studio that saves to GitHub, a person who signed in with an invite link now gets a draft branch whose name can't collide with a GitHub login. A draft they saved before you upgraded stays on GitHub under the old name, and Studio no longer shows it.

What to do: before upgrading, have invite-link editors publish or discard their drafts. A draft left behind is an ordinary branch named graft-studio/drafts/…: merge it by hand, or delete it. People who sign in with GitHub are not affected.

1.0.0-beta.5

GET and PUT /api/studio/v1/document are removed, with the DocumentDto type from @usegraft/studio. Studio's editor moved to /entry in this release. The new route reads and saves the same files, and it also saves to GitHub when Studio is hosted for a team.

This affects you only if your own code calls the Studio HTTP API. The Studio UI, MCP tools and the CLI need no change, and old Studio links still open the right entry.

What to do: change each call as follows.

BeforeAfter
GET /document?collection=…&slug=…GET /entry?collection=…&slug=…
sourcePath in the responsepath
PUT /document with { collection, slug, data, body } or rawPUT /entry with the same fields, plus baseVersion
branch in the PUT bodyNot accepted. A save goes to the branch Studio serves
DocumentDtoEntryDto

Send the version from GET /entry back as baseVersion. If the entry changed since you read it, the save is refused with CONTENT_CONFLICT (409) and nothing is written. Read it again and resend. A save without baseVersion overwrites whatever is there, as PUT /document did.

GET /entries also changed: an entry deleted in a draft stays in the list with status: "deleted" until the deletion is published, and GET /entry returns its published copy. If you count or filter entries, skip that status. See the Studio API.

1.0.0-beta.4

No breaking changes. Studio and agent writes can now refresh your app's cache. See Refresh pages when content changes.

1.0.0-beta.3

@usegraft/sdk-next needs Next.js 15.5.24 or 16.3.3 or newer. Those are the first releases with fixes for the critical Next.js security advisories published in 2026. Anything older on either line, such as 16.3.0, is outside the peer range, and installing shows a warning like incorrect peer dependency "next@16.3.0". The app may still build. The warning is about the advisories, not about Graft's code.

What to do: upgrade Next.js. npm i next@16 installs the latest 16.x, and npm i next@15 the latest 15.x. Next.js 14 is not supported in this release: it no longer gets security fixes, and those advisories are still open on it. A later release runs on 14.2 again, with a warning. See Check your Next.js version.

graft asset put refuses a key that already holds a file. It used to replace the stored file, and the store keeps no history, so the old file was gone. It now fails with ASSET_EXISTS, the same error the MCP put_asset tool returns.

What to do: if a script uploads to the same key on purpose, add --overwrite. Otherwise give each upload a new key.

1.0.0-beta.1 and 1.0.0-beta.2

No breaking changes. These releases raise dependency versions past known security advisories and fix security findings.

1.0.0-beta.0

The approval policy moved from an environment variable into your config. GRAFT_APPROVAL_POLICY is ignored, and graft serve warns once at start if it is still set. Move it to graft.config.ts:

// before: GRAFT_APPROVAL_POLICY=unattended in .env
export const approvalPolicy = "unattended";

An unknown value is refused rather than replaced with a default, so a typo cannot quietly weaken the policy. See Audit & approvals.

If you call createClient from @usegraft/sdk-core directly with a db, it no longer takes one. It reads a compiled index (index) only. Pass your Postgres handle to createDbClient from @usegraft/sdk-core/db instead:

// before: createClient({ db, collections })
import { createDbClient } from "@usegraft/sdk-core/db";

export const client = createDbClient({ db, collections });

Framework adapters' createGraft still accepts either, so app code that goes through an adapter needs no change.

Other changes in this release need no action from you, but change behavior:

  • An approval used on an unattended call is now marked as used, so it cannot authorize a later call if you tighten the policy.
  • run_function over MCP now gets the same default rate limit as POST /api/fn under graft serve.
  • MDX that cannot be parsed now returns INPUT_VALIDATION_FAILED from write_content and Studio saves, instead of a plain error.

From 0.x to 1.0

0.x is deprecated. The 0.2.0 release tightened security defaults, and each item below can stop an older project from working until you change it.

Agents over HTTP need an identity

MCP over HTTP now refuses anonymous callers by default.

  • requireActor is replaced by allowAnonymous, which defaults to false.
  • createGraftMcpHandler refuses to start with neither an actor resolver nor allowAnonymous: true.
  • graft serve allows anonymous agents only when it listens on your own machine, or when you opt in with GRAFT_MCP_ALLOW_ANONYMOUS=1. Use the opt-in only behind your own authenticating proxy: it opens the MCP tools to anyone who can reach the server. decide_approval still refuses anonymous callers. graft serve warns at start when the opt-in is set and it listens beyond your own machine.

What to do: give your HTTP endpoint an actor resolver. See Auth and Connecting agents.

Permissions are scopes, not just "signed in"

  • write_content, put_asset, and delete_content require the content:write scope. graft mcp on your own machine has it automatically.
  • Studio routes require studio:read, studio:write, or approvals:decide.
  • Studio's authorize option is replaced by authenticate, which returns the caller and their scopes.

What to do: add the scopes your agents and users need to their tokens. See Studio API for authenticate.

Approvals are recorded against a verified identity

  • decide_approval (MCP) no longer accepts decidedBy, and the Studio API ignores it.
  • decideApproval(db, id, decision, decidedBy) now takes an ApprovalDecider.
  • A function that needs approval refuses callers with no stable identity, since nobody could later tell who asked.

What to do: remove decidedBy from any code that decides approvals.

Executable MDX is refused

graft compile now fails on MDX containing {…} expressions, import, export, or spread attributes (<Card {...props} />), and on MDX it cannot parse.

What to do: remove executable MDX from your content, or, if every author has commit access to your repository, set mdxTrust = "full". See MDX safety.

Rate limits use the real client address

x-forwarded-for is ignored unless you set trustedProxyHops, and the old x-graft-peer header is gone.

What to do: behind a proxy or CDN, set trustedProxyHops to the number of proxies. See Rate limits.

Hardened roles need one new permission

The runtime role now needs UPDATE (status, duration_ms) on audit_log.

What to do: run graft harden <role> again with the operator login.

Safer file handling

  • put_asset over MCP only reads files inside your project, and only on a local server. Remote agents send the file's contents instead.
  • Studio and the registry refuse paths that escape their folders, including through symlinks.
  • The local Studio refuses requests from other sites. See Studio API.

What to do: nothing, unless you relied on reading files outside the project.

Full changelog

Every release's complete notes are in each package's CHANGELOG.md, for example packages/cli/CHANGELOG.md.