Migrating from the 1.x API

This page moves a 1.x plugin to the 2.0 API. Move one plugin at a time. Keep each change small and testable.

Use this when

  • Move a 1.x plugin. Replace its package manifest, broad SDK access, and app slots.
  • Plan a gradual move. Keep 1.x plugins active while other plugins use 2.0.
  • Find a new API home. Use the coverage matrix for each old member.
  • Choose a first plugin. Use the rebuild stories to compare real plugin shapes.
  • Review a port. Check the manifest, contract edges, surface claims, and tier factories.

The coexistence plan

The 1.x API stays available as a second implementation during the move. A plugin does not need an immediate rewrite.

The old bb.sdk object becomes a compatibility facade over named 2.0 services. Both paths use one source of truth.

First-party plugins move first. External plugins can move after their required domain services become available.

The project can deprecate a facade member after its use reaches zero. This process does not require one large cutover.

Move a complete plugin generation when possible. Do not add new work to the facade during the move.

Start with the two maps

Use the coverage matrix for an old symbol. It maps each old item to its 2.0 home.

Use the plugin rebuild stories for a complete plugin. They show all 22 shipping plugin shapes.

The rebuild stories also give a useful order. Start with a leaf UI plugin such as inline-vis or pdf-preview.

Move data-heavy plugins after their domain services exist. Move provider plugins after their shared host services exist.

The three-step move

1. Adopt the 2.0 manifest

Create bb.plugin.jsonc. Move plugin metadata out of the package.json plugin block.

Declare these records when the plugin needs them:

  • claims lists the surfaces and services that the plugin implements.
  • surfaces and services declare contracts that the plugin owns.
  • requires, optional, and watched declare service edges.
  • exports lists public module subpaths.
  • artifacts lists the app, server, and host outputs.

The build generates contract.json. Do not edit that file.

The build checks bb.plugin.jsonc and contract.json as one unit. It rejects a claim without a matching implementation.

Contract IDs use dots. First-party IDs start with bb.. A third-party contract can use an ID such as acme.counter.service.

2. Replace broad SDK calls

Replace each bb.sdk call with a named domain service or a kernel port. Declare the matching edge in the manifest.

The fixed kernel ports are bb.storage, bb.secrets, bb.preferences, bb.realtime, bb.http, bb.rpc, and bb.plugins.

Domain data comes from owner services. Examples include bb.threads, bb.workspace, bb.files, and bb.providers.

Use requires when activation needs the provider. Use optional when the plugin can work without it.

Use watched when the plugin must react to provider changes without a restart.

This 1.x call uses the compatibility facade:

// Before: 1.x server code
const thread = await bb.sdk.threads.get(threadId);

The 2.0 factory receives a typed API object. It gets the required service through a declared edge:

// After: 2.0 server code
import { defineServerPlugin } from "@get-bb/plugin/server";
import { bbThreads } from "@bb/threads/contracts";
import { threadTitleRpc } from "./contracts.js";

export default defineServerPlugin(async (api) => {
  const threads = await api.services.use(bbThreads.server);

  api.rpc.register(threadTitleRpc, {
    async get({ threadId }) {
      const thread = await threads.records.get({ threadId });
      return { title: thread.title };
    },
  });
});

This example keeps the plugin's typed RPC contract. Give that contract a dotted ID, such as acme.thread-title.rpc.

The manifest must include the required edges:

{
  "requires": [
    { "service": "bb.threads", "range": "^1" },
    { "service": "bb.rpc", "range": "^1" }
  ]
}

3. Convert slots to claims

Find the presentation plugin that declares the target surface. Add a static claim for that contract.

Then provide the implementation from the app factory. Import definePlugin from @get-bb/plugin/app.

The four contract kinds control composition:

Kind Result
single One implementation wins for the whole contract.
list All active claims appear in a stable order.
keyed One implementation wins for each key.
chain Ordered members call the next member.

A replaceable contract joins the global winner picker. The kernel does not use plugin load order to select its winner.

Only a single app surface gives its winner an Original component. List, keyed, and chain surfaces do not give Original.

All API registrations have automatic cleanup. Return a disposer only for a resource that the API does not own.

Worked move: a small status plugin

This plugin adds one status item to the application shell. It has no server tier and no bb.sdk calls.

Therefore, step 2 needs only a search check. Steps 1 and 3 contain the code changes.

