Lumen Docs

Middleware

Hono middleware for auth + access control. Located at apps/api/src/middleware/access.ts.

Available middleware

requireAuth

Parses the Authorization: Bearer <token> header, verifies the JWT, loads the user from DB, and sets c.set("user", authedUser) and c.set("userId", userId). 401 on any failure.

providersRoutes.use("*", requireAuth);

AuthedUser shape:

interface AuthedUser {
  id: string;
  email: string;
  platformRole: PlatformRole;
  orgPosition: OrgPosition;
  departmentId: string | null;
}

requirePlatformRole(allowed)

Gates endpoints by platform role. Superadmin is in allowed by default (priority 1 in the resolver). Use for admin/engineer endpoints.

providersRoutes.post(
  "/",
  requirePlatformRole(["engineer", "superadmin"]),
  zValidator("json", createSchema),
  handler
);

userRoutes.post(
  "/",
  requirePlatformRole(["admin", "superadmin"]),
  zValidator("json", createUserSchema),
  handler
);

Returns 403 with {error: "insufficient_platform_role"} if mismatched.

requireCeoOrManager(paramKey = "deptId")

For endpoints where the CEO or the department's manager can act. Reads the department ID from the route param (default deptId). Looks up the manager relationship. Validates against the current user.

departmentRoutes.post(
  "/:deptId/members",
  requireAuth,
  requireCeoOrManager(),
  handler
);

requireProjectAccess(paramKey, minTier)

The workhorse for project-scoped endpoints. Calls resolveAccess(userId, projectId) and enforces a minimum tier.

projectRoutes.patch(
  "/:id",
  requireAuth,
  requireProjectAccess("id", "edit"),   // edit = can modify settings
  zValidator("json", updateSchema),
  handler
);

grantRoutes.post(
  "/projects/:projectId/grants",
  requireAuth,
  requireProjectAccess("projectId", "full"),  // full = can grant to others
  handler
);

Sets c.set("accessResolution", { tier, source }) so the handler can inspect how the user got access.

requireBootstrapped

Returns 503 with {error: "not_bootstrapped"} if bootstrap_state.is_initialized = false. Use on every user-facing endpoint except /auth/* and /bootstrap/*.

requireNotBootstrapped

Inverse — 409 if already bootstrapped. Only POST /bootstrap/init uses it.

Middleware ordering

Order matters — middleware is applied in the sequence you chain it:

// WRONG — validator runs first, even without auth
routes.post("/", zValidator("json", schema), requireAuth, handler);

// RIGHT — auth first, then validator, then handler
routes.post("/", requireAuth, zValidator("json", schema), handler);

Rule of thumb:

  1. requireAuth (or skip for public)
  2. requireBootstrapped (or requireNotBootstrapped)
  3. requirePlatformRole(...) or requireProjectAccess(...) or requireCeoOrManager(...)
  4. zValidator(...) for body validation
  5. Handler

Adding a new middleware

Example: rate-limiting per user.

// apps/api/src/middleware/rate-limit.ts
import { createMiddleware } from "hono/factory";
import type { AccessEnv } from "./access";

const hits = new Map<string, number[]>();

export function rateLimit(maxPerMinute: number) {
  return createMiddleware<AccessEnv>(async (c, next) => {
    const user = c.get("user");
    if (!user) return next(); // unauth requests hit elsewhere

    const now = Date.now();
    const cutoff = now - 60_000;
    const userHits = (hits.get(user.id) || []).filter(t => t > cutoff);

    if (userHits.length >= maxPerMinute) {
      return c.json({ error: "rate_limited" }, 429);
    }

    userHits.push(now);
    hits.set(user.id, userHits);
    await next();
  });
}

Usage:

chatRoutes.post(
  "/projects/:projectId",
  requireAuth,
  requireProjectAccess("projectId", "use"),
  rateLimit(20),  // 20 chats/minute per user
  handler
);

Testing middleware

Integration-style — instantiate Hono, wire middleware, call with a fake JWT:

import { test, expect } from "bun:test";
import { Hono } from "hono";
import { requireAuth, requirePlatformRole } from "../middleware/access";

test("requirePlatformRole rejects wrong role", async () => {
  const app = new Hono()
    .use("*", requireAuth)
    .use("*", requirePlatformRole(["admin"]))
    .get("/", c => c.text("ok"));

  const token = signTestJWT({ sub: "u1", platformRole: "none" });
  const res = await app.request("/", {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(res.status).toBe(403);
});

Full suite in apps/api/src/middleware/access.test.ts.