Plugins
TypoKit’s plugin system lets you hook into the build pipeline and the application lifecycle with a single interface. Plugins can generate code at build time, register services at startup, and expose CLI commands — all without touching your application code.
The TypoKitPlugin Interface
Section titled “The TypoKitPlugin Interface”Every plugin implements the TypoKitPlugin interface exported from @typokit/core:
import type { TypoKitPlugin, BuildPipeline, AppInstance, AppError, RequestContext, SchemaChange, CliCommand, InspectEndpoint,} from "@typokit/core";
export interface TypoKitPlugin { name: string;
// ─── BUILD-TIME HOOKS (called during `typokit build`) onBuild?(pipeline: BuildPipeline): void;
// ─── RUNTIME LIFECYCLE HOOKS onStart?(app: AppInstance): Promise<void>; // Before server listens onReady?(app: AppInstance): Promise<void>; // After routes registered & listening onError?(error: AppError, ctx: RequestContext): void; // Unhandled errors observed onStop?(app: AppInstance): Promise<void>; // Graceful shutdown
// ─── DEV-ONLY HOOKS onSchemaChange?(changes: SchemaChange[]): void; // Schema file changed (dev mode)
// ─── OPTIONAL EXTENSIONS commands?(): CliCommand[]; // Expose CLI subcommands inspect?(): InspectEndpoint[]; // Debug sidecar endpoints}All hooks are optional — implement only the ones you need.
Build-Time Hooks: The Tapable Pipeline
Section titled “Build-Time Hooks: The Tapable Pipeline”The onBuild hook receives a BuildPipeline with seven async series hooks. Each hook fires sequentially, and every registered tap completes before the next phase begins:
beforeTransform → afterTypeParse → afterValidators → afterRouteTable → emit → compile → doneexport interface BuildPipeline { hooks: { /** Register additional type sources before parsing */ beforeTransform: AsyncSeriesHook<[BuildContext]>;
/** Inspect or modify parsed types */ afterTypeParse: AsyncSeriesHook<[SchemaTypeMap, BuildContext]>;
/** Add custom validators alongside generated ones */ afterValidators: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
/** Inspect the compiled route table */ afterRouteTable: AsyncSeriesHook<[CompiledRouteTable, BuildContext]>;
/** Emit generated artifacts (validators, clients, etc.) */ emit: AsyncSeriesHook<[GeneratedOutput[], BuildContext]>;
/** Override or extend the compilation step (e.g., cargo build) */ compile: AsyncSeriesHook<[CompileContext, BuildContext]>;
/** Cleanup and reporting */ done: AsyncSeriesHook<[BuildResult]>; };}Plugins tap into individual phases using pipeline.hooks.{phase}.tap(name, fn):
const myPlugin: TypoKitPlugin = { name: "my-metrics-plugin",
onBuild(pipeline) { // Tap into the afterTypeParse phase pipeline.hooks.afterTypeParse.tap("my-metrics", async (typeMap, ctx) => { console.log(`Parsed ${typeMap.size} types`); });
// Tap into the emit phase to generate additional files pipeline.hooks.emit.tap("my-metrics", async (outputs, ctx) => { outputs.push({ path: "metrics/type-stats.json", content: JSON.stringify({ typeCount: ctx.types.size }), }); }); },};Runtime Lifecycle Hooks
Section titled “Runtime Lifecycle Hooks”Runtime hooks fire during application startup and shutdown, in this order:
createApp() → onStart → server.listen() → onReady │ (app running) │ app.close() → onStop → server.close()| Hook | When | Typical use |
|---|---|---|
onStart | Before the server begins listening | Register services, open database connections, start background workers |
onReady | After routes are registered and the server is listening | Health check registration, log “ready” message, start sidecar servers |
onError | When an unhandled error is observed | Error tracking, metric collection, alerting |
onStop | During graceful shutdown | Close connections, flush buffers, stop background workers |
onSchemaChange | A type file changed (dev mode only) | Clear caches, regenerate derived data |
Lifecycle hooks are invoked in plugin registration order and run sequentially (each onStart completes before the next plugin’s onStart runs).
Plugin Registration in createApp()
Section titled “Plugin Registration in createApp()”Plugins are passed as an array in the createApp() options:
import { createApp } from "@typokit/core";import { createNativeAdapter } from "@typokit/adapter-native";import { debugPlugin } from "@typokit/plugin-debug";import { wsPlugin } from "@typokit/plugin-ws";
const app = createApp({ server: createNativeAdapter(), plugins: [ debugPlugin({ port: 9229 }), wsPlugin({ heartbeatInterval: 30_000 }), ],});
await app.listen(3000);Plugins are stored on the AppInstance and their lifecycle hooks are called automatically:
export interface AppInstance { name: string; plugins: TypoKitPlugin[]; services: Record<string, unknown>; // DI container for plugins}Why No onRequest / onResponse Hooks
Section titled “Why No onRequest / onResponse Hooks”You might expect plugins to have onRequest and onResponse hooks like other frameworks. TypoKit deliberately omits these — plugins that need per-request behavior register middleware instead.
This avoids two separate per-request execution models and keeps the system composable:
import { defineMiddleware } from "@typokit/core";
const rateLimitPlugin: TypoKitPlugin = { name: "rate-limit",
async onStart(app) { // Register a TypoKit middleware — same system your app uses app.services["rate-limit"] = defineMiddleware<{ rateLimit: { remaining: number } }>({ name: "rate-limit", priority: -100, // Run early async handler(ctx) { const remaining = await checkLimit(ctx.requestId); if (remaining <= 0) { ctx.fail(429, "Rate limit exceeded"); } return { rateLimit: { remaining } }; }, }); },};Benefits of this approach:
- Single execution model — middleware and plugin per-request logic use the same pipeline, with the same priority ordering and context type-narrowing
- Composable — middleware from plugins composes with your application middleware naturally
- Testable — middleware can be tested independently, without a plugin harness
- AI-agent friendly — one consistent concept to reason about, not two parallel hooks systems
For HTTP-level concerns that must run before TypoKit normalization (CORS, compression), plugins can access the raw server via app.getNativeServer() and register framework-native middleware directly.
Official Plugins
Section titled “Official Plugins”@typokit/plugin-debug
Section titled “@typokit/plugin-debug”A read-only HTTP debug sidecar that exposes introspection endpoints on a separate port. Designed for both human developers and AI agents.
import { debugPlugin } from "@typokit/plugin-debug";
createApp({ server: createNativeAdapter(), plugins: [ debugPlugin({ port: 9229, // Sidecar port (default: 9229) apiKey: process.env.DEBUG_API_KEY, // Optional auth for production allowedIPs: ["10.0.0.0/8"], // CIDR allowlist }), ],});Endpoints exposed:
| Endpoint | Description |
|---|---|
/_debug/routes | Route table with params, methods, and handler metadata |
/_debug/middleware | Registered middleware names and priority ordering |
/_debug/performance | Request latency histograms (p50, p95, p99) |
/_debug/errors | Recent errors (1000-entry ring buffer) |
/_debug/health | Memory usage, uptime, and process metrics |
/_debug/dependencies | Service dependency graph |
/_debug/traces | OpenTelemetry span data (500-entry ring buffer) |
/_debug/logs | Application log entries (2000-entry ring buffer) |
Security modes:
- Development — No authentication, binds to
0.0.0.0, auto-enabled - Production — Opt-in with API key, IP allowlist with CIDR support, rate limiting
Hooks used: onStart (register data collection services), onReady (start sidecar server), onError (record errors), onStop (shut down sidecar), onSchemaChange (clear cached route table).
@typokit/plugin-ws
Section titled “@typokit/plugin-ws”Schema-first typed WebSocket support with runtime validation and connection management. This plugin hooks into both build-time and runtime phases.
import { wsPlugin } from "@typokit/plugin-ws";
createApp({ server: createNativeAdapter(), plugins: [ wsPlugin({ heartbeatInterval: 30_000, // Keep-alive ping interval (ms) }), ],});Build-time code generation:
The plugin taps into the build pipeline to generate WebSocket validators and route tables:
afterTypeParse— Extracts WebSocket channel contracts from the type mapemit— Generatesws-validators.tsandws-route-table.ts
Runtime services (exposed via app.services._ws):
| Method | Description |
|---|---|
getConnectionManager() | Access the WsConnectionManager |
send(connectionId, data) | Send a message to a single connection |
broadcast(channel, data) | Broadcast to all connections on a channel |
registerValidator(channel, fn) | Add a custom message validator |
registerHandlers(defs) | Register channel handlers dynamically |
Connection lifecycle:
Client connects → handleWsUpgrade() → onConnect │ onMessage (validated) ←→ handler dispatch │ Client disconnects → onDisconnectMessages are validated against the channel’s schema before dispatch. Invalid messages are rejected with a structured error frame. A heartbeat timer (default 30s) keeps connections alive and detects stale clients.
@typokit/plugin-axum
Section titled “@typokit/plugin-axum”A Rust code generation plugin that compiles TypeScript schema types and route contracts into a complete, production-ready Axum web server backed by PostgreSQL and sqlx. This plugin hooks into both the emit and compile build phases.
import { axumPlugin } from "@typokit/plugin-axum";
createApp({ plugins: [ axumPlugin({ db: "sqlx", // Database adapter (default: 'sqlx') outDir: ".", // Output directory (default: project root) }), ],});What it generates:
| Directory | Contents | Overwrite |
|---|---|---|
.typokit/models/ | Rust structs with serde, validator, and sqlx derives | ✅ Always |
.typokit/db/ | PgPool connection setup and typed CRUD repository functions | ✅ Always |
.typokit/router.rs | Axum Router with typed route registrations | ✅ Always |
.typokit/app.rs | AppState struct (shared PgPool) | ✅ Always |
.typokit/error.rs | AppError enum → HTTP status code mapping | ✅ Always |
.typokit/migrations/ | SQL CREATE TABLE migration files | ✅ Always |
src/handlers/ | Per-entity handler stubs (extract → service → respond) | ❌ Never |
src/services/ | Business logic layer stubs | ❌ Never |
src/middleware/ | Auth/logging middleware stubs | ❌ Never |
src/main.rs | Tokio async entrypoint | ✅ Always |
src/lib.rs | Module bridge (#[path] to .typokit/) | ✅ Always |
Cargo.toml | Project manifest with all dependencies | ✅ Always |
Hooks used: emit (generate Rust source files from parsed type metadata and route contracts), compile (run cargo build instead of the default TypeScript compiler, setting compileCtx.handled = true to skip tsc).
Prerequisites:
- Rust (1.85+ for edition 2024)
- PostgreSQL 16+
- The
sqlxCLI (optional, for running migrations):cargo install sqlx-cli --no-default-features --features postgres
See the Building a Rust/Axum Server guide for a step-by-step walkthrough.
Writing Your Own Plugin
Section titled “Writing Your Own Plugin”Here’s a complete example of a plugin that tracks request counts and exposes them via a CLI command:
import type { TypoKitPlugin, AppInstance } from "@typokit/core";
let requestCount = 0;
export function requestCounterPlugin(): TypoKitPlugin { return { name: "request-counter",
async onStart(app: AppInstance) { // Register a service other plugins and handlers can use app.services["request-counter"] = { getCount: () => requestCount, reset: () => { requestCount = 0; }, }; },
onError(error, ctx) { // Track errors separately if needed console.error(`[request-counter] Error in ${ctx.requestId}:`, error.message); },
async onStop(app: AppInstance) { console.log(`[request-counter] Total requests served: ${requestCount}`); },
// Expose a CLI subcommand: `typokit request-counter stats` commands() { return [ { name: "request-counter:stats", description: "Show request count statistics", handler: () => { console.log(`Requests: ${requestCount}`); }, }, ]; },
// Expose a debug sidecar endpoint inspect() { return [ { path: "/_inspect/request-counter", handler: () => ({ count: requestCount }), }, ]; }, };}Plugin Design Guidelines
Section titled “Plugin Design Guidelines”-
Name your plugin — The
namefield is used in logs, error messages, and the debug sidecar. Use a descriptive kebab-case name. -
Use services for DI — Register capabilities on
app.servicesduringonStartso other plugins and handlers can discover them at runtime. -
Prefer middleware over lifecycle hooks for per-request logic — If you need to run code on every request, register middleware during
onStart. Don’t try to replicate onRequest/onResponse semantics. -
Keep build hooks pure —
onBuildtaps should read from the pipeline context and append to outputs. Avoid side effects that depend on the file system or network. -
Handle errors gracefully — If your plugin’s
onReadyoronStopthrows, it can break the application lifecycle. Wrap cleanup in try/catch. -
Support both dev and production — The debug plugin demonstrates this pattern: full access in dev mode, opt-in with authentication in production.