Before: the 1.x package block

The 1.x plugin keeps its plugin data in package.json:

{
  "name": "@acme/bb-plugin-ready",
  "version": "1.4.0",
  "type": "module",
  "bb": {
    "id": "acme-ready",
    "name": "Acme Ready",
    "description": "Shows Acme status in the sidebar.",
    "app": "./src/app.tsx"
  }
}

Before: the 1.x app slot

The old app entry registers a named sidebar slot:

// Before: 1.x src/app.tsx
import { definePluginApp } from "@get-bb/plugin-sdk/app";

export default definePluginApp({
  setup(app) {
    app.slots.sidebarFooterAction({
      id: "ready",
      title: "Acme is ready",
      icon: "Check",
      run() {
        window.alert("Acme is ready.");
      },
    });
  },
});

This code is the before side only. Do not use this entry or package path in new 2.0 code.

After: bb.plugin.jsonc

The Layout plugin owns bb.layout.statusbar. The contract has the list kind, so each active claim appears.

// After: bb.plugin.jsonc
{
  "$schema": "https://getbb.app/schemas/plugin-v2.schema.json",
  "schemaVersion": 2,
  "id": "acme.ready",
  "version": "2.0.0",
  "name": "Acme Ready",
  "description": "Shows Acme status in the application shell.",
  "engines": {
    "bb": "^2.0.0",
    "sdk": "^2.0.0"
  },
  "claims": [
    {
      "surface": "bb.layout.statusbar",
      "version": "^1.0.0",
      "order": 40
    }
  ],
  "artifacts": {
    "app": "./dist/app.js"
  }
}

The manifest uses a dotted plugin ID. Its claim names a dotted contract ID.

The build writes the app claim and its checked type data to contract.json.

After: the 2.0 app factory

The app factory provides the claimed list item:

// After: 2.0 src/app.tsx
import { definePlugin } from "@get-bb/plugin/app";

export default definePlugin((api) => {
  api.surfaces.provide("bb.layout.statusbar", {
    id: "acme.ready.status",
    order: 40,
    title: "Acme is ready",
    icon: "Check",
    onActivate() {
      window.alert("Acme is ready.");
    },
  });
});

The typed api object replaces the old app builder. The claim replaces the named slot registration.

The API owns the status item cleanup. This factory does not need a disposer.

Build and check the move

Run bb plugin build .. The build must produce dist/bb.plugin.json, dist/contract.json, and dist/app.js.

Then check the result:

  1. Confirm that bb.plugin.jsonc has the claim and app artifact.
  2. Confirm that contract.json contains bb.layout.statusbar.
  3. Confirm that the status item appears once.
  4. Disable and enable the plugin. Confirm that the item leaves and returns.
  5. Remove the 1.x package block after the 2.0 loader uses the new manifest.

Common translations

1.x form 2.0 form
A plugin block in package.json bb.plugin.jsonc
definePluginApp definePlugin from @get-bb/plugin/app
A server activation entry defineServerPlugin from @get-bb/plugin/server
An experimental host entry defineHostPlugin from @get-bb/plugin/host
bb.sdk.<domain> A required, optional, or watched service edge
A fixed backend helper A kernel port on the typed server API
A named app slot A claim on an owner-declared surface
Local replacement priority The global winner picker for a replaceable contract
Manual registration cleanup Factory-scope cleanup
A handwritten contract artifact The generated contract.json

The SDK also provides @get-bb/plugin/testing. Use it for app, server, and host harnesses after the runtime move.

Review rules

  • Do not copy a slash-based contract ID into 2.0. Rename it with the owner dot grammar.
  • Do not declare a surface in the consumer manifest. Claim the surface that its presentation owner declares.
  • Do not use Original on list, keyed, or chain surfaces.
  • Do not keep a required service handle after the factory generation stops.
  • Do not add manual cleanup for registrations that the typed API owns.
  • Do not edit the generated contract.json file.
  • Do not remove the 1.x implementation before the 2.0 plugin passes its load and rollback checks.

Finish the move

Search the plugin for bb.sdk, definePluginApp, PluginAppSlots, and old package paths. Every remaining use needs a clear reason.

Use the coverage matrix for each remaining symbol. Use the rebuild stories for the final shape.

The move is complete when the plugin uses only 2.0 artifacts, edges, claims, factories, and generated contracts.