Coming in Broadcast 3.0. Until then, this page applies only to the Upcoming (edge) build.

Plugin Core Concepts

Every plugin uses the same six ideas. This page explains each one with a small example. The Plugin Reference lists every detail.

Manifest

The manifest is the file manifest.json. It declares everything your plugin wants. Broadcast reads it when you upload the plugin, and it refuses a manifest that breaks the rules.

This is the smallest manifest that passes:

{
  "id": "local.hello",
  "name": "Hello",
  "version": "1.0.0",
  "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" }
}

The rule to remember is this: if it is not in the manifest, it does not exist. A hook that the manifest does not list is never called. A host that it does not list cannot be reached.

Hooks

A hook is a function that Broadcast calls. Most hooks have two parts, and you need both:

  1. A line in the manifest, under hooks, which says “call me”.
  2. A function in dist/main.js, which says what to do.
"hooks": { "events": ["subscriber.created"] }
export default definePlugin({
  async onEvent(event, ctx) {
    ctx.log.info(`Something happened: ${event.type}`);
  },
});

Each hook is a separate kind of moment. Plugin Hooks helps you choose.

Your code runs only while a hook runs. Broadcast can stop the process between calls. So do not keep data in a variable between calls. A global variable can be gone at the next call. Use storage instead.

ctx

ctx is the second value that each hook gets. It is your one way to talk to Broadcast.

ctx.channel.name                                 // the channel this call runs in
ctx.settings.get("tag_name")                     // a setting
await ctx.store.get("count")                     // your own data
ctx.log.info("Hello")                            // a line in the logs
await ctx.broadcast.subscribers.get(12)          // Broadcast data

Two things are good to know:

Each call belongs to one channel. If your plugin is on in two channels, Broadcast calls it separately for each one. ctx gives you only the data of that channel. You cannot ask for another channel.

Most calls need await. A call that goes to Broadcast takes time, so it gives you a promise. If you forget await, your function can end first. The call then fails with the code call_ended.

A refused call throws an error with a code, and you can catch it:

try {
  await ctx.broadcast.subscribers.get(12);
} catch (error) {
  if (error.code === "not_found") ctx.log.warn("That subscriber is gone.");
  else throw error;
}

fetch also works, but only for hosts that your manifest declares. See the full ctx list.

Permissions

A permission is something your plugin asks to do. The admin sees each one in plain words before deploying. Without the permission, the call fails.

"permissions": {
  "subscribers": "read",
  "tags": "write",
  "network": ["api.example.com"]
}

Every plugin gets a few things without asking: its own settings, its own storage, its own logs, its own events and the channel name.

Other permissions have a level, which tells the admin how careful to be:

Level Meaning Examples
Standard Normal access Read subscribers, change tags, reach one host
Sensitive A strong warning for the admin Copy the whole list, add new subscribers, change emails before they are sent
Elevated Off until an admin turns it on for a channel Reach one host on a private network

Ask for the least you need. An admin trusts a plugin more when the list is short. It also limits the harm if your plugin has a bug.

Some hooks add a sensitive permission by themselves. For example, a public action adds “Links anyone can click in emails”. The reference lists all of them.

Settings

A setting is a value that the admin fills in, so that you do not have to write it in the code. Each channel has its own values.

"settings": {
  "greeting": { "type": "string", "label": "Greeting" },
  "webhook_url": { "type": "url", "label": "Webhook URL", "secret": true, "required": true }
}

Broadcast builds the form from this. Read a normal setting with get, and a secret setting with secret:

const greeting = ctx.settings.get("greeting");            // no await
const url = await ctx.settings.secret("webhook_url");     // await

Mark every API key, token and private URL as "secret": true. A secret is stored encrypted, it is never shown again in the form, and ctx.settings.get does not return it.

Warning

Every setting value is a string. A boolean setting gives you "true" or "false", and an integer setting gives you "5", not 5. An empty field gives you "" or undefined. So write ctx.settings.get("enabled") === "true", and write Number(ctx.settings.get("limit")).

A required setting must have a value before the plugin can be turned on in a channel.

Storage

Storage is a small database for your plugin. You save a value under a key, and it is there at the next call. The value can be any JSON value.

await ctx.store.set("last_sync", "2026-01-31");
const lastSync = await ctx.store.get("last_sync");   // "2026-01-31"
await ctx.store.delete("last_sync");

get gives you null when there is no such key.

To count something, use increment. It adds to a number and gives you the new total. It starts from 0 if the key is new.

const count = await ctx.store.increment("signups");       // adds 1
await ctx.store.increment("signups", -1);                 // takes 1 away

Warning

Do not count with get and set. A plugin can run up to four calls at once. If two calls read 10, add one and save, the total is 11 and one count is lost. increment is atomic: both counts always land.

Your data is separate for each channel, and no other plugin can read it.

Data about one person

If you save something about a subscriber, use forSubscriber:

const person = ctx.store.forSubscriber(12);
await person.set("rating", "good");
const rating = await person.get("rating");

It works like the normal store, with one big difference: when the subscriber is deleted, Broadcast deletes this data too. You do not have to write any code for it. This is the easiest way to respect a person’s right to be forgotten, so use it for all personal data.

Limits

One value can be 256 KB, and one plugin has 10 MB in each channel. A key can have 200 characters.

ctx.store.list(prefix) returns the entries whose key starts with prefix, as { key, value } objects in key order. It returns the first 100 rows at most, so it is not a way to walk through thousands of entries.

Next steps

Last updated

Was this page helpful?

Thanks for your feedback!

Thanks for letting us know. We'll work on improving this page.