Plugin API v1 reference
Author, install, inspect, and operate Plugin API v1 descriptors with explicit ownership, scope, authority, and failure contracts.
Declare identity before implementation
Import plugin authoring APIs from @tavojs/core/plugin. A descriptor identifies the package, publishes literal Plugin API version 1, and declares every resource it may own before any client, server, or build phase is loaded.
Every plugin descriptor must write apiVersion: 1 literally so compatibility is visible to tooling and reviewers. TAVO_PLUGIN_API_VERSION is useful for host-side compatibility checks, not as a replacement inside the descriptor.
| API / contract | Type / boundary | Default / result | Behavior |
|---|---|---|---|
id | string | required | Stable plugin identity used by dependencies, owners, exposure, and overrides. |
version | semver string | required | Plugin package version checked against dependency ranges. |
apiVersion | 1 | required | Incompatible or missing versions fail with TAVO_PLUGIN_001 before a phase is loaded. |
manifest | TavoPluginManifest | required | Declares capabilities, stores, pages, endpoints, middleware, head entries, build contributions, permissions, and public exposure. |
client / server / build | lazy phase loaders | omitted | Load environment-specific implementation only after graph preflight succeeds. |
tsimport {
defineCapability,
definePlugin,
definePluginPhase
} from "@tavojs/core/plugin";
export type AuditLog = {
write(event: { name: string; actorId?: string }): Promise<void>;
};
export const auditLog = defineCapability<AuditLog, "runtime">({
provider: "@acme/audit",
name: "audit-log",
scope: "runtime"
});
export const auditPlugin = definePlugin({
id: "@acme/audit",
version: "1.0.0",
apiVersion: 1,
manifest: {
provides: [auditLog]
},
server: async function loadAuditServerPhase() {
return definePluginPhase({
capabilities: {
"audit-log": function createAuditLog(): AuditLog {
return {
async write(event) {
await persistAuditEvent(event);
}
};
}
}
});
}
});Choose capability scope and dependency authority
Capabilities are owned tokens, not global service names. A runtime capability may be shared for the life of the plugin runtime. A request capability is created for one Fetch Request and disposed after its response body completes or is cancelled.
| API / contract | Type / boundary | Default / result | Behavior |
|---|---|---|---|
runtime | PluginCapabilityToken<T, "runtime"> | one value per plugin runtime | Use for stateless clients, shared pools, clocks, and other concurrency-safe resources. |
request | PluginCapabilityToken<T, "request"> | one value per request scope | Use for the current request, tenant, authenticated identity, trace, or request-owned transaction. |
dependencies[].capabilities | AnyPluginToken[] | no access | A consumer may resolve only tokens explicitly declared on its dependency. |
resolve / tryResolve | typed capability lookup | throws / undefined | Resolution is owner-aware; tryResolve converts unavailable access to undefined. |
Never place the current user, session, token, tenant, or permissions in a runtime capability.
Request-scoped resources stay alive while a streaming response is being read and dispose after completion or cancellation.
Dependency and capability cycles fail graph validation; they are not resolved by array order.
MVC controllers can resolve runtime tokens through
this.capabilitieswhile an active pages runtime is rendering.
tsimport {
definePlugin,
definePluginPhase
} from "@tavojs/core/plugin";
import { auditLog } from "./audit-log";
export const auditConsumer = definePlugin({
id: "@acme/audit-consumer",
version: "1.0.0",
apiVersion: 1,
manifest: {
dependencies: [{
id: "@acme/audit",
version: "^1.0.0",
capabilities: [auditLog]
}],
endpoints: [{
id: "record",
methods: ["POST"],
match: { kind: "exact", path: "/record" }
}]
},
server: async function loadConsumerServerPhase() {
return definePluginPhase({
endpoints: {
record: async function recordAuditEvent(context) {
const audit = await context.resolve(auditLog);
await audit.write({ name: "recorded" });
return Response.json({ ok: true });
}
}
});
}
});Declare store hydration explicitly
Plugin stores are runtime-scoped Tavo.js stores. Hydration is opt-in because serialized server state crosses into the browser. A hydrated store must provide validation, serialization, and deserialization together.
hydrate defaults to false.
hydrate: true without validate, serialize, and deserialize throws
TAVO_PLUGIN_002during definition.Hydration payloads are keyed by plugin owner and store name, so named instances stay isolated.
Invalid deserialized state is rejected instead of being installed into the store.
tsimport {
definePlugin,
definePluginPhase,
definePluginStore
} from "@tavojs/core/plugin";
type CounterState = { count: number };
export const counterStore = definePluginStore<CounterState>({
provider: "@acme/counter",
name: "counter",
hydrate: true,
serialize(value) {
return { count: String(value.count) };
},
deserialize(value) {
return {
count: Number((value as { count?: unknown }).count)
};
},
validate(value): value is CounterState {
return Boolean(
value &&
typeof value === "object" &&
Number.isFinite((value as { count?: unknown }).count)
);
}
});
export const counterPlugin = definePlugin({
id: "@acme/counter",
version: "1.0.0",
apiVersion: 1,
manifest: {
stores: [counterStore]
},
server: async function loadCounterServerPhase() {
return definePluginPhase({
stores: {
counter: function createCounterState(): CounterState {
return { count: 0 };
}
}
});
}
});Match every manifest contribution to its runtime phase
| API / contract | Type / boundary | Default / result | Behavior |
|---|---|---|---|
pages | page phase | namespaced | Plugin pages remain under their plugin namespace unless declared exposure maps them publicly. |
endpoints | server phase | namespaced | Declare methods, exact or subtree matching, and origin validation. Exact and method-specific matches win deterministically. |
middleware | server or page phase | declared stage | Use server:before-handler, page:before-app, or page:after-app plus explicit before/after owner constraints. |
head | client/server phase | escaped TSX | Declare a stable key and singleton or multi cardinality. Raw strings require unsafeHeadHtml on the entry and a manifest permission with a reason. |
build | build phase | none | Declare aliases, defines, and ordered build plugin IDs before implementing their values. |
setup / dispose | phase lifecycle | omitted | Setup runs after successful initialization; dispose releases plugin-owned runtime resources in reverse lifecycle order. |
Framework paths under /_tavo remain reserved even when an application remaps exposure.
Endpoint, page, singleton head, alias, and define collisions fail unless an exact owner-aware override resolves them.
Plugin endpoint handlers return a Fetch Response. Uncaught request or disposal failures use
TAVO_PLUGIN_009.Raw head HTML must be declared twice:
unsafeHeadHtmlon the head entry and theunsafeHeadHtmlpermission with a reviewable reason.
tsimport { definePlugin } from "@tavojs/core/plugin";
export default definePlugin({
id: "@example/audit",
apiVersion: 1,
version: "1.0.0",
manifest: {
endpoints: [{
id: "events",
methods: ["POST"],
match: { kind: "exact", path: "/events" }
}],
middleware: [{
id: "request-context",
target: "server",
stage: "server:before-handler"
}]
},
server: () => import("./server")
});Install defaults, named instances, and overrides
The top-level plugins field in tavo.config.ts accepts the TavoPluginInput union. Use a simple array for default installations. Use the { use, overrides } form for named instances, disabling an installation, exposure remapping, or explicit replacement.
The owner of a default installation is plugin-id#default. A named installation uses plugin-id#
instanceId.Installing the same plugin more than once without distinct
instanceIdvalues is fatal.enabled: false omits the installation and its manifest-declared permissions and exposure.
expose remaps manifest-declared page or server exposure; it does not grant undeclared contributions.
Override kinds are page, endpoint, head, alias, and define. Both the replaced owner and winning owner must match exactly.
tsimport { defineConfig } from "@tavojs/core/config";
import { analyticsPlugin } from "./src/plugins/analytics";
import { dashboardPlugin } from "./src/plugins/dashboard";
export default defineConfig({
plugins: {
use: [
{
plugin: analyticsPlugin,
instanceId: "primary",
expose: {
server: {
from: "/",
to: "/analytics"
}
}
},
{
plugin: dashboardPlugin,
instanceId: "primary"
},
{
plugin: dashboardPlugin,
instanceId: "disabled-preview",
enabled: false
}
],
overrides: [{
kind: "page",
key: "/dashboard",
replace: {
plugin: "@acme/dashboard",
instanceId: "primary"
},
with: {
owner: "app"
}
}]
}
});Inspect before loading phases
Use the CLI inspection command for normal plugin verification. It reports the serializable preflight without presenting framework host compilation or runtime construction as plugin-author APIs. Experimental tooling that genuinely needs the graph can import from @tavojs/core/dev.
Inspect owners, versions, dependencies, capabilities, mounts, middleware, endpoints, head keys, build values, permissions, exposure, and overrides.
A diagnostic includes code, severity, phase, message, and optional resource, owners, and remediation hint.
TAVO_PLUGIN_001rejects incompatible API versions before any phase load.TAVO_PLUGIN_002through 009 cover invalid identity/manifest, ownership, dependency, cycle, permission, phase, initialization/build, and request/disposal failures.
bashnpx tavo inspect plugins --json
npx tavo check
npx tavo verify --json
npx tavo buildLook up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.