Building a plugin
A plugin is a small app — HTML, JS, usually React — that runs inside the editor in a sandboxed iframe and talks to Revyme through a typed SDK. With it you can read and change the canvas, design tokens, CMS, code files, assets, and more, and put your own UI right next to the design.
How it works
Your plugin loads in an isolated iframe. It can't touch the editor's internals directly — instead it calls createPlugin() once to handshake with the host, and gets back a revyme object. Every call on revyme (read the selection, add a node, upload an image…) is sent to the editor over a message protocol and resolves with a plain JSON result. That message boundary, plus a permission check on every call, is the security model.
import { createPlugin } from '@revyme/plugin-sdk';
const plugin = await createPlugin({ pluginId: 'com.acme.my-plugin' });
const selected = await plugin.revyme.canvas.getSelection();The libraries
A plugin uses two npm packages — both published, MIT-licensed:
| Package | Install | What it's for |
|---|---|---|
@revyme/plugin-sdk | npm install @revyme/plugin-sdk | createPlugin() and the fully-typed revyme.* SDK. |
@revyme/plugin-tools | npm install -D @revyme/plugin-tools | The revyme-plugin CLI that packs your build for the marketplace. |
Source lives on GitHub at revymeweb/revyme-open — the SDK in plugin-sdk/, the CLI in plugin-tools/, and a full reference plugin in example-plugin/.
Three ways to build & run
| Mode | Where the code lives | How it runs |
|---|---|---|
| In the editor | a single .tsx file in your project | written in the built-in code editor, bundled live on Run |
| Local dev URL | your own Vite project on localhost | you npm run dev, then point the editor at the URL (with hot reload) |
| Cloud | the marketplace | an approved plugin loaded from the CDN |
Pick the editor for quick single-file tools; pick a local Vite project when you want multiple files, components, and your own dependencies.
Quick start — in the editor
- Open the Library panel, go to Plugins, hit + → New plugin.
- You get a starter
.tsxfile in a code editor, with a live preview beside it. - Edit, then press ▶ Run (or ⌘↵) to bundle and reload. ⌘S saves without running.
It's a single React file. You can import other npm packages right in the code — they're resolved automatically, no install step.
import { createPlugin } from '@revyme/plugin-sdk';
import { createRoot } from 'react-dom/client';
import { useEffect, useState } from 'react';
function App({ plugin }) {
const [ids, setIds] = useState([]);
useEffect(() => plugin.revyme.subscribe.selection(setIds), []);
return (
<div style={{ padding: 16 }}>
<h3>Selected: {ids.length}</h3>
<button onClick={() => plugin.revyme.canvas.setSelection([])}>Deselect all</button>
</div>
);
}
createPlugin({ pluginId: 'local.my-plugin' }).then((plugin) => {
createRoot(document.getElementById('root')).render(<App plugin={plugin} />);
});Quick start — locally with Vite
For anything bigger, build a normal Vite + React app and point the editor at it.
npm create vite@latest my-plugin -- --template react-ts
cd my-plugin
npm install @revyme/plugin-sdk
npm install -D @revyme/plugin-toolsTwo requirements:
- In
vite.config.ts, setbase: './'so assets resolve from any path. - Add a
public/manifest.json(see below).
Run it and connect it:
npm run dev # e.g. http://localhost:5180Then in the editor: Library → Plugins → + → Add dev URL, paste your dev URL, and install. Saving in your editor hot-reloads the plugin live.
The canonical end-to-end example — palette UI, multiple components, util modules, and manifest.json — is example-plugin/ (the "Gradient Generator").
The manifest
A local or marketplace plugin ships a manifest.json that tells the editor who it is and what it needs. (Editor plugins synthesize one for you.)
{
"id": "com.acme.gradient",
"name": "Gradient Generator",
"version": "0.1.0",
"author": "Acme",
"description": "Drops gradient backgrounds onto the canvas.",
"icon": "Sparkles",
"entry": "/",
"sdkVersion": "^1.0.0",
"mode": "panel",
"permissions": ["canvas:read", "canvas:write", "pages:read", "presets:write"],
"ui": { "defaultWidth": 360, "defaultHeight": 520 }
}| Field | Required | Notes |
|---|---|---|
id | yes | Reverse-DNS, e.g. com.acme.gradient. Your plugin's stable identity and storage namespace. |
name | yes | Display name. |
version | yes | Your version. |
entry | yes | The HTML the editor loads (e.g. "/"). |
sdkVersion | yes | SDK range you target — "^1.0.0". |
mode | yes | panel, floating, modal, headless, or contextMenu. |
author | no | Shown in the Library. |
description | no | One line. |
icon | no | A Lucide icon name, e.g. "Sparkles". |
permissions | no | What the plugin may do (see below). |
ui | no | defaultWidth, defaultHeight, minWidth, minHeight. |
The SDK
createPlugin({ pluginId }) resolves to { revyme, close }. Everything you do goes through revyme.*. Calls are async and return plain JSON. Here are the main areas — see the @revyme/plugin-sdk types for the complete, typed surface.
Canvas — revyme.canvas
| Method | Does |
|---|---|
getSelection() / setSelection(ids) | read / replace the selected node ids |
getNode(id) / getRect(id) | a node's { id, tag, name, parentId, styles } / its rect |
getParent(id) / getChildren(id) | walk the tree |
getNodesWithType(tag) / getNodesWithAttribute(attr, value?) | find nodes |
addNode(parentId, { tag, name?, styles?, insertIndex? }) | create a node, returns its id |
setAttributes(id, attrs) | patch styles + attributes |
removeNode(id) / cloneNode(id) / setParent(id, parentId, index?) | edit the tree |
zoomIntoView(idOrIds) | frame nodes in view |
Live updates — revyme.subscribe — each returns an unsubscribe function and fires once with the current value on attach: selection, activePage, canvasRoot, colorStyles, textStyles, codeFiles, openCodeFile, text(nodeId, …), customCode.
Content & structure
| Namespace | Highlights |
|---|---|
revyme.cms | getCollections, createCollection, getFields / addFields, getItems / addItems, setItemOrder |
revyme.pages | list, getActive, switch, create |
revyme.components | list, addInstance, createDesign, createCode, addDetachedComponentLayers |
revyme.text | getText, setText, addText |
revyme.localization | getLocales, getActiveLocale, getLocalizationGroups, setLocalizationData |
Design system
| Namespace | Highlights |
|---|---|
revyme.presets | listColorTokens, listTextTokens, addColorToken, addTextToken, createFolder |
revyme.styles | createColorStyle / getColorStyles, createTextStyle / getTextStyles |
revyme.fonts | getFont, getFonts |
revyme.variables | list, get, set |
Assets, code & site
| Namespace | Highlights |
|---|---|
revyme.assets | uploadImage(file), addImage, setImage, uploadFile, addSvg |
revyme.codeFiles | list, get, create, setContent, rename, remove, navigateTo |
revyme.customCode | getCustomCode, setCustomCode({ location, html }) — head/body injection |
revyme.redirects | list, add, remove |
Storage, network & UI
| Namespace | Highlights |
|---|---|
revyme.pluginData | get / set / delete / keys — per-plugin key/value storage |
revyme.secrets | request, use, list, revoke — encrypted, per-plugin (never exposes values) |
revyme.fetch(url, init?) | network request, allowed only for origins your manifest lists |
revyme.ui | resize({ width, height }), notify, showContextMenu, closePlugin |
Permissions
Your manifest's permissions declare what the plugin may do. Each capability area has a :read and a :write permission — canvas:read, canvas:write, cms:read, cms:write, presets:write, assets:write, codeFiles:write, customCode:write, secrets:read, localization:write, and so on. The editor checks the matching permission on every call — if you call a method whose permission you didn't declare, it's denied. Declare only what you use.
Network access is per-origin: list the hosts your plugin may reach, e.g. "network:api.openai.com" (exact), "network:*.openai.com" (subdomains), or "network:*" (any). revyme.fetch is blocked for any origin you didn't allow.
Isolation & data
- The plugin runs in a sandboxed iframe and can't navigate the editor or read its DOM — it only communicates over the SDK.
- Each plugin gets its own storage (
revyme.pluginData) and own encrypted secret vault (revyme.secrets), namespaced by itsid. Plugins can't read each other's data. - The iframe auto-sizes to your content; you can also request a width with
manifest.ui.defaultWidthor at runtime viarevyme.ui.resize.
When your plugin is ready, see Submitting plugins to pack and publish it.