dezignee-plugin · v0.0.1
Embed the editor in your product.
An iframe and a postMessage bridge in a package with no runtime dependencies. It does not bundle the editor — it mounts it, and gives you a typed handle to drive it.
Quickstart
Working editor in three steps.
Your backend mints a session token from your API key; the browser only ever sees the session token. Then you mount the editor into any element by its DOM id.
1 · Install
npm install dezignee-plugin2 · Mint a session (server)
// Never ship dzg_api_… to the browser.
const res = await fetch("https://api.dezignee.com/api/v1/sessions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.DEZIGNEE_API_KEY}`,
},
body: JSON.stringify({ workspaceId, actorRef: user.id }),
})
const session = await res.json()
// → { sessionId, workspaceId, actorRef? }3 · Mount (client)
import { init } from "dezignee-plugin"
const editor = await init({
id: "editor", // DOM id string, not an element
sessionToken, // from step 2
chatEnabled: true,
ready: () => console.log("editor ready"),
})Two things that trip people up
idis a DOM id string — the element must already be in the document.- Omit
sequenceIdand a new sequence is created during init. Pass one to load existing work — and noteundo/redorequire it.
Three paths
Own as much or as little as you like.
The same package covers all three. They are not separate products or tiers.
Embed the editor
The full editor including Dezignee's chat UI. You own a div.
init
const editor = await init({
id: "editor",
sessionToken,
chatEnabled: true,
})Bring your own chat
Turn our chat off and drive the model from your interface.
chat
const editor = await init({
id: "editor",
sessionToken,
chatEnabled: false,
})
await editor.chat(input, {
onProgress: setStage,
onToolEmit: setTool,
onReplyChunk: appendText,
})One call
Any input in your app, straight to the document. No chat surface at all.
chatAndApply
await editor.chatAndApply(
"Make the CTA bigger"
)Under the hood all three emit the same thing: commands (intent, not diffs) that the backend validates, versions, and answers with RFC 6902 JSON patches. There is no second write path — which is why an agent can do anything a user can do by hand.
Make it yours
Your branding, your bucket.
Theme with CSS variable overrides, switch modes at runtime with setTheme(), and route every upload to your own storage.
Theming
await init({
id: "editor",
sessionToken,
theme: {
"--primary": "#6366f1",
"--radius": "12px",
},
themeMode: "system", // light | dark | system
})Custom asset storage
await init({
id: "editor",
sessionToken,
assets: {
provider: "custom",
upload: (file) => uploadToS3(file), // → { url }
deleteAsset: (id) => removeFromS3(id),
listAssets: () => listFromS3(),
},
})
// Asset calls are serviced over postMessage
// against your bucket. Nothing touches ours.Reference
init() options and the editor handle.
init(options)
| Option | Type | Notes |
|---|---|---|
| id* | string | DOM id of the container element. A string, not an element. |
| sessionToken* | string | Minted server-side from your API key. |
| sequenceId | string | Omit and a new sequence is created during init. Required for undo/redo. |
| chatEnabled | boolean | Show Dezignee's chat UI inside the editor. |
| ready | () => void | Fires on EDITOR_READY. Wait for it before getActiveTemplateId() or a client-source export. |
| refreshToken | () => Promise<string> | Called on 401 so the session refreshes and the request retries — no page reload. Without it, a 401 simply fails. |
| onSessionRefreshFailed | () => void | Your hook for showing a login modal. |
| theme / themeMode | object / string | CSS variable overrides; light, dark, or system. |
| assets | object | upload / deleteAsset / listAssets for custom storage. |
| initialPrompt | string | Seeds the AI chat on load — good for suggested-prompt tiles. |
DezigneeEditor
| Method | Returns | Notes |
|---|---|---|
| chat(msg, cbs) | Promise | Streams over SSE with three callback channels. autoApply defaults to true. |
| chatAndApply(msg) | Promise | One-liner: chat, then apply the resulting commands. |
| applyCommands(cmds) | Promise | Apply commands you built yourself. |
| undo(count?) / redo(count?) | Promise | Sequence-level, not per template. Max 10 steps. Needs a sequenceId from init. |
| exportHTML(opts?) | Promise<{ mode, results[] }> | Always an array of results. See Export below. |
| getActiveTemplateId() | string | Call after ready. |
| setTheme(vars?, mode?) | void | Runtime theming, no re-init. |
| destroy() | void | Call on unmount in React/SPA hosts or you leak event listeners. |
Streaming
Three channels, so your UI can show real progress.
chat() streams from the AI endpoint over server-sent events. Coarse stages, tool lifecycle, and incremental text arrive separately — render whichever your interface needs.
Driving your own chat UI
await editor.chat("Add a testimonial under the hero", {
// 1 · coarse stages: thinking → editing → done
onProgress: (stage) => setStage(stage),
// 2 · tool lifecycle: tool_start | tool_progress | tool_end
onToolEmit: (evt) => {
if (evt.type === "tool_start") setTool(evt.name)
if (evt.type === "tool_end") setTool(null)
},
// 3 · incremental assistant text
onReplyChunk: (chunk) => setReply((r) => r + chunk),
})
// autoApply defaults to true — patches reach the
// iframe automatically. Set false to review first
// and apply yourself with editor.applyCommands().What to render
- onProgress
- A status line. Low frequency, safe to animate.
- onToolEmit
- A chip per tool call — this is what makes the edit feel legible rather than magical.
- onReplyChunk
- Token-by-token text. Append, don't replace.
Export
Email-safe HTML, from the browser or from a cron job.
exportHTML() always resolves to an array — one item for a single template, N for a whole sequence.
exportHTML()
const { mode, results } = await editor.exportHTML({
mode: "single", // active template
source: "client", // in-memory state, unsaved edits included
})
const html = results[0].html
const warnings = results[0].warnings // inbox validationsource is the flag worth understanding
- client (default) renders the editor's in-memory state, including unsaved edits. Needs a live editor — use it for a "Preview" or "Publish" button in the UI.
- backend returns the authoritative saved version and needs no live editor at all. Use it from a server job, a webhook, or a nightly sync.
Each result carries its own warnings[] — surface them before send rather than discovering them in a client.
Recipes
React, Vue, or a script tag.
The package is framework-agnostic. The only framework-specific concern is teardown.
Mounting
import { useEffect, useRef } from "react"
import { init, type DezigneeEditor } from "dezignee-plugin"
export function EmailEditor({ sessionToken, sequenceId }) {
const editor = useRef<DezigneeEditor | null>(null)
useEffect(() => {
let cancelled = false
init({ id: "dezignee-editor", sessionToken, sequenceId, chatEnabled: true })
.then((e) => {
if (cancelled) return e.destroy()
editor.current = e
})
return () => {
cancelled = true
editor.current?.destroy() // or you leak listeners
editor.current = null
}
}, [sessionToken, sequenceId])
return <div id="dezignee-editor" className="h-[720px] w-full" />
}Security
Three token types, and only one belongs in a browser.
API key
dzg_api_…
Backend-minted, shown once. Used server-side to mint sessions. Never in frontend code, never in a public env var.
Session token
short-lived
What init() takes. Scoped to a workspace and optionally an actor, so a leaked token is bounded in both blast radius and time.
MCP one-time code
dzg_mcp_…
Generated in the dashboard under Settings → Connect MCP, valid 5 minutes, exchanged on first run for stored credentials.
Session expiry, handled for you
Pass refreshToken and a 401 triggers a silent re-mint and retry — the user keeps typing. Pass onSessionRefreshFailed to show a login modal when the refresh itself fails. Without refreshToken, a 401 fails the request outright, which is the single most common cause of "the editor stopped saving".
Want an agent to wire this up for you?
The setup MCP server detects your stack, scaffolds the embed component, and writes the init() config — read-only, so it cannot touch your documents.