Wire the business logic
defineFeature is where entities meet HTTP: CRUD handlers, custom queries, screens,
and navigation. After this chapter the host can create events. The screens below
render from the schema with no custom dashboard React.



Host CRUD on event
Section titled “Host CRUD on event”Handlers live in handlers/event-handlers.ts and register on the feature:
r.entity("event", eventEntity);r.writeHandler(eventCreateHandler);r.writeHandler(eventUpdateHandler);r.writeHandler(eventDeleteHandler);r.queryHandler(eventListHandler);r.queryHandler(eventDetailHandler);src/features/show-pony/feature.ts.
Each handler is built with defineEntityCreateHandler / defineEntityListHandler
and friends, so no manual executor wiring is needed. eventCreateHandler wraps the stock
create handler with a tier cap (withStockCap) so free-tier tenants cannot
exceed their event limit.
Read handlers on rsvp
Section titled “Read handlers on rsvp”Hosts see the guest list; guests write through a custom handler (chapter 7):
r.entity("rsvp", rsvpEntity);r.queryHandler(defineEntityListHandler("rsvp", rsvpEntity, hostAccess));r.queryHandler(defineEntityDetailHandler("rsvp", rsvpEntity, hostAccess));Anonymous event lookup by slug
Section titled “Anonymous event lookup by slug”The public page needs the event without login:
export const eventBySlugQuery = defineQueryHandler({ name: "event:by-slug", schema: z.object({ slug: z.string().min(1).max(120) }), access: { roles: [...access.anonymous] }, handler: async (query, ctx) => { return (await findEvent(ctx, (row) => row.slug === query.payload.slug)) ?? null; },});src/features/show-pony/handlers/event-by-slug.query.ts.
findEvent runs against the subdomain tenant (chapter 4).
A slug on another host is invisible here.
Screens and nav
Section titled “Screens and nav”Dashboard screens and sidebar entries live in register/screens.ts and
register/nav.ts. These are data definitions, not React components:
export const eventListScreen: EntityListScreenDefinition = { id: "event-list", type: "entityList", entity: "event", columns: ["title", "slug", "startsAt", "location", "guestLimit"], pageSize: 25, defaultSort: { field: "title", dir: "asc" },};
registerShowPonyScreens(r);registerShowPonyNav(r);src/features/show-pony/register/screens.ts and register/nav.ts.
The events list is sorted server-side by defaultSort. There is no Meilisearch
adapter in the current sample. List filtering is through the schema-driven
list query, not a separate search backend.
Boot check
Section titled “Boot check”docker compose up -d # Postgres + Redisbun devLog in as [email protected] / changeme. On bun dev there are no
seeded events. Create one in Events → New event, or follow
chapter 11 for the production seed path.
Switch to Acme Studios in the top bar to confirm tenant isolation. No custom
dashboard React is needed, just schema + screens.
The complete file
Section titled “The complete file”// show-pony — event + RSVP feature (server registration only).
import { mailFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";import { defineEntityDetailHandler, defineEntityListHandler, defineFeature,} from "@cosmicdrift/kumiko-framework/engine";import { billingInfoQuery } from "./handlers/billing-info.query";import { eventBySlugQuery } from "./handlers/event-by-slug.query";import { eventCreateHandler, eventDeleteHandler, eventDetailHandler, eventListHandler, eventUpdateHandler,} from "./handlers/event-handlers";import { inviteBrandingQuery } from "./handlers/invite-branding.query";import { rsvpSubmitHandler } from "./handlers/rsvp-submit.write";import { usageQuery } from "./handlers/usage.query";import { showPonyTranslations } from "./i18n";import { INVITE_BRANDING_KEYS } from "./invite-branding";import { registerShowPonyNav } from "./register/nav";import { registerShowPonyScreens } from "./register/screens";import { eventEntity, rsvpEntity } from "./schema";
// RSVP rows carry guest PII (name/email/note) — unlike event CRUD, this is// restricted to Admin rather than openToAll: show-pony has no dedicated// organizer role yet, and Admin is the same privileged role billing-info// and usage already gate on.const rsvpReadAccess = { access: { roles: ["Admin"] } } as const;
export { eventEntity, rsvpEntity, rsvpTable } from "./schema";
export const showPonyFeature = defineFeature("showpony", (r) => { r.requires(mailFoundationFeature.name, "config", "managed-pages");
r.config({ keys: INVITE_BRANDING_KEYS });
r.translations({ keys: showPonyTranslations });
r.entity("event", eventEntity); r.writeHandler(eventCreateHandler); r.writeHandler(eventUpdateHandler); r.writeHandler(eventDeleteHandler); r.queryHandler(eventListHandler); r.queryHandler(eventDetailHandler);
r.queryHandler(eventBySlugQuery); r.queryHandler(inviteBrandingQuery);
r.entity("rsvp", rsvpEntity); r.writeHandler(rsvpSubmitHandler);
r.queryHandler(defineEntityListHandler("rsvp", rsvpEntity, rsvpReadAccess)); r.queryHandler(defineEntityDetailHandler("rsvp", rsvpEntity, rsvpReadAccess));
r.queryHandler(billingInfoQuery); r.queryHandler(usageQuery);
registerShowPonyScreens(r); registerShowPonyNav(r);});📄 On GitHub: src/features/show-pony/feature.ts