Navigated to /docs/core/plugins

Plugins

Declare framework integrations with an explicit manifest and lazy client, server, and build implementations.

Use a plugin for framework integration

Create a plugin when a package must contribute framework-owned behavior such as capabilities, stores, pages, endpoints, middleware, head entries, or build configuration. Keep ordinary feature code in pages, components, and server modules.

A plugin has an identity, a package version, a manifest of everything it may contribute, and lazy phase loaders. The manifest is the contract; a phase may implement only the declarations owned by that plugin.

Declare Plugin API v1 before implementing

Published plugins bake the literal apiVersion: 1 into their descriptor. Keep server implementation behind the server phase so client builds do not evaluate private dependencies.

TS
tsimport {
  definePlugin,
  definePluginPhase
} from "@tavojs/core/plugin";

export const projectApi = definePlugin({
  id: "@project/api",
  version: "1.0.0",
  apiVersion: 1,
  manifest: {
    endpoints: [{
      id: "projects",
      methods: ["GET"],
      match: { kind: "exact", path: "/api/projects" }
    }],
    exposure: [{
      target: "server",
      from: "/api/projects",
      to: "/api/projects",
      reason: "Expose the project's public read endpoint."
    }]
  },
  server: async () => definePluginPhase({
    endpoints: {
      projects: async () => {
        const { listProjects } = await import("../server/projects");
        return Response.json(await listProjects());
      }
    }
  })
});
TS
tsimport "@tavojs/core/server-only";

type Project = { id: string; name: string };

const projects: Project[] = [
  { id: "alpha", name: "Alpha" },
  { id: "beta", name: "Beta" }
];

export async function listProjects(): Promise<Project[]> {
  return projects;
}

Install it in application configuration

Add the plugin instance to the existing plugins array in tavo.config.ts. Preserve other plugins and configuration fields; array order is not a substitute for declared dependencies or middleware ordering.

TS
tsimport { defineConfig } from "@tavojs/core/config";
import { projectApi } from "./src/plugins/project-api";

export default defineConfig({
  pagesDir: "src/pages",
  cssEntries: ["src/styles.css"],
  plugins: [projectApi]
});

Plugin hooks and lifecycle

  • id identifies the package contract; version is the plugin package version used for dependency checks.

  • The manifest declares every capability, store, page, endpoint, middleware entry, head entry, build contribution, permission, and public exposure the plugin may own.

  • Client, server, and build loaders are lazy boundaries. Put environment-specific imports inside the matching phase module.

  • Use definePluginFactory when consumers configure typed plugin options and definePluginPhase to preserve literal implementation keys.

  • Runtime stores and capabilities may be shared between SSR requests. Use request-scoped capabilities, middleware, or handlers for the current user, session, or token.

TS
tstype TavoPlugin = {
  id: string;
  version: string;
  apiVersion: 1;
  manifest: TavoPluginManifest;
  client?: () => MaybePromise<TavoPluginPhase | { default: TavoPluginPhase }>;
  server?: () => MaybePromise<TavoPluginPhase | { default: TavoPluginPhase }>;
  build?: () => MaybePromise<TavoPluginPhase | { default: TavoPluginPhase }>;
};

Manifest declarations and phase implementations

  • Every implementation key must match an ID declared in the manifest. Missing, extra, or cross-plugin contributions become diagnostics.

  • Endpoint manifests declare allowed methods, exact or subtree path matching, and optional origin-validation policy. Endpoint handlers return a Response.

  • Plugin pages and endpoints are namespaced by default. A manifest exposure must deliberately map them to a public application URL.

  • Middleware manifests declare server or page target, lifecycle stage, and before/after ownership constraints.

  • Head manifests declare a stable key, singleton or multi cardinality, and whether unsafe HTML permission is required. A raw contribution must declare the unsafeHeadHtml permission with a reviewable reason.

  • Capabilities explicitly declare runtime or request scope. Request-scoped factories receive the current Fetch Request.

TS
tstype TavoPluginPhase = {
  capabilities?: Record<string, PluginResourceFactory | PluginRequestResourceFactory>;
  stores?: Record<string, PluginStoreFactory>;
  pages?: Record<string, PageModule>;
  endpoints?: Record<string, PluginServerHandler>;
  middleware?: Record<string, PageMiddleware | PluginServerMiddleware>;
  head?: Record<string, Child | string | ((context) => MaybePromise<Child | string>)>;
  build?: { plugins?: Record<string, unknown> };
  setup?: (context: PluginResolveContext) => MaybePromise<void>;
  dispose?: () => MaybePromise<void>;
};

Compatibility, ordering, and diagnostics

  • Published descriptors must bake in the literal apiVersion: 1. Application-local plugins may use TAVO_PLUGIN_API_VERSION because they are rebuilt with their host.

  • checkPluginCompatibility accepts a minimal { id, apiVersion } descriptor and rejects missing or incompatible versions with TAVO_PLUGIN_001 before loading any phase.

  • Declare plugin dependencies by plugin ID, compatible package version, and any required capability tokens.

  • Declare middleware ordering with before and after ownership IDs. Dependency and ordering cycles become diagnostics.

  • Duplicate ownership, undeclared contributions, incompatible contracts, unapproved raw head HTML, and implicit route replacement are rejected or diagnosed.

  • Install a default plugin with plugins: [plugin]. Repeated installations require a unique application-supplied instanceId.

  • Use plugins: { use, overrides } only for owner-aware replacement.

  • Plugin diagnostics use the severity field.

Look up exact public types

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