# Plugins

> What a plugin is, why its data and table halves run on two machines, its three capabilities, the files it can ship, and how one reaches a table.

- Source: https://diceytable.com/docs/plugins
- Last verified: 2026-09-12

A **plugin** brings card data from an external provider onto a DiceyTable table. It is the
counterpart to a mod: where a mod ships table content and a sandboxed script, a plugin ships a
declaration of *where card data comes from* and *what shape it arrives in*.

Like a mod, a plugin is a public GitHub repository, registered against an **immutable commit
sha**. There is no build step — the reviewed source is what runs, which is only checkable
because anyone can read it. Its manifest is `diceytable.plugin.json` at the repository root.

Plugins are a smaller and stricter thing than mods, and most games never need one. If your
cards are a fixed list you can write down, ship them as a card catalogue in your own mod's
repository instead — that path involves no plugin, no registry and no external provider. A
plugin earns its complexity only when the card data belongs to somebody else's service.

## The two halves

A plugin has up to two halves, and they run on different machines in different trust domains.
**They cannot talk to each other directly**, and the whole security story rests on that.

| Half | Has | Never has |
|---|---|---|
| **Data half** | Network access to declared hostnames, from **our** servers | Table state, deck contents, user identity, auth tokens |
| **Table half** | Table state, through the **existing mod sandbox** | Network access of any kind |

