Silgi
Advanced

Building Custom Plugins

Create reusable guards, wraps, and hooks.

Silgi 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 'silgi'

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  = s.$use(tenantGuard).$resolve(({  }) => {
  // 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 s.guard(async () => {
    const  = .headers[]

    if (! && . !== false) {
      throw new SilgiError('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 s.wrap(async (, ) => {
    const  = . ? .(, .__input) : .(.__input)

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

    const  = await ()

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

    return 
  })
}

Use it:

const  = createCacheWrap({ : 60_000 })

const  = s.$use().$resolve(({  }) => .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() call.

import {  } from 'silgi'

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

Apply it per-procedure:

const  = createMetricsPlugin(statsd)

const  = s.$use().$resolve(({  }) => .db.users.findMany())

Or apply it globally via instance-level hooks:

const  = silgi({
  : () => ({ : 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  = s.guard(() => {
    return {
      : [] as string[],
    }
  })

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

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

    return 
  })

  return { ,  }
}

Use both:

const { ,  } = createAuditPlugin({  })

const  = s.$use(auth, , ).$resolve(({ ,  }) => {
  .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 silgi 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
// silgi-plugin-tenant/index.ts
import type {  } from 'silgi'

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

  return {  }
}

Consumers install and use it:

import {  } from 'silgi-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