# Upgrading

> Move a project to the latest Graft beta, with every breaking change since 0.x and what to change for each.

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

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](/docs/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:

```sh
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.

> **Prompt for a coding agent:**
> Upgrade every @usegraft package in this project to the latest version,
> following https://graft.page/docs/upgrading.md. Run graft compile and fix
> anything it reports.

## 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](/docs/mdx-safety#what-is-refused).

**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](/docs/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:

```text
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](/docs/cli#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](/docs/next#run-graft-on-nextjs-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](/docs/mdx-safety#frontmatter-is-read-as-yaml-or-json).

**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](/docs/schema#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](/docs/studio-hosting).

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.

| Before | After |
| --- | --- |
| `GET /document?collection=…&slug=…` | `GET /entry?collection=…&slug=…` |
| `sourcePath` in the response | `path` |
| `PUT /document` with `{ collection, slug, data, body }` or `raw` | `PUT /entry` with the same fields, plus `baseVersion` |
| `branch` in the `PUT` body | Not accepted. A save goes to the branch Studio serves |
| `DocumentDto` | `EntryDto` |

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](/docs/studio-api#routes).

## 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](/docs/next#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](/docs/next#check-your-nextjs-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`:

```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](/docs/audit-and-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:

```ts
// 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](/docs/auth) and [Connecting agents](/docs/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](/docs/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](/docs/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](/docs/rate-limits#if-your-app-is-behind-a-proxy-or-cdn).

### 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](/docs/studio-api#requests-that-change-data).

**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`](https://github.com/AndersonDesign1/graft/blob/main/packages/cli/CHANGELOG.md).