The table half is not a second runtime. It is a mod script — the same
`SandboxedModRunner` iframe, the same capability model, the same static scanner. Everything
on [Script safety](https://diceytable.com/docs/mods/scanner/script-safety) applies to it unchanged.

The data half runs no author code at all. Its behaviour is a **declaration**: named endpoints
and a field mapping, both interpreted by the platform. See
[Writing a plugin](https://diceytable.com/docs/plugins/authoring#the-card-mapping) for the mapping grammar.

## The three capabilities

A plugin manifest declares `capabilities.allowed` from a closed set of three, or leaves it empty if
the plugin only [ships files](https://diceytable.com/docs/plugins#plugins-can-also-ship-files). These are the
*plugin* capability slugs and are unrelated to the [mod capability
slugs](https://diceytable.com/docs/mods/manifest/capabilities) — a plugin's table half declares those separately, in
the ordinary way, for the script it ships.

| Capability | What it permits |
|---|---|
| `network` | The data half may fetch from the manifest's declared hostnames. The platform hands it no table state. Requires at least one declared origin. |
| `table` | The plugin ships a table-side script, which runs in the mod sandbox with no network. Requires `tableScript` to name a `.js` file. |
| `combined` | The data half may additionally *receive table-derived data* from the table half. Requires `network` and `table` as well, plus a disclosure block carrying the platform's warning text verbatim. |

`combined` is allowed and is deliberately the hard path. A plugin that can read table state and
send data to its own servers is, on the host, a cheating vector: it can observe other players'
hands and face-down cards and transmit them off the table. The warning shown to players is
written by the platform, not by the author, and every seated player sees it —
[the details, and what the author does own](https://diceytable.com/docs/plugins/authoring#the-combined-capability).

### `network` alone is not a promise that nothing table-derived reaches the provider

The platform hands a `network` plugin no table state, but the **caller** chooses the parameter
values, and a declared `text` parameter is free text. A mod holding both `read-hidden-information`
and `plugin-call` can therefore read hidden state and put it into an ordinary search parameter of
an ordinary `network` plugin.

DiceyTable treats that pairing as `combined`, because that is what it amounts to. Whenever a table
runs a mod holding **both** of those capabilities alongside a plugin declaring `network`, the
room's capability disclosure elevates that plugin to `combined`: every seated player gets the
verbatim off-table warning before they can sit, and the table blocks on it. Neither party declared
`combined` — the *room* did.

Two consequences worth planning for:

- As a **plugin** author, a mod you have never heard of can make your plugin read as `combined` at
  someone's table. Nothing about your manifest changes, and nothing you can declare prevents it.
- As a **mod** author, declaring `read-hidden-information` together with `plugin-call` makes every
  table that loads your mod alongside any network plugin show the strongest warning DiceyTable
  has. If your mod does not genuinely need to read hidden information, do not declare it.

Every mod→plugin call additionally emits a server-side flow record: plugin, function, room,
calling mod, **payload size in bytes**, and whether it was accepted. The size is recorded so the
volume of a channel like this is visible; the payload itself never is.

## How a plugin reaches a table

Three routes, and they are independent.

1. **As a game's card source.** A mod's `data/cardSchema.json` declares
   `source: { kind: "plugin", pluginId: "…" }`. The deck builder then resolves that game's cards
   through the plugin instead of a catalogue file in the mod's repository. Nothing in the mod's
   script is involved.
2. **From a mod script.** A mod declares the plugin in its own manifest and calls a declared
   function with `api.callPlugin(...)`, under the `plugin-call` capability. See
   [Calling a plugin from a mod](https://diceytable.com/docs/plugins/calling-from-a-mod).
3. **As a deck import.** A plugin may declare a `deckImport` block, and a game names it in
   `data/cardSchema.json`. A person in the deck builder pastes a deck id or a link and gets a
   saved, editable deck. No mod code runs on this route, so it needs **no mod capability** at
   all — see [Deck import](https://diceytable.com/docs/plugins/authoring#deck-import-letting-a-plugin-supply-a-deck-not-just-cards).

Both routes go through our servers. Neither one puts a provider's address, a credential, or a
raw response in a browser.

## Plugins can also ship files

Separately from card data, a plugin may declare **resources** — models, textures, materials and
deck definitions in its own repository — as library items mod authors can use in their projects.
They are declared one file at a time under `resources` in `diceytable.plugin.json`, content-checked
by the scanner at the registered commit sha, and served through the asset CDN at **exactly** the
declared paths and nothing else from the repository. Every resource carries its own credit, and
`sound` is deliberately not a resource kind. See
[Resources](https://diceytable.com/docs/plugins/authoring#resources-files-a-project-can-copy).

A plugin can ship files and nothing else: it declares an empty capability list
(`"capabilities": { "version": "1", "allowed": [] }`) and no origins, endpoints or table script.
An empty list is accepted only when the plugin declares at least one resource
([example](https://diceytable.com/docs/plugins/authoring#using-a-plugin-only-for-its-resources)). A mod that uses only a
plugin's files lists it with an empty function list — `{ "id": "…", "functions": [] }` — which grants no call and needs no
`plugin-call` capability ([details](https://diceytable.com/docs/plugins/calling-from-a-mod#using-a-plugin-only-for-its-resources)).

Resources are data for the editor and renderer. They do not give a mod script a network, and a
registered plugin is still inert until a project lists it.

## Four rules that shape everything else

These come up on every page below, so they are worth reading once here.

1. **A plugin never supplies a URL.** It declares bare hostnames and named endpoints with typed
   parameters; the platform composes the request. A plugin has no way to *express* a
   destination, which is what makes the origin allowlist unbypassable rather than merely
   enforced.
2. **No executable code in the data half.** The response mapping is a closed grammar — dotted
   paths and a fixed list of transforms — with no expression language, no regexes and no
   callbacks.
3. **No credential, ever, in the manifest.** Plugin source is public and sha-pinned, so a key in
   the manifest is a published key. Credentials are written server-side, separately.
4. **The manifest is strictly parsed.** An unrecognised key is a parse error, not something
   quietly ignored. That is what makes rule 3 mechanical instead of advisory.

## Where to go next

- [Writing a plugin](https://diceytable.com/docs/plugins/authoring) — the manifest end to end: origins, endpoints, the
  card mapping, the exposed API, capabilities, resources, credentials, quotas and the failure enum.
- [A worked example](https://diceytable.com/docs/plugins/scryfall-example) — one complete, validated manifest for a real
  card provider, read block by block, including the four things it cannot do yet. Start here if
  you would rather see a whole plugin than a field table.
- [`breakingVersion` and migration](https://diceytable.com/docs/plugins/breaking-changes) — when a version bump forks
  every saved deck, and what you owe the people whose decks it strands.
- [Calling a plugin from a mod](https://diceytable.com/docs/plugins/calling-from-a-mod) — `listPlugins`, `callPlugin`,
  the `plugin-call` capability, and why a dynamic call is a publish error.

## See also

- [Mod capabilities](https://diceytable.com/docs/mods/manifest/capabilities) — the mod-side capability model a
  plugin's table half lives under.
- [Script safety](https://diceytable.com/docs/mods/scanner/script-safety) — the five patterns that reject a script,
  applied to a plugin's table half exactly as to a mod's.
- [The mod `api` object](https://diceytable.com/docs/scripting-api/api) — including `listPlugins` and `callPlugin`.
- [Building a deck](https://diceytable.com/docs/editor/workflows/build-a-deck) — the deck builder a plugin-sourced
  game feeds.
