Skip to content

Host Routing

Tenant per subdomain with one options object for the API and the HTML pages.

  • createHostRoutingFeature(options): the Host header is the only source of the anonymous tenant; a forged X-Tenant header answers 400 tenant_mismatch.
  • createHostDispatch(options): HTML file and CSP per host, a 404 page for unknown shops, and /de/... redirecting to the unprefixed path.
  • The same hostDispatch goes into runProdApp({ hostDispatch }); the dev server takes it through toDevHostDispatch from @cosmicdrift/kumiko-dev-server.

Feature entry point: src/feature.ts.

Needs a running Postgres and TEST_DATABASE_URL set (e.g. postgres://postgres:postgres@127.0.0.1:5432/postgres, see demo/.env.example).

Terminal window
cd samples/recipes/host-routing
bun test

The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):

// Host routing: one options object drives both halves. The feature makes the
// Host header the only source of the anonymous tenant; the dispatch picks the
// HTML file per host for runProdApp's `hostDispatch` (and, through
// toDevHostDispatch, for the dev server).
import {
createHostDispatch,
createHostRoutingFeature,
type HostRoutingOptions,
} from "@cosmicdrift/kumiko-bundled-features/host-routing";
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
import { z } from "zod";
export const routingOptions: HostRoutingOptions = {
tenantBy: "host",
baseDomain: "shop.test",
apex: { file: "index.html", localeFiles: { en: "en/index.html" } },
tenant: { file: "shop.html", csp: "default-src 'self'; frame-ancestors 'none'" },
unknownTenant: { file: "unknown-shop.html" },
locales: { default: "de", prefixed: ["en"] },
};
export const hostRoutingFeature = createHostRoutingFeature(routingOptions);
export const hostDispatch = createHostDispatch(routingOptions);
// A public query that reports which tenant the request landed on.
export const storefrontFeature = defineFeature("storefront", (r) => {
r.queryHandler({
name: "whoami",
schema: z.object({}),
access: { roles: ["anonymous"] },
rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
handler: async (query) => ({ tenantId: query.user.tenantId }),
});
});

📄 On GitHub: samples/recipes/host-routing/src/feature.ts