Skip to main content
Introducing packages.sweber.dev
Documentation menuOther frameworks

Other frameworks

Hono, Remix and React Router, Express and Cloudflare Workers.

The viewer needs one thing from your server: a GET endpoint that returns the tenant's events after your own access check. createAuditHandler speaks the Fetch API, so wherever you get a Request and return a Response, it is one line. For frameworks that do not, call audit.query yourself.

Hono

import { Hono } from "hono"
import { contextFromRequest, createAuditHandler } from "@sweberdev/logarithm"
import { audit } from "./audit"

const app = new Hono()

const handler = createAuditHandler({
  log: audit,
  async authorize(request) {
    const session = await sessionFrom(request)
    return session?.role === "admin" ? { tenantId: session.orgId } : null
  },
})

app.get("/api/audit", (c) => handler(c.req.raw))

Record events in your route handlers with a log scoped to the request:

app.post("/api/projects/:id/archive", async (c) => {
  const session = await requireSession(c.req.raw)
  const log = audit.with({
    tenantId: session.orgId,
    actor: { id: session.userId, name: session.name },
    context: contextFromRequest(c.req.raw),
  })
  await archiveProject(c.req.param("id"))
  await log.record({ action: "project.archived", targets: [{ type: "project", id: c.req.param("id") }] })
  return c.json({ ok: true })
})

Remix and React Router

A resource route returns the handler's response:

// app/routes/api.audit.ts
import type { LoaderFunctionArgs } from "react-router"
import { audit, auditHandler } from "~/audit.server"

export const loader = ({ request }: LoaderFunctionArgs) => auditHandler(request)

Record in an action, next to the change:

export async function action({ request, params }: ActionFunctionArgs) {
  const session = await requireAdmin(request)
  const log = audit.with({ tenantId: session.orgId, actor: { id: session.userId } })
  const before = await getProject(params.id)
  const after = await updateProject(params.id, await request.formData())
  await log.record({ action: "project.updated", targets: [{ type: "project", id: after.id }], before, after })
  return redirect("/settings")
}

Express and Fastify

These pass Node request objects, not Fetch requests. Build the query with parseQueryParams and ask the scoped log:

import express from "express"
import { AuditQueryError, parseQueryParams } from "@sweberdev/logarithm"

const app = express()

app.get("/api/audit", async (req, res) => {
  const session = await requireAdmin(req, res) // sends 403 itself and returns null
  if (!session) return
  try {
    const params = new URL(req.originalUrl, "http://localhost").searchParams
    const page = await audit.with({ tenantId: session.orgId }).query(parseQueryParams(params))
    res.set("cache-control", "no-store").json(page)
  } catch (error) {
    if (error instanceof AuditQueryError) return res.status(400).json({ error: error.message })
    throw error
  }
})

The scoped log (with({ tenantId })) adds the tenant to every query, so the URL cannot reach another customer. Fastify works the same with request.url and reply.send.

Cloudflare Workers and edge runtimes

createAuditHandler and the core package use only Web APIs. Use a store whose driver runs on the edge, for example postgresStore with @neondatabase/serverless or Cloudflare Hyperdrive, see Postgres. SQLite through node:sqlite or better-sqlite3 does not run on Workers.