Navigated to /docs/core/plugin-api-v1

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 / contractType / boundaryDefault / resultBehavior
idstringrequiredStable plugin identity used by dependencies, owners, exposure, and overrides.
versionsemver stringrequiredPlugin package version checked against dependency ranges.
apiVersion1requiredIncompatible or missing versions fail with TAVO_PLUGIN_001 before a phase is loaded.
manifestTavoPluginManifestrequiredDeclares capabilities, stores, pages, endpoints, middleware, head entries, build contributions, permissions, and public exposure.
client / server / buildlazy phase loadersomittedLoad environment-specific implementation only after graph preflight succeeds.
TS
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 / contractType / boundaryDefault / resultBehavior
runtimePluginCapabilityToken<T, "runtime">one value per plugin runtimeUse for stateless clients, shared pools, clocks, and other concurrency-safe resources.
requestPluginCapabilityToken<T, "request">one value per request scopeUse for the current request, tenant, authenticated identity, trace, or request-owned transaction.
dependencies[].capabilitiesAnyPluginToken[]no accessA consumer may resolve only tokens explicitly declared on its dependency.
resolve / tryResolvetyped capability lookupthrows / undefinedResolution 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.capabilities while an active pages runtime is rendering.

TS
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_002 during 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.

TS
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 / contractType / boundaryDefault / resultBehavior
pagespage phasenamespacedPlugin pages remain under their plugin namespace unless declared exposure maps them publicly.
endpointsserver phasenamespacedDeclare methods, exact or subtree matching, and origin validation. Exact and method-specific matches win deterministically.
middlewareserver or page phasedeclared stageUse server:before-handler, page:before-app, or page:after-app plus explicit before/after owner constraints.
headclient/server phaseescaped TSXDeclare a stable key and singleton or multi cardinality. Raw strings require unsafeHeadHtml on the entry and a manifest permission with a reason.
buildbuild phasenoneDeclare aliases, defines, and ordered build plugin IDs before implementing their values.
setup / disposephase lifecycleomittedSetup 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: unsafeHeadHtml on the head entry and the unsafeHeadHtml permission with a reviewable reason.

TS
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 instanceId values 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.

TS
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_001 rejects incompatible API versions before any phase load.

  • TAVO_PLUGIN_002 through 009 cover invalid identity/manifest, ownership, dependency, cycle, permission, phase, initialization/build, and request/disposal failures.

BASH
bashnpx tavo inspect plugins --json
npx tavo check
npx tavo verify --json
npx tavo build

Look up exact public types

Follow linked API names to their canonical TypeScript declarations and package boundaries.