Skip to main content
Content type plugins let you add new question types to Feedal. Your UI runs in a sandboxed iframe; the host page communicates with it via a postMessage protocol. The @feedal/content-type-sdk package provides a minimal bridge so you don’t have to implement the protocol yourself.

Security model

Community plugin UI never runs in the Feedal origin. Your bundle is loaded in an iframe with sandbox="allow-scripts" only:
  • No access to the parent page’s DOM, localStorage, or cookies
  • No cross-origin network requests from the iframe (except to your own CDN for the bundle itself)
  • postMessage is the only communication channel between your plugin and Feedal
This means respondent data stays in Feedal’s hands — your iframe receives the current answer value and node config, and can only send back an answer value or a size hint.

How it works

Feedal host page

├── sends: PLUGIN_RENDER (node, mode, currentValue)

└─── iframe (your bundle, sandboxed)
      ├── calls: signalReady()          → parent receives PLUGIN_READY
      ├── calls: sendAnswer(value)      → parent receives PLUGIN_ANSWER
      └── calls: sendSize(height)       → parent receives PLUGIN_SIZE

Installing the SDK

npm install @feedal/content-type-sdk
# or
pnpm add @feedal/content-type-sdk

SDK reference

import { onRender, sendAnswer, signalReady, sendSize } from "@feedal/content-type-sdk";

// Tell Feedal your iframe is ready to receive renders
signalReady();

// Listen for render events (called every time the node is shown or the value changes)
const unsubscribe = onRender((payload) => {
  const { node, mode, value } = payload;
  // node   — the full GraphNode object (id, question_type, data.config, etc.)
  // mode   — "preview" | "interactive" | "readonly"
  // value  — the current answer value (null if unanswered)
  renderMyUI(node, mode, value);
});

// Send the respondent's answer back to Feedal
sendAnswer("the answer value");

// Tell Feedal to resize the iframe (call when your content height changes)
sendSize(350);  // height in pixels

onRender(callback)

Registers a callback that fires every time Feedal sends a PLUGIN_RENDER message. Returns an unsubscribe function. The RenderPayload passed to the callback:
FieldTypeDescription
nodeRecord<string, unknown>Full node object from the graph
mode"preview" | "interactive" | "readonly"Current render context
valueunknownCurrent answer value (null if none)

sendAnswer(value)

Sends the respondent’s answer to the host page. Call this when the user makes a selection or submits input. value can be any JSON-serialisable type.

signalReady()

Signals to the host page that your iframe has finished loading and is ready to receive PLUGIN_RENDER messages. Call this once, after your bundle is initialised.

sendSize(height)

Requests the host page to resize the iframe to height pixels. Call this whenever your content changes height to avoid scroll bars or clipped content.

Raw postMessage protocol

If you’re not using the SDK, here are the raw message types: Host → iframe
typeAdditional fieldsDescription
PLUGIN_RENDERnode, mode, valueRender the question with this data
iframe → host
typeAdditional fieldsDescription
PLUGIN_READYiframe has initialised
PLUGIN_ANSWERvalueRespondent submitted an answer
PLUGIN_SIZEheightRequest iframe resize
PLUGIN_ERRORFatal load failure

Building your bundle

Your renderer must be a self-contained ES module bundle served over HTTPS. The renderer_bundle_url in your manifest must point to this file.
# Example using Vite
vite build --lib --entry src/index.ts --formats es
The bundle is loaded into the iframe as a <script type="module">. There is no HTML shell — your bundle is responsible for creating DOM elements and appending them to document.body.

Minimal example

import { onRender, sendAnswer, signalReady, sendSize } from "@feedal/content-type-sdk";

// Build UI using safe DOM methods (never insert untrusted data via innerHTML)
const container = document.createElement("div");
document.body.appendChild(container);

onRender(({ node, mode, value }) => {
  // Clear previous content safely
  while (container.firstChild) container.removeChild(container.firstChild);

  if (mode === "readonly") {
    const text = document.createElement("span");
    text.textContent = String(value ?? "—");   // textContent is XSS-safe
    container.appendChild(text);
    sendSize(40);
    return;
  }

  const input = document.createElement("input");
  input.type = "text";
  input.value = String(value ?? "");
  input.placeholder = String(node?.data?.config?.placeholder ?? "Enter value");

  input.addEventListener("change", () => {
    sendAnswer(input.value);
  });

  container.appendChild(input);
  sendSize(60);
});

signalReady();

Registering with the manifest

Add a content_type extension point to your app manifest:
{
  "type": "content_type",
  "question_type_id": "signature-capture",
  "display_name": "Signature Capture",
  "renderer_bundle_url": "https://cdn.example.com/signature-plugin/v1.0.0/bundle.esm.js",
  "version": "1.0.0",
  "icon": "✍️"
}
Once your app is approved in the marketplace, the question_type_id becomes available in the studio’s Add content panel and in the form graph as "question_type": "plugin:signature-capture".

Testing locally

During development, simulate the postMessage protocol in a plain HTML file to test your bundle without a live Feedal account:
<!DOCTYPE html>
<html>
<body>
  <iframe id="plugin" sandbox="allow-scripts"></iframe>
  <script type="module">
    const frame = document.getElementById("plugin");

    window.addEventListener("message", (e) => {
      if (e.data.type === "PLUGIN_READY") {
        frame.contentWindow.postMessage({
          type: "PLUGIN_RENDER",
          node: { id: "test", data: { config: {} } },
          mode: "interactive",
          value: null,
        }, "*");
      }
      if (e.data.type === "PLUGIN_ANSWER") {
        console.log("Answer received:", e.data.value);
      }
    });
  </script>
</body>
</html>
Point the iframe src to a local server that serves your bundle.

Next steps

Developer portal

Submit your plugin for marketplace review.

Marketplace

Browse community and official plugins.