# Plugin Core Concepts

> The six ideas behind every Broadcast plugin: the manifest, hooks, ctx, permissions, settings and storage, each with a small example.

Source: https://sendbroadcast.net/docs/plugins-concepts

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

Every plugin uses the same six ideas. This page explains each one with a small example. The [Plugin Reference](https://sendbroadcast.net/docs/plugins-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:

```json
{
  "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.

```json
"hooks": { "events": ["subscriber.created"] }
```

```javascript
export default definePlugin({
  async onEvent(event, ctx) {
    ctx.log.info(`Something happened: ${event.type}`);
  },
});
```

Each hook is a separate kind of moment. [Plugin Hooks](https://sendbroadcast.net/docs/plugins-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.

```javascript
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:

```javascript
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](https://sendbroadcast.net/docs/plugins-reference#ctx).

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

```json
"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](https://sendbroadcast.net/docs/plugins-reference#permissions) 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.

```json
"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`:

```javascript
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.

```javascript
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.

```javascript
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`:

```javascript
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

- [Plugin Hooks](https://sendbroadcast.net/docs/plugins-hooks): choose a hook.
- [Plugin Security and Privacy](https://sendbroadcast.net/docs/plugins-security): learn the rules that keep your users safe.
