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-plugin

2 · 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

  • id is a DOM id string — the element must already be in the document.
  • Omit sequenceId and a new sequence is created during init. Pass one to load existing work — and note undo/redo require 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.

01

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,
})
02

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,
})
03

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)

OptionTypeNotes
id*stringDOM id of the container element. A string, not an element.
sessionToken*stringMinted server-side from your API key.
sequenceIdstringOmit and a new sequence is created during init. Required for undo/redo.
chatEnabledbooleanShow Dezignee's chat UI inside the editor.
ready() => voidFires 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() => voidYour hook for showing a login modal.
theme / themeModeobject / stringCSS variable overrides; light, dark, or system.
assetsobjectupload / deleteAsset / listAssets for custom storage.
initialPromptstringSeeds the AI chat on load — good for suggested-prompt tiles.

DezigneeEditor

MethodReturnsNotes
chat(msg, cbs)PromiseStreams over SSE with three callback channels. autoApply defaults to true.
chatAndApply(msg)PromiseOne-liner: chat, then apply the resulting commands.
applyCommands(cmds)PromiseApply commands you built yourself.
undo(count?) / redo(count?)PromiseSequence-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()stringCall after ready.
setTheme(vars?, mode?)voidRuntime theming, no re-init.
destroy()voidCall 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 validation

source 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.

MCP servers