Developers

Write the plugin. The platform does the rest.

A plugin is a small TypeScript project: one declaration, a Bun server and React screens. The platform signs every call, keeps the state and mounts the screens in the Portal.

A plugin in one command.

bun forge new <name> scaffolds a typed declaration, a Bun/TypeScript server, React 19 + Tailwind 4 screens, fixtures and a Playwright test. On every change, CI proves that a fresh scaffold builds, passes its tests and packs into an image.

plugin.tsas bun forge new writes it
import { definePlugin } from '@forge-plugins/server/define';
import { z } from 'zod';

export const plugin = definePlugin({
  name: 'notes',
  version: '0.1.0',
  displayName: { cs: 'Poznámky', en: 'Notes' },
  screens: { main: { title: { cs: 'Poznámky', en: 'Notes' } } },
  state: {
    schema: z.looseObject({ note: z.string().max(2000).optional() }),
    maxBytes: 16_384,
  },
  ops: {
    saveNote: {
      privileges: ['TASKS'],
      task: true,
      effects: true,
      timeoutMs: 5_000,
      input: z.object({ note: z.string().max(2000) }),
    },
  },
});

What it writes

plugin.ts
The declaration: screens, state, datasets and operations.
fixtures.ts
The synthetic task the harness and the test start from.
server/src/handlers.ts
Exactly one handler for each operation.
web/src/screens/main.tsx
The screen, built on the plugin UI kit.
web/e2e/<name>.spec.ts
The Playwright test, run against the dev harness.
handlers.tsone handler per operation
import type { Handlers } from '@forge-plugins/server';
import type { plugin } from '../../plugin';

export const handlers = {
  saveNote: (input, ctx) => ({
    state: { ...ctx.state, note: input.note },
  }),
} satisfies Handlers<typeof plugin>;

An effect operation is a pure reducer: it returns the next state, and the platform commits it.

Declare once, typed everywhere.

One plugin.ts with zod schemas types the server’s handlers and the screens’ call() and stream(). Change a schema and the compiler shows every place that has to follow.

Typed end to end

Handlers receive typed input and a context shaped by the operation’s flags. Screens get typed state and a typed call for every operation.

The manifest is rendered

bun forge render writes manifest.json and the platform’s registry from plugin.ts. Nobody writes a manifest by hand.

Drift is blocked

bun forge doctor fails when a manifest or the registry no longer matches its declaration, when an operation has no handler, or when the built screens outgrow their budget.

Develop without the platform running.

bun forge dev <name> runs the web build in watch mode, the plugin’s real server and a harness that answers like the platform: signed calls, state written by compare-and-set, and the screen mounted exactly as the Portal mounts it.

The harness bar switches the screen, the user’s privileges, read-only mode, the theme and the language. The Playwright test runs against the same harness.

The bun forge dev plugin harness with screen, privilege, theme and locale switches
The dev harness: a plugin screen with its screen, privilege, read-only, theme and language switches.

The declaration, field by field.

What plugin.ts declares and the platform enforces. The rendered manifest is checked against the versioned contract before the platform accepts it.

name
Lowercase and hyphenated, up to 40 characters. It becomes the plugin’s address on the platform.
version
The plugin’s semantic version.
displayName
The title users see, in Czech and English.
screens
Each screen a process designer can bind to a task form, titled in both languages. The element name is derived, never declared.
state
schema is the zod schema of the plugin’s one state document, maxBytes its size cap, up to 1 MiB. The platform checks every write against both.
datasets
Business datasets the operations may read. They arrive with the call and are read with the user’s own rights.
ops
The operations the platform exposes for the plugin. Each one declares:
privileges
At least one; the user must hold one of them.
task
The operation runs against one task.
context
What the call carries: the state, the read-only submission, datasets.
effects
The reply’s state replaces the plugin state, committed by compare-and-set.
stream
The reply streams as server-sent events.
timeoutMs
The time budget per attempt.
input / output
zod schemas for what goes in and what comes back. They type the handler and the screen’s call.

The bun forge commands.

bun forge new <name>
Scaffolds a typed plugin and registers it: declaration, server, screens, fixtures and a Playwright test.
bun forge render
Writes the manifest and the registry from plugin.ts. With --check it only compares, for CI.
bun forge dev <name>
Runs the plugin on the dev harness, mounted exactly as the Portal mounts it, and rebuilds as you save.
bun forge doctor
The one gate CI runs: rendered files match, every server boots with one handler per operation, and the built screens stay within their budget and the CSS contract.

Tell us the process that's costing you.

Bring one real process to the demo. We describe it to Studio together and you watch it take shape on a working platform — not on slides.