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.
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());
}
}
})
});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.
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
definePluginFactorywhen consumers configure typed plugin options anddefinePluginPhaseto 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.
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.
Headmanifests declare a stable key, singleton or multi cardinality, and whether unsafe HTML permission is required. A raw contribution must declare theunsafeHeadHtmlpermission with a reviewable reason.Capabilities explicitly declare runtime or request scope. Request-scoped factories receive the current Fetch Request.
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 useTAVO_PLUGIN_API_VERSIONbecause they are rebuilt with their host.checkPluginCompatibilityaccepts a minimal { id,apiVersion} descriptor and rejects missing or incompatible versions withTAVO_PLUGIN_001before 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.