Skip to main content

Backend Plugin API

The backend API is what a server-side plugin receives in its activate() call: restricted abstractions for routes, storage, events, logging, network access, providers, and files — and nothing else.

Entry Point

A backend plugin exports a definition with an activate(api) function that receives the ServerPluginApi object:

import { definePlugin } from '@neotavern/plugin-sdk';

export default definePlugin({
activate(api) {
const off = api.routes.get('/hello', async (request) => ({
status: 200,
body: { hello: 'world' },
}));
},
});

The backend entry runs as a separate Node.js process. The plugin never receives the Fastify root instance, the SQLite connection, internal tables, absolute paths, the full environment, or other providers' API keys.

Routes

api.routes is a scoped router mounted under /api/plugins/{pluginId}/. Each method takes a path and a handler and returns a cleanup function:

  • api.routes.get(path, handler)
  • api.routes.post(path, handler)
  • api.routes.put(path, handler)
  • api.routes.delete(path, handler)

A PluginRequest carries params, query, headers, a parsed JSON body, and an AbortSignal. A PluginResponse is { status, body, headers }. Handlers may return a value directly or a promise; the host enforces timeouts and cancels work through the signal.

Storage

api.storage is a namespaced key/value store isolated per plugin:

await api.storage.set('state', { count: 1 });
const state = await api.storage.get('state');
await api.storage.delete('state');
const keys = await api.storage.keys();

Data is scoped to your plugin id, so two plugins can never collide.

Events and Logging

api.events is the same typed event bus the frontend uses. Subscribing returns an unsubscribe function, and all subscriptions are removed automatically on disable, crash, or shutdown. Emitting is restricted to your own namespace ({pluginId}.event), payloads must be JSON-safe, and the host caps payload size and the number of event names per runtime.

api.logger provides debug, info, warn, and error methods, each taking a message and optional metadata. Logs never include secrets.

Permission-Checked Fetch

api.fetch is fetch guarded by the plugin's network:<host> permissions:

const response = await api.fetch('https://api.example.com/data', {
method: 'GET',
headers: { Accept: 'application/json' },
signal,
});

Requests to hosts that are not granted are rejected before any network activity. Secrets from other providers are never injected into your requests. The response object exposes ok, status, text(), and json().

Providers and Context Strategies

api.providers lets a plugin extend generation:

  • api.providers.register(kind, factory, options) registers a new provider adapter kind (requires providers.register). Registration returns a cleanup function.
  • api.providers.registerTokenizer(profile) registers a local model-specific tokenizer. A profile declares id, approximate, matches(model), and count(text). Exact tokenizers can be built from tiktoken, SentencePiece, or Hugging Face tokenizer JSON; until one is registered for a model, the host falls back to a script-aware heuristic and marks counts as approximate. Registration is removed automatically on deactivate.

api.contextStrategies.register(strategy) adds a context-shifting strategy. The host verifies that system, pinned, and current-user blocks survive, and applies the final token budget itself — the fitsBudget value a strategy returns is not trusted.

api.postProcessors.register(processor) adds a post-generation hook. It runs after the stream completes and before the message is saved; returning a new string replaces the assistant reply. It requires prompt.modify.

Virtual Filesystem

api.files is a sandboxed virtual filesystem rooted at the plugin's own data directory:

await api.files.write('notes.txt', 'content');
const content = await api.files.read('notes.txt');
const entries = await api.files.list('.');
await api.files.delete('notes.txt');

Paths cannot escape the plugin root, so a plugin can only ever touch its own data.

What a Backend Plugin Cannot Do

The API surface is deliberately small. There is no way to reach the host database, other plugins' storage, arbitrary filesystem paths, or unvetted network hosts. If the SDK does not expose it, it is not accessible. The generated Plugin SDK reference lists the full ServerPluginApi surface, and Providers explains how provider plugins fit into the model.