Publish a plugin
Naming, peer dependencies, what makes a good plugin, and how to get it listed on Hanko.
A plugin is worth more once other people can find it. This page is about the step after Writing a plugin: putting it on npm so it works with every inkan app, and listing it on Hanko, next to the official ones.
New in 0.7.0
npx @vxnsin/inkan create plugin and the inkan option of plugin() are new in inkan 0.7.0.
Start from the template
npx @vxnsin/inkan create plugin inkan-tenant # into ./inkan-tenant
npx @vxnsin/inkan create plugin @you/inkan-tenant pkg # a scoped name, into ./pkgcreate plugin <name> [folder] writes a package for a plugin of your own:
index.js the plugin, plain JavaScript: no build step, what you test is what npm installs
index.d.ts its types, so ctx.tenant is typed after register
test.ts node --test on app.inject: no port, no network
package.json the name, the inkan-plugin keyword, inkan as a peer, up to the next major
README.md install, the smallest example, the options
tsconfig.json for npm run typecheck
LICENSE MIT
.github/workflows/ci.yml typecheck and tests on Node 22 and 24
.gitignoreThe example plugin asks every request for a header, answers 400 without it, puts it on
ctx.tenant and writes the header into OpenAPI with onOperation(): replace it, keep the
shape. The function is named after the package (inkan-tenant → tenant). It refuses a
name npm would not take, the @inkanjs scope, and a folder that is already there.
Everything below is what the template does for you, and why.
Naming
| name | for |
|---|---|
inkan-<name> | community plugins: inkan-tenant, inkan-prometheus |
@you/inkan-<name> | the same under your own scope, if you keep one |
@inkanjs/* | reserved for the official packages |
Name it after what it does, not after inkan: inkan-tenant, not inkan-plugin-tenant.
package.json
{
"name": "inkan-tenant",
"keywords": ["inkan-plugin"],
"type": "module",
"peerDependencies": {
"@vxnsin/inkan": ">=0.8.0 <1.0.0"
}
}- The keyword
inkan-pluginis how a plugin is found on npm, Hanko or not. - inkan is a peer dependency, never a dependency. The app brings its own inkan; a second
copy in your package would give it a second
t, a secondproblemand types that do not line up. From 0.8.0 the stable API does not break within a major (see Stability), so the template declares>=0.8.0 <1.0.0: from the minor you build on up to the next major. Lean on experimental parts and you may want a narrower range.
Say which inkan it works with
npm only warns about a peer dependency that does not fit. The plugin can say it too, as a range in its options, and inkan checks it when the plugin is registered:
import { } from "@vxnsin/inkan";
export const = (: { : string }) =>
(() => .("tenant", () => .[.]), {
: true,
: "inkan-tenant",
: ">=0.8.0 <1.0.0",
});On any other version register() throws, so a plugin built against one inkan does not
half-work on another:
inkan-tenant needs inkan >=0.8.0 <1.0.0, this app runs 1.0.0Ranges read as npm reads them: ^, ~, 0.7.x, <, >=, hyphens, spaces and ||. A
prerelease of inkan is in a range only when the range names a prerelease. A range that is
not one throws where the plugin is made, not later. Keep it the same as the peer
dependency's. PluginOptions is exported for the options' type.
What makes a good one
-
Zero or few dependencies. inkan has none, and the official middleware runs on
node:cryptoalone. Every dependency you add is one every app installs. -
Typed through the app. Make it a shared plugin that returns its scope, so
ctx.userorctx.tenantis typed afterregisterwith no generics to write. See typed decorations. -
Honest in the OpenAPI document. A plugin that adds a header, an answer or a security scheme says so with
onOperation(). Then/docsshows the 401 your plugin can send. -
Tested with
app.inject. No port, no network: build a small app, register the plugin, inject requests.import { } from "node:test"; import from "node:assert/strict"; import { } from "@vxnsin/inkan"; import { } from "./index.js"; ("reads the tenant from the header", async () => { const = ().(({ : "x-tenant" })).("/", () => ({ : .tenant })); const = await .({ : "GET", : "/", : { "x-tenant": "tea" } }); .(., { : "tea" }); }); -
A README with three parts: how to install it, the smallest example that does something, and every option with its default. That README is what Hanko links to.
Getting listed on Hanko
The keyword is enough. Hanko asks npm once a day for every package with the keyword
inkan-plugin and lists it under community, with its version, its last publish and last
week's downloads. No form, no pull request.
A pull request to inkanjs/inkan.dev is only for
more than npm knows: a better line than the package description, tags to be found by, or a
link to docs of your own. Add an entry to the community part of data/hanko.ts; it takes
the place of what npm says:
{
name: "inkan-tenant",
description: "One line: what it does, not how good it is.",
kind: "community",
href: "https://github.com/you/inkan-tenant#readme", // the README, docs, or the npm page
npm: "inkan-tenant",
repo: "https://github.com/you/inkan-tenant",
tags: ["multi-tenant", "headers"],
}To be honest about what a listing means: a package with the keyword is listed as npm has it,
unreviewed. An entry in data/hanko.ts is looked over lightly before it goes in (it is
published, it does what the line says), and that is all. Neither is maintained, audited or
supported by inkan. Issues and releases stay with their authors, and a package that misuses
the keyword, or no longer works with a current inkan, may be left off the list.