# Plugins overview

A **plugin** adds live, shared behavior to a deck: a poll, audience Q&A, emoji reactions, or anything you can model as shared state. Plugins are ordinary Bun packages; a deck depends on them like any dependency.

## Anatomy

A plugin is a `definePlugin({ … })` value with four fields:

| Field | What it is |
| --- | --- |
| `id` | A short string. Used to place the plugin (`<Plugin id="poll" />`) and to namespace its shared state (`plugin:poll`). |
| `state` | A typed schema describing the shared CRDT state (the SDK's `schema`/`t`). |
| `client` | React surfaces: `Slide` (in-deck), an optional `presenter` console (a tab in the presenter view), and a `fallback` (offline/thumbnail). Runs in every browser. |
| `server` | Optional host-side logic. Runs **once**, on the machine presenting, never in the audience's browsers. |

## Built-in plugins

- **`@liebstoeckel/plugin-poll`**: live poll; everyone votes, results update in real time.
- **`@liebstoeckel/plugin-qa`**: audience asks + upvotes questions **from any slide** (a 💬 chrome button, no Q&A slide required); presenter moderates from the presenter console; the queue re-ranks live.
- **`@liebstoeckel/plugin-reactions`**: ephemeral floating emoji over the deck (rate-limited, self-pruning).

All three are pure-CRDT (no server part) and make good templates.

## Using a plugin

Register it with `<Present>` and place it on a slide:

```tsx title="main.tsx"
import poll from "@liebstoeckel/plugin-poll";

<Present plugins={[poll]} slides={[…]} />
```

```tsx title="a slide"
import { Plugin } from "@liebstoeckel/engine";

<Plugin id="poll" props={{ question: "Ship it?", options: ["Yes", "Also yes"] }} />
```

`<Plugin>` renders the plugin's `Slide` when a live server is connected, else its `fallback`. On touch screens it provides the [tap-to-interact breakout](https://docs.liebstoeckel.app/guides/mobile/) automatically.

The `id` you place **must** be in `<Present plugins={[…]}>`. A `<Plugin>` whose id isn't registered renders nothing (the build still succeeds) — a blank spot on a slide almost always means a missing entry in `plugins`. Dev builds log a console warning to flag it.

## Discovery & bundling

A package is recognized as a plugin by a marker in its `package.json`:

```json
{
  "keywords": ["liebstoeckel-plugin"],
  "liebstoeckel": { "client": "./src/client.tsx", "server": "./src/server.ts" }
}
```

At build time the deck's dependencies are scanned for this marker; matching plugins go into a **manifest** embedded in the deck. Any `server` entry is bundled (target `bun`) and base64-encoded into that manifest. It is decoded and run only by a live server, never in the browser.
**Threat model:** When you present through the **relay** (a public viewer link to an untrusted audience), audience write-scope is **enforced server-side by default**: the relay drops any audience update that touches state outside the plugin's declared `audienceWrites` allowlist, bounds the size and shape of the values it does accept, and rate-limits the rest. The presenter (or runner) may write the whole doc; an audience peer can only touch its allowed keys. A plugin's *client* code still runs in the audience's browser, so a plugin must never render submitted content as raw HTML (XSS must be impossible) — and the engine sanitizes incoming state against each plugin's schema before render as a backstop, so a malformed value can't crash the deck. The plain `live` server on your LAN is the **trusted** model (you and the audience share your own network) and does **not** enforce write-scope, so don't hand its link to an untrusted crowd — use the relay for that.

[Build a plugin](https://docs.liebstoeckel.app/plugins/building-a-plugin/)