inkan

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 ./pkg

create 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
.gitignore

The 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

namefor
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-plugin is 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 second problem and 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.0

Ranges 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:crypto alone. Every dependency you add is one every app installs.

  • Typed through the app. Make it a shared plugin that returns its scope, so ctx.user or ctx.tenant is typed after register with 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 /docs shows 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.

On this page