Katman
Advanced

Building Custom Plugins

Create reusable guards, wraps, and hooks.

Katman has no special plugin API. A "plugin" is a guard, a wrap, a set of lifecycle hooks, or a combination — packaged for reuse. This page shows how to build and share your own.

Custom guard — tenant isolation

A guard that reads a tenant ID from the request and adds it to the context. Every downstream procedure and guard can access ctx.tenant.

import { ,  } from "katman"

const  = ({
  : () => ({
    : .(.),
  }),
})

// The guard
export const  = .(async () => {
  const  = .["x-tenant-id"]
  if (!) {
    throw new ("BAD_REQUEST", { : "Missing X-Tenant-Id header" })
  }

  const  = await db.tenants.findById()
  if (!) {
    throw new ("NOT_FOUND", { : "Tenant not found" })
  }

  return {  } // ctx.tenant is now typed
})

Use it in any procedure:

const  = k.query({
  : [tenantGuard],
  : ({  }) => {
    // ctx.tenant is typed — TypeScript knows it exists
    return .db.users.findMany({
      : { : .tenant.id },
    })
  },
})

Making it configurable

Wrap the guard in a factory function to accept options:

export function (: {
  ?: string
  ?: boolean
}) {
  const  = . ?? "x-tenant-id"

  return k.guard(async () => {
    const  = .headers[]

    if (! && . !== false) {
      throw new KatmanError("BAD_REQUEST", {
        : `Missing ${} header`,
      })
    }

    if (!) return { : null }

    const  = await db.tenants.findById()
    return {  }
  })
}

Custom wrap — response caching

A wrap that caches procedure responses in memory. It intercepts the pipeline, checks the cache, and either returns the cached value or calls next().

export function (: {
  ?: number // milliseconds
  ?: (: any, : any) => string
} = {}) {
  const  = . ?? 60_000 // 1 minute default
  const  = new <string, { : unknown; : number }>()

  return k.wrap(async (, ) => {
    const  = .
      ? .(, .__input)
      : .(.__input)

    const  = .()
    if ( && . > .()) {
      return .
    }

    const  = await ()

    .(, {
      : ,
      : .() + ,
    })

    return 
  })
}

Use it:

const  = createCacheWrap({ : 60_000 })

const  = k.query({
  : [],
  : ({  }) => .db.products.findMany(),
})

This is an in-memory cache for demonstration. For production, use Redis or a dedicated caching layer. The pattern is the same — the wrap checks the cache before calling next().

Custom lifecycle hooks

For cross-cutting concerns that apply to every procedure, lifecycle hooks are cleaner than adding a wrap to every use array.

import {  } from "katman"

export function (: ) {
  return ({
    : ({  }) => {
      .increment("rpc.requests")
    },
    : ({  }) => {
      .histogram("rpc.duration", )
    },
    : ({  }) => {
      .increment("rpc.errors")
    },
  })
}

Apply it per-procedure:

const  = createMetricsPlugin(statsd)

const  = k.query({
  : [],
  : ({  }) => .db.users.findMany(),
})

Or apply it globally via instance-level hooks:

const  = katman({
  : () => ({ : getDB() }),
  : {
    : () => metrics.increment("rpc.requests"),
    : ({  }) => metrics.histogram("rpc.duration", ),
    : () => metrics.increment("rpc.errors"),
  },
})

Composing multiple pieces

A real plugin often combines a guard, a wrap, and hooks. Package them together:

export function (: { :  }) {
  const  = k.guard(() => {
    return {
      : [] as string[],
    }
  })

  const  = k.wrap(async (, ) => {
    const  = await ()

    if (.auditTrail.length > 0) {
      ..info({
        : .__procedurePath,
        : .user?.id,
        : .auditTrail,
      })
    }

    return 
  })

  return { ,  }
}

Use both:

const { ,  } = createAuditPlugin({  })

const  = k.mutation({
  : [auth, , ],
  : ({ ,  }) => {
    .auditTrail.push(`Deleted user ${.id}`)
    return .db.users.delete(.id)
  },
})

Packaging for distribution

If you want to publish your plugin as an npm package:

  1. Export factory functions, not instances — let consumers pass their own katman instance or options
  2. Use TypeScript generics so the guard/wrap types compose with any context
  3. Document what the plugin adds to ctx and what it expects
// katman-plugin-tenant/index.ts
import type {  } from "katman"

export function < extends >(: ) {
  const  = .guard(async () => {
    // ...
    return {  }
  })

  return {  }
}

Consumers install and use it:

import {  } from "katman-plugin-tenant"

const {  } = (k)

What's next?

  • Middleware — guard and wrap fundamentals
  • Plugins — built-in plugins for reference
  • Server — lifecycle hooks reference

On this page