Docs

Adapter

Learn how adapters let you plug in your own auth, logging, cookies, geo, and captcha, then swap them later.

Some of what Kizlo needs has no single right answer: which provider signs in your users, where logs go, how cookies are read, what separates a real visitor from a bot. Kizlo doesn't pick for you.

For each capability it defines a small contract, a handful of methods it promises to call, and lets you supply the implementation. That implementation is an adapter.

src/lib/kizlo/server/index.ts
export const { client, handler } = createKizlo({
  adapters: {
    auth: myAuth(),
    captcha: turnstile({ secret: process.env.TURNSTILE_SECRET }),
    logger: consoleLog(),
  },
})

You name the implementation; Kizlo calls it when it needs that capability and stays out of the rest. It never knows whether auth is Clerk, better-auth, or your own session store, only that it can ask for the current user.

Why it's not locked in

Because each adapter is just a contract, the service behind it is yours to swap. Move from Clerk to better-auth, Vercel to Cloudflare, console to Datadog, and you change one adapter. Kizlo and the rest of your app never notice, because they only ever spoke to the contract. Your dependency is on a capability, never on a specific service.

The interfaces

Kizlo asks for a fixed set of capabilities, each with its own contract, and each optional — provide an adapter when your app needs that capability, leave it out when it doesn't. Each section below is one contract in full. (Promisify<T> in the signatures is T | Promise<T> — return sync or async, whichever suits.)

auth — who's making the request

interface AuthUser {
  id: number
  email?: string
  phone?: string
  firstName: string
  lastName: string
}

interface AuthAdapter {
  getUser(request: Request | null): Promisify<AuthUser | null>
}

Kizlo hands you the incoming Request; you read your session however you issue it (cookie, header, bearer token) and return the user, or null when no one's signed in. This is the seam between Kizlo and whatever owns your sessions. Wrap your provider in createAuthAdapter to fill in the contract:

import { createAuthAdapter } from "kizlo"

export function myAuth() {
  return createAuthAdapter({
    async getUser(request) {
      const session = await mySessionStore.read(request)
      if (!session) return null
      return {
        id: session.userId,
        firstName: session.firstName,
        lastName: session.lastName,
        email: session.email,
      }
    },
  })
}

WordPress keeps its own auth — and should. Admins, editors, and other privileged roles sign in through it, exactly what it's built for. What it isn't built for is modern end-user sign-in: social logins, passkeys, the providers a customer-facing app expects. So your users come through an auth adapter instead — one of the reasons Kizlo keeps WordPress auth off the frontend.

cookies — read & write cookies

interface CookiesAdapter {
  getAll(): Promisify<{ name: string; value: string }[] | null>
  setAll(cookies: CookieWithOptions[]): Promisify<void>
  deleteAll(cookies: { name: string; options?: CookieOptions }[]): Promisify<void>
}

Three operations over whatever cookie store your framework gives you — Kizlo uses them to set, read, and clear sessions. On Next.js this maps straight onto next/headers; on another framework you wrap its equivalent. The framework integration usually wires this one for you, so you rarely write it by hand.

geo — describe the connection

interface ConnInfo {
  ip: string | null
  country: string | null
  city: string | null
  state: string | null
  postcode: string | null
  timezone: string | null
  userAgent: string | null
}

interface GeoAdapter {
  getConnInfo(request: Request | null): Promisify<ConnInfo>
}

Given the request, return what you can about where it came from. Fields you can't resolve are null — return the shape, not an error. The prebuilt geoVercelNext reads Vercel's edge headers (x-vercel-ip-country and friends); behind another CDN, you read its headers instead.

captcha — verify a challenge token

type CaptchaAdapter = (input: { token: string; ip: string }) => Promise<boolean>

The simplest of the five: just a function. You get the token your frontend collected and the caller's ip, verify it with your provider, and return true for human. turnstile, recaptcha, hcaptcha, altcha and the rest are all this one function pointed at different verification APIs:

import type { CaptchaAdapter } from "kizlo"

export function myCaptcha(opts: { secret: string }): CaptchaAdapter {
  return async ({ token, ip }) => {
    const data = await verifyWithMyProvider(opts.secret, token, ip)
    return data.success === true
  }
}

logger — receive log records

type LogLevel = "debug" | "info" | "warn" | "error"

interface LogPayload {
  level: LogLevel
  message: string
  timestamp: Date
  context?: Record<string, unknown>
  error?: Error
}

type LoggerAdapter = (payload: LogPayload) => void | Promise<void>

Kizlo calls this with every log record it produces; you decide where it goes — stdout, Datadog, Sentry. The built-in consoleLog prints; a one-liner can forward each payload to your platform's SDK instead.

Built-ins, defaults, and mocks

You rarely write an adapter from scratch. Kizlo ships implementations for the common cases: turnstile, recaptcha, and hcaptcha for captcha; consoleLog for logging; geoVercelNext for reading geo on Vercel. The framework integration wires sensible defaults, like your framework's cookie store, so the everyday setup needs little or no adapters block. Reach for the option to override a default or plug in something Kizlo doesn't ship (auth is always yours to write).

Because an adapter is just a contract, it's also the natural place to substitute behaviour in tests. Each capability has a mock, such as authMock and geoMock, that returns fixed values, so a test can stand in a known user or location without touching a real provider:

kizlo.test.ts
createKizlo({
  adapters: {
    auth: authMock({ mockUserId: 1 }), // every request is user 1
    geo: geoMock({ country: "IN" }),   // pinned location
  },
})

Adapters cover the services around a request — identity, cookies, geo, captcha, logs. Bridging WordPress itself, or a third-party plugin, is a different job done by extensions and companion plugins, not adapters.

On this page