Authoring Plugins & Drivers
Iskra is extended through three contracts: Drivers (lifecycle-managed resources), Plugins (lightweight install-time extensions), and Features (Web Kit modules with dependency resolution). This guide shows how to author each one.
Writing a Driver
Section titled “Writing a Driver”Implement the Driver interface. The App calls init() then start() during app.start(), and stop() during app.stop().
import type { Driver, App } from '@iskra-bun/core';
export class MetricsDriver implements Driver { name = 'MetricsDriver'; #app!: App; #timer?: ReturnType<typeof setInterval>;
async init(app: App) { // Keep a reference; register services in the DI container. this.#app = app; app.context.set('metrics', this); }
async start() { this.#timer = setInterval(() => { this.#app.emit('metrics:tick', { at: Date.now() }); }, 5000); this.#app.logger.info('MetricsDriver started'); }
async stop() { if (this.#timer) clearInterval(this.#timer); this.#app.logger.info('MetricsDriver stopped'); }}Register it:
app.register(new MetricsDriver());Declare what the driver puts on the app, so app.context.get('metrics') and app.on('metrics:tick', …) are typed for everyone using it (the kits do the same):
declare module '@iskra-bun/core' { interface AppContextRegistry { metrics: MetricsDriver; } interface AppEvents { 'metrics:tick': { at: number }; }}Guidelines
Section titled “Guidelines”initacquires nothing heavy — store theAppreference and register DI entries. Do not open sockets or connections here.startopens resources — connections, servers, intervals.stopis the mirror ofstart— release everything, idempotently.- Throw typed errors — wrap failures in
DriverErrorwithcauseandcontextso the lifecycle reports them clearly.
Writing a Plugin
Section titled “Writing a Plugin”Plugins have no lifecycle; install(app) runs immediately when you call app.use(plugin).
import type { Plugin, App } from '@iskra-bun/core';
export const requestIdPlugin: Plugin = { name: 'requestId', install(app: App) { app.context.set('genId', () => crypto.randomUUID()); app.logger.debug('requestId plugin installed'); },};app.use(requestIdPlugin);Use a Plugin when there is no resource to open or close — just behavior or helpers to attach.
Writing a Web Kit Feature
Section titled “Writing a Web Kit Feature”Features are modules of the web Kernel. They declare dependencies and the Kernel resolves them with a topological sort.
import type { Feature, Kernel } from '@iskra-bun/web-kit';import type { Hono } from 'hono';
export class GreetingFeature implements Feature { name = 'greeting'; dependencies = ['error-handler'];
async initialize(kernel: Kernel) { // Set up shared state, read config from the kernel. }
routes(app: Hono) { app.get('/greet/:name', (c) => c.json({ hello: c.req.param('name') })); }
async shutdown() { // Optional cleanup. }}Add it to a WebPlugin:
import { WebPlugin } from '@iskra-bun/web-kit';
app.register(new WebPlugin({ port: 3000, features: [new GreetingFeature()] }));dependencies vs peerDependencies
Section titled “dependencies vs peerDependencies”dependencies— names of features that must be registered. They are initialized first, so their middleware runs before yours. A missing one is an error.peerDependencies— names of npm packages the feature needs. A missing one only logs a warning, and they do not change the initialization order.
Features with no dependency between them run their middleware in the order they were registered.
Rules the Kernel enforces
Section titled “Rules the Kernel enforces”- Middleware in
initialize(), routes inroutes(). Hono only runs the middleware registered before a route. The Kernel registers every feature’s middleware first and every feature’s routes after, so CSRF, rate limiting, auth and CORS apply to all routes. A feature that adds a route ininitialize()would escape the middleware of the features initialized after it, soinitialize()fails instead. - Unique names. Registering a feature whose name is already taken throws. It used to replace the first one silently: a helper named
csrfdropped the CSRF check.RateLimitFeaturetakes anamefor a second limiter. - Routes after
initialize(). With a standaloneKernel, callawait kernel.initialize()beforekernel.getApp().get(...): a route added earlier would skip the security headers and every feature’s middleware, soinitialize()throws. WithWebPlugin, pass your routes asrouter.
A Driver, Plugin or Feature runs with full access to the app: its config and secrets, app.context (where set() replaces an existing key such as db) and every event on the bus. Review one as you would any other dependency.
Packaging for reuse
Section titled “Packaging for reuse”Start from the plugin-starter template. Export your Driver/Plugin from the package entry point and declare @iskra-bun/core as a peer dependency so consumers control the version. Use the caret range of the core version you build against (the template itself uses workspace:*, which only resolves inside this monorepo):
{ "peerDependencies": { "@iskra-bun/core": "^0.1.1" }}See Architecture for how these pieces fit into the layered model.