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.
Prerequisites
Section titled “Prerequisites”- 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).
The code
Section titled “The code”Configure anonymousAccess on the app entrypoint with a tenant
resolver:
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"]:
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.
Common gotchas
Section titled “Common gotchas”- 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 notwrite. Public writes (e.g. “subscribe to incident updates”) need a handler that explicitly setsroles: ["anonymous"]plus its own validation; default-deny applies until you opt in.
See also
Section titled “See also”- 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 anopenToAllshorthand.