The SDK and scaffold are currently available from the source repository; npm publication is deferred. Registry installation commands below describe the planned release. Use the source workflow in the first-plugin guide for this preview.

Plugin docs

Custom canvas types

Register a new kind of card with validated, versioned state, a summary for search and export, and migrations with host backups.

A custom canvas type is a card the user can place on the canvas. Your plugin owns the component and the state shape; GhostBoard owns persistence, revisions, search indexing, preview and the missing-plugin fallback.

Registered types and core node types

registerType always creates an extension node. It is not a way to define, replace or impersonate a core node type that GhostBoard ships itself — cards, stickies, images, folders, groups and Topics are host-owned.

The distinction matters most for Topic, GhostBoard’s lightweight mind-map node. A Topic keeps its title inline in the canvas manifest, and its parent/child hierarchy lives entirely on host-managed tree edges that the host also lays out. There is no SDK call to read a Topic title, create a subtopic, reparent a branch or influence its layout, and no event that reports topic structure changes. Use registerType for your own versioned instance state; if you need hierarchy inside your card, model it in your own state, not on host trees.

Minimal counter card

import { createElement } from "react";

function CounterCard({ context, state, updateState }) {
  return createElement(
    "button",
    {
      type: "button",
      disabled: context.readOnly,
      onClick: () => updateState({ count: state.count + 1 }),
    },
    `Count: ${state.count}`,
  );
}

app.canvas.registerType({
  id: "counter",
  title: "Counter",
  dataVersion: 1,
  component: CounterCard,
  createInitialState: () => ({ count: 0 }),
  validateState: (state) =>
    Boolean(
      state &&
      typeof state === "object" &&
      !Array.isArray(state) &&
      typeof state.count === "number",
    ),
  summarize: (state) => ({
    title: `Counter: ${state.count}`,
    plainText: `Counter value ${state.count}`,
  }),
});

The persisted type name is <pluginId>/<id>. createElement avoids a build step; the --react and --combined templates show JSX and a richer scorecard.

Contract

Member Purpose
dataVersion Integer version of your state shape. Increase it when the shape changes.
createInitialState() State for a newly placed card.
validateState(state) Type guard. Persisted state is untrusted input; reject anything unexpected.
migrateState(state, fromVersion) Converts old state to the current shape. Required when you raise dataVersion.
summarize(state) title and plainText used for search, accessibility, preview, export and when the plugin is missing or disabled.
component / previewComponent Rendered inside the card and (optionally) in compact previews.
surface Default, minimum and maximum size, resizable, and openBehavior: 'detail' to get a focused view.

updateState(next) commits a new revision and returns it. Pass { history: 'ephemeral' } only for transient interaction frames (dragging a slider) and finish with a recorded update so undo has a sensible step.

What happens when the plugin is missing

Cards keep their state, summary and search text when the plugin is disabled or uninstalled. Users see the summary placeholder; nothing is deleted. Re-enabling the plugin restores the live component.

Migrations and recovery

When a card with an older dataVersion is loaded, GhostBoard first writes a preimage (a host-owned backup of the old state), then calls migrateState and saves the result with a new revision. If the backup cannot be written, the migration does not run. Downgrading the plugin never overwrites data with an older shape, and a migration that hits a revision conflict leaves live data untouched.

Users can restore the most recent preimage from Settings → Plugins (Restore pre-migration backup). Restoring disables the plugin and keeps a guard copy of the current data. Design migrations to be pure functions of the old state and keep validateState strict so a bad migration fails loudly instead of persisting garbage.

Only canvas card state has this backup contract. Plugin key-value storage does not; version your own values there.

Testing

createFakeHost exposes registered types through harness.inspection.canvasTypes. Unit-test validateState, migrateState and summarize directly; verify rendering and persistence in a packaged GhostBoard build.