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 withreason
. - 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; useextend
. - 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