UsefulKey
Plugins

Authoring Plugins

How to write a UsefulKey plugin — hooks, setup, and extending the instance with typed helpers.

Plugins let you add policy and behavior to UsefulKey without forking the core. A plugin is a factory function that receives the running UsefulKey instance and returns lifecycle hooks. Plugins can also add typed helpers to the instance via extend.

Plugin shape

import type { UsefulKeyPlugin } from "betterkey";

export function myPlugin(): UsefulKeyPlugin<{ __hasMyPlugin: true }> {
  return {
    name: "my-plugin",
    async setup(ctx) {
      // optional: run once at startup (e.g., warm caches, verify config)
    },

    // Called before any verification work. Return { reject } to block.
    async beforeVerify(ctx, { key, ip, identifier, namespace, rateLimit }) {
      if (key.endsWith("bad")) return { reject: true, reason: "bad_suffix" };
    },

    // Called after the key record is loaded but before core checks.
    async onKeyRecordLoaded(ctx, { input, record }) {
      // inspect record or input; return { reject } to block
    },

    // Called after a successful verification.
    async onVerifySuccess(ctx, { input, record }) {
      // emit custom analytics, side effects, etc.
    },

    // Called before a key is created. Return { reject } to block creation.
    async beforeCreateKey(ctx, { input }) {
      if (input.usesRemaining === 0) return { reject: true, reason: "invalid_uses" };
    },

    // Called after a key is created.
    async onKeyCreated(ctx, { record }) {
      // e.g., notify another system
    },

    // Add custom, typed helpers on the instance
    extend: {
      __hasMyPlugin: true,
    },
  };
}

Using a plugin

import { usefulkey } from "betterkey";
import { myPlugin } from "./my-plugin";

const uk = usefulkey({}, { plugins: [myPlugin()] as const });
await uk.ready; // optional: wait for all plugin setups

// uk now has any helpers provided via `extend`
uk.__hasMyPlugin; // typed: true

Lifecycle and blocking semantics

  • beforeVerify: Runs first during verifyKey. Return { reject: true, reason?: string } to immediately fail verification with reason.
  • onKeyRecordLoaded: Runs after the key is fetched but before expiry/usage checks. Can also block.
  • onVerifySuccess: Runs only on successful verification.
  • beforeCreateKey: Runs before creation; return { reject } to block creation.
  • onKeyCreated: Runs after a key is created.
  • setup: Runs once after instance construction. Use this for initialization rather than work in hot paths.

Throwing in a hook is caught and logged; core continues to the next hook. If you need to stop the flow, return { reject: true }.

Extending the instance surface

Plugins can add custom utilities that become available on the UsefulKey instance. Provide them via extend. The usefulkey factory merges the extension object and preserves types when you pass the plugins with as const.

export function enableFlagPlugin() {
  return {
    name: "enable-flag",
    extend: {
      // e.g., a typed helper the rest of your app can call
      setFeatureFlag(id: string, flag: string, value: boolean) {
        // implementation detail up to the plugin
      },
    },
  } satisfies ReturnType<UsefulKeyPlugin>;
}

const uk = usefulkey({}, { plugins: [enableFlagPlugin()] as const });
uk.setFeatureFlag("key_123", "beta", true);

Design guidelines

  • Keep hooks fast; prefer setup for I/O-heavy initialization.
  • Return { reject } to block instead of throwing.
  • Avoid mutating the UsefulKey instance directly; use extend.
  • Namespacing: pick a unique name for logging and debugging.
  • If you maintain external state, consider idempotency and retries.

Full type references

Key types are available from usefulkey:

import type {
  UsefulKeyPlugin,
  UsefulKeyPluginHooks,
} from "betterkey";

See the source of existing plugins for patterns:

  • src/plugins/enable-disable/index.ts
  • src/plugins/permissions-scopes/index.ts
  • src/plugins/usage-limits-per-key/index.ts