Skip to content

Expose a public, anonymous-readable endpoint

A logged-in user has a tenant ID in their JWT; the framework reads it and ctx.db is automatically tenant-scoped. An anonymous caller has neither. To expose a public endpoint, you tell the framework two things: where to get the tenant ID from, and which handlers anonymous callers may reach.

  • You’ve read Multi-tenancy so the tenant-DB invariant is clear.
  • Your app uses subdomains, custom hosts, or a header to identify tenants in public contexts (e.g. acme.status.example.com).

Configure anonymousAccess on the app entrypoint with a tenant resolver:

src/app/server.ts
import { runProdApp } from "@cosmicdrift/kumiko-dev-server";
import { statusFeature } from "../features/status/feature";
await runProdApp({
features: [statusFeature],
anonymousAccess: {
tenantResolver: async ({ db, request }) => {
const host = request.headers.get("host") ?? "";
const subdomain = host.split(".")[0];
const tenant = await db.query("tenant", "by-slug", { slug: subdomain });
return tenant?.id ?? null;
},
},
});

In the feature, declare the public handler with roles: ["anonymous"]:

src/features/status/handlers/components.query.ts
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
import { z } from "zod";
export const listPublicComponents = defineQueryHandler({
name: "components-list",
schema: z.object({}),
access: { roles: ["anonymous"] },
handler: async (_query, ctx) => {
return ctx.db.list("component", { where: { showOnPage: true } });
},
});

ctx.db is still tenant-scoped — the resolver supplied the tenant ID, so reads filter automatically. The anonymous caller never sees data from another tenant.

Why roles: ["anonymous"] and not openToAll

Section titled “Why roles: ["anonymous"] and not openToAll”

{ openToAll: true } means any logged-in user can call this. It does not mean anonymous. The split is deliberate: a regression that turns auth off should not be one character away from a publicly-callable endpoint. A code review can grep for "anonymous" and see every public surface; openToAll is internal-only.

  • Field-level access still applies. A field with read: { roles: ["User"] } is invisible to anonymous callers. If a field should appear on the public page, include "anonymous" in the read roles (or omit the access block entirely).
  • The resolver runs on every request. Cache the slug→tenant lookup if your traffic is high — Kumiko does not cache it for you.
  • Anonymous can query, but not write. Public writes (e.g. “subscribe to incident updates”) need a handler that explicitly sets roles: ["anonymous"] plus its own validation; default-deny applies until you opt in.
  • Multi-tenancy — the tenant resolver is the public-page counterpart to JWT-driven tenancy.
  • Auth and permissions — why "anonymous" is a first-class role rather than an openToAll shorthand.