# Server plugins

Most plugins are pure-CRDT: client UI + shared state is enough. Add a **server** part when you need something a browser can't (or shouldn't) do:

- call an external API with a **secret** (the client runs in an opaque sandbox with no secrets),
- enforce logic centrally (e.g. authoritative tallying, timed transitions),
- bridge to a database or another service.

## Where it runs

The server part runs **once**, on the machine that started [`liebstoeckel live`](https://docs.liebstoeckel.app/guides/live/). It never runs in the audience's browsers, and **never on the relay**. With a [relay](https://docs.liebstoeckel.app/guides/relay/), your local process connects as a privileged "runner" peer and applies the server plugin's effects to the shared doc.

<Aside type="caution" title="It's your machine">
Because the server part executes locally, running a deck means running its plugins' server code on your machine. That's why a trust warning prints before a deck loads. Only run decks you trust.
</Aside>

## The shape

Declare the entry in `package.json` and export a `server(ctx)` function:

```json title="package.json"
{
  "liebstoeckel": { "client": "./src/client.tsx", "server": "./src/server.ts" }
}
```

```ts title="src/server.ts"
import type { PluginServerCtx } from "@liebstoeckel/plugin-sdk";
import { pluginState } from "@liebstoeckel/plugin-sdk";
import { mySchema, type MyState } from "./logic";

export default function server(ctx: PluginServerCtx<MyState>) {
  const state = pluginState(ctx.doc, "myplugin", mySchema);

  // react to shared-state changes, run host-only work, write results back
  const stop = state.subscribe((snap) => {
    // …e.g. when a round closes, compute + write the official result
  });

  return () => stop(); // optional teardown
}
```

The runtime `ctx` gives you the shared `doc`, the `session` id, and the `instance` string. Build a typed state accessor yourself by calling `pluginState(ctx.doc)`, as the example above does.

## How it's bundled

At build time the `server` entry is bundled (target `bun`, self-contained) and **base64-encoded into the deck's plugin manifest**. A live server decodes it to a temp module and runs it with an injected `ctx`. The browser never touches it.
**Built-ins have none:** `plugin-poll`, `plugin-qa`, and `plugin-reactions` are all pure-CRDT (`hasServer: false`). Reach for a server part only when client + shared state genuinely isn't enough.

[Testing plugins](https://docs.liebstoeckel.app/plugins/testing/)