BackmatterPricingDocs
Contents

Studio

The full editor, mounted on your site, with your inputs and previews.

The Studio is the editing UI: content lists, the document editor, Media, chat, publish, and settings. The dashboard runs it. Your site can run it too, at /backmatter, built by your own bundler from @backmatter/studio.

That is what makes the dev bar work on a production domain. The login never crosses sites: the Studio on your site uses a session cookie on your origin and talks to a route on your origin. It is also what lets you change the editor for your content, with React components you write.

Mount it

backmatter init writes three files for a Next site:

app/(backmatter)/backmatter/layout.tsx
export { StudioLayout as default } from "@backmatter/next/studio";
app/(backmatter)/backmatter/[[...path]]/page.tsx
import { createStudioPage } from "@backmatter/next/studio";
import { cms } from "backmatter";
import studio from "../../../../backmatter.studio";

export default createStudioPage(cms, { studio });
backmatter.studio.tsx
"use client";

import { defineStudio } from "@backmatter/studio/define";

export default defineStudio({
  menu: [{ label: "Open site", href: "/" }],
});

The route group has its own root layout, so your header, footer, fonts, and global CSS do not wrap the editor. @backmatter/next depends on @backmatter/studio; add it to your own package.json as well, since backmatter.studio.tsx imports it directly.

createStudioPage takes basePath (default /backmatter), proxyPath (default /api/backmatter/cms), previewEntryPath (default /api/backmatter/preview), and dashboardUrl (default: the Backmatter dashboard, or BACKMATTER_DASHBOARD_URL). The page is static HTML with no data in it. Without a session it redirects to the dashboard, which signs you in, mints a ticket, and returns.

How it signs in

StepWhere
You open /backmatter or the bar on your siteYour site
No session: redirect to {dashboard}/enter?workspace=…&return=…Dashboard, first-party cookie
Dashboard mints a ticket and sends you to /api/backmatter/preview?ticket=…Your site
The ticket route sets backmatter_preview_session (HttpOnly, 12h) and returns you to returnYour site

Every request from the Studio goes to /api/backmatter/cms/* on your site. That route turns the cookie into a bearer token for the CMS and forwards an allow-list of paths, scoped to the session's workspace, branch, and tree. Paths that change the workspace rather than its content (members, GitHub, host tokens) are not on the list; those stay in the dashboard.

return must be an origin in the workspace's site URLs. The dashboard refuses anything else.

defineStudio

backmatter.studio.tsx is a "use client" module. Its default export is passed to createStudioPage. The server only forwards the reference; the browser builds the components.

backmatter.studio.tsx
"use client";

import { defineStudio } from "@backmatter/studio/define";
import { ColorInput } from "./components/color-input";
import { HeroPreview } from "./blocks/hero.preview";

export default defineStudio({
  theme: { accent: "#0f2a3f" },
  fields: { color: ColorInput },
  blocks: { Hero: { preview: HeroPreview, label: "Hero" } },
  menu: [{ label: "Open site", href: "/" }],
});
KeyWhat it does
theme.accentThe accent color: buttons, focus rings, active states.
fieldsA React input per field. Keyed by the field's input hint first, then by its kind (text, number, …).
blocksPer block type: preview renders above the fields when a block is open, label and icon replace the type name in the list.
menuLinks under Settings in the sidebar.

Custom inputs

Ask for an input in the model, then provide it:

blocks/Cta.tsx
background: field.text({ input: "color" }),
components/color-input.tsx
import type { StudioFieldInputProps } from "@backmatter/studio/define";

export function ColorInput({ value, onChange, readOnly }: StudioFieldInputProps) {
  const color = typeof value === "string" ? value : "#000000";
  return (
    <input type="color" value={color} disabled={readOnly} onChange={(e) => onChange(e.target.value)} />
  );
}

StudioFieldInputProps is { name, field, value, readOnly, onChange }. field is the schema field (kind, required, input, …). The input is used wherever that field is edited: the document details, a block's fields, and the block settings dialog. The value you pass to onChange is what autosave writes.

A field with no input falls back to fields[kind], then to the built-in control.

Block previews

blocks/hero.preview.tsx
import type { StudioBlockPreviewProps } from "@backmatter/studio/define";

export function HeroPreview({ props }: StudioBlockPreviewProps) {
  return <div>{String(props.headline ?? "")}</div>;
}

The preview receives the block's current props, so it follows typing. It is a picture for the editor, not the site's render; the page next to the Studio is still the preview of record.

Versions

The Studio on your site is the version in your package.json. The dashboard records the version each site last reported and shows it under Settings → General. When the CMS needs a newer Studio it answers 426, and the Studio on the site shows one notice: update @backmatter/studio and @backmatter/next, then reload.

Limits

  • Signing in happens at the dashboard. There is no login form on your domain.
  • A static export has no server, so no proxy and no cookie. The dashboard's split-view editor still works there; the bar does not.
  • Chat tools are not customizable from the site.