Dashboard from schema
A host needs to create events and see who’s coming. In Kumiko that UI is derived from the entity schema — screen definitions from chapter 6, rendered by the admin shell.
Screens are data, not code
Section titled “Screens are data, not code”An entityList or entityEdit screen is a plain object: which entity, which
columns, which form layout. No component, no state, no fetch — the framework
supplies all of it:
export const eventEditScreen: EntityEditScreenDefinition = { id: "event-edit", type: "entityEdit", entity: "event", layout: { sections: [ { title: "showpony:section.event-basics", columns: 2, fields: [{ field: "title", span: 2 }, "slug", "startsAt", "location", "guestLimit"] }, { title: "showpony:section.event-details", columns: 1, fields: ["description"] }, ], },};src/features/show-pony/feature.ts
The guest list is the same mechanism on rsvp:
Two accounts, two stories
Section titled “Two accounts, two stories”Show Pony seeds two logins on the apex — on purpose they are not the same kind of user:
| Account | Who | Tenant membership | Workspace |
|---|---|---|---|
[email protected] | Mira (demo host) | demo — Admin + TenantAdmin | Events only |
[email protected] | Platform operator | _platform — User (login anchor only) | Platform only |
Why not put sysadmin on the demo tenant?
Section titled “Why not put sysadmin on the demo tenant?”Early demos gave the operator a membership on demo so login had a tenant context.
That leaked into the product UX:
- Sysadmin appeared on Mira’s Team screen as a tenant admin.
- The Events workspace showed up (because
access.adminincludesSystemAdmin). - Operators thought they were “also a host” instead of running the platform.
Fix (publicstatus pattern):
- Internal tenant
_platform— membership anchor for login, not a customer subdomain. - Global role
SystemAdmin— tenants, users, audit, jobs. - Host workspace uses tenant roles only (
TenantAdmin,Admin) — not the globalaccess.adminpreset that includesSystemAdmin.
const HOST_WORKSPACE_ROLES = access.roles("TenantAdmin", "Admin");
r.workspace({ id: "host", access: { roles: HOST_WORKSPACE_ROLES }, // ...});r.workspace({ id: "platform", access: { roles: access.systemAdmin }, // ...});src/features/app-shell/feature.ts
Sysadmin seed (dev + prod):
await seedAdmin(db, { globalRoles: ["SystemAdmin"], memberships: [{ tenantId: PLATFORM_TENANT.id, tenantKey: "_platform", roles: ["User"], }],});bin/server.ts · bin/demo-tenants.ts
Fresh installs get this from boot seed (bin/server.ts / bin/main.ts +
bin/demo-tenants.ts).
No extra migration files needed for new clones.
Log in as sysadmin → you land in Platform (tenants, users, audit). Log in as Mira → Events only. Same apex URL, different story.
Guest PII on the list is covered in chapter 14.
Text, separate from the schema
Section titled “Text, separate from the schema”Labels come from chapter 2 (src/features/show-pony/i18n/keys.ts). Here
you only define which columns and sections appear — the framework resolves
the strings.
Browser entry
Section titled “Browser entry”createKumikoApp with the shell and client features — three lines:
createKumikoApp({ shell: AppShell, clientFeatures: [emailPasswordClient(), showPonyClient],});src/client-admin.tsx
emailPasswordClient() adds the login gate and session on top of the admin bundle.
The complete files
Section titled “The complete files”Workspace split (host vs platform):
// show-pony workspaces — host tenant surface vs platform operator surface.
import { access, defineFeature, type FeatureDefinition,} from "@cosmicdrift/kumiko-framework/engine";
const HOST_NAV_MEMBERS = [ "admin-shell:nav:tenant-overview", "showpony:nav:events", "showpony:nav:event-new", "showpony:nav:guests", "showpony:nav:appearance", "showpony:nav:invite-branding", "showpony:nav:account", "showpony:nav:billing", "tenant:nav:members",] as const;
const PLATFORM_NAV_MEMBERS = [ "admin-shell:nav:platform-overview", "admin-shell:nav:tenants", "admin-shell:nav:tier-admin", "config:nav:audience-system", "app-shell:nav:users", "audit:nav:audit-log", "jobs:nav:job-runs",] as const;
/** Tenant-scoped host tools — excludes global SystemAdmin (platform workspace). */const HOST_WORKSPACE_ROLES = access.roles("TenantAdmin", "Admin");
export const appShellFeature: FeatureDefinition = defineFeature("app-shell", (r) => { r.systemScope();
r.nav({ id: "users", label: "app-shell:nav.users", screen: "user:screen:user-list", order: 20, icon: "users", access: { roles: access.systemAdmin }, });
r.translations({ keys: { "app-shell:workspace.host": { de: "Events", en: "Events" }, "app-shell:workspace.platform": { de: "Plattform", en: "Platform" }, "app-shell:nav.users": { de: "Nutzer", en: "Users" }, // tenant feature label is tenant:nav.members; tenantClient bundles tenant.nav.members. "tenant:nav.members": { de: "Team", en: "Team" }, }, });
r.workspace({ id: "host", label: "app-shell:workspace.host", icon: "calendar", order: 1, access: { roles: HOST_WORKSPACE_ROLES }, nav: [...HOST_NAV_MEMBERS], default: true, });
r.workspace({ id: "platform", label: "app-shell:workspace.platform", icon: "shield", order: 2, access: { roles: access.systemAdmin }, nav: [...PLATFORM_NAV_MEMBERS], });});Sysadmin + demo tenant constants:
// Demo tenants for show-pony — shared by bin/server.ts and bin/main.ts.//// demo: Mira's host tenant (events, guest list, public subdomain).// acme: second demo tenant for isolation proof (separate subdomain).// _platform: internal anchor for sysadmin login only — not a customer subdomain.
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
export type DemoTenant = { readonly id: TenantId; readonly tenantKey: string; readonly name: string;};
export const DEMO_TENANT: DemoTenant = { id: "00000000-0000-4000-8000-0000000000a1" as TenantId, tenantKey: "demo", name: "Demo Host",};
export const ACME_TENANT: DemoTenant = { id: "00000000-0000-4000-8000-0000000000a2" as TenantId, tenantKey: "acme", name: "Acme Studios",};
/** Sysadmin membership anchor — same pattern as publicstatus PLATFORM_TENANT. */export const PLATFORM_TENANT: DemoTenant = { id: "00000000-0000-4000-8000-000000000099" as TenantId, tenantKey: "_platform", name: "Platform (sysadmin only)",};i18n keys:
// show-pony i18n keys. Convention: feature prefix `showpony:`// + `entity:<e>:field:<name>` for field labels (columns + form),// `screen:<id>.title` for page titles, `showpony:nav.<id>` for// sidebar labels, `:option:<value>` for select values.
type LocaleEntry = { de: string; en: string };
export const showPonyTranslations = { "showpony:nav.events": { de: "Events", en: "Events" }, "showpony:nav.event-new": { de: "Neues Event", en: "New event" }, "showpony:nav.guests": { de: "Gästeliste", en: "Guest list" }, "showpony:nav.appearance": { de: "Erscheinungsbild", en: "Appearance" }, "showpony:nav.invite-branding": { de: "Invite-Branding", en: "Invite branding" }, "showpony:nav.account": { de: "Konto", en: "Account" }, "showpony:nav.billing": { de: "Tarif & Abrechnung", en: "Plan & billing" },
"screen:event-list.title": { de: "Events", en: "Events" }, "screen:event-edit.title": { de: "Event bearbeiten", en: "Edit event" }, "screen:rsvp-list.title": { de: "Gästeliste", en: "Guest list" }, "screen:invite-branding-settings.title": { de: "Invite-Branding", en: "Invite branding", }, "screen:billing.title": { de: "Tarif & Abrechnung", en: "Plan & billing" },
"showpony:branding.section.identity": { de: "Marke", en: "Brand" }, "showpony:branding.section.hero": { de: "Hero-Bild", en: "Hero image" },
"showpony:entity:__config-edit__:field:title": { de: "Markenname", en: "Brand name" }, "showpony:entity:__config-edit__:field:description": { de: "Tagline / Beschreibung", en: "Tagline / description", }, "showpony:entity:__config-edit__:field:accentColor": { de: "Akzentfarbe", en: "Accent color", }, "showpony:entity:__config-edit__:field:logoUrl": { de: "Logo-URL", en: "Logo URL" }, "showpony:entity:__config-edit__:field:heroImageUrl": { de: "Hero-Bild-URL", en: "Hero image URL", }, "showpony:entity:__config-edit__:field:heroStyle": { de: "Hero-Layout", en: "Hero layout" }, "showpony:entity:__config-edit__:field:heroStyle:option:immersive": { de: "Immersiv (Vollbild)", en: "Immersive (full bleed)", }, "showpony:entity:__config-edit__:field:heroStyle:option:split": { de: "Geteilt (Text + Bild)", en: "Split (text + image)", },
"showpony:section.event-basics": { de: "Eckdaten", en: "Basics" }, "showpony:section.event-details": { de: "Beschreibung", en: "Description" },
"showpony:entity:event:field:title": { de: "Titel", en: "Title" }, "showpony:entity:event:field:slug": { de: "Link-Kürzel", en: "Link slug" }, "showpony:entity:event:field:startsAt": { de: "Beginn", en: "Starts at" }, "showpony:entity:event:field:location": { de: "Ort", en: "Location" }, "showpony:entity:event:field:description": { de: "Beschreibung", en: "Description" }, "showpony:entity:event:field:guestLimit": { de: "Gäste-Limit", en: "Guest limit" },
"showpony:entity:rsvp:field:name": { de: "Name", en: "Name" }, "showpony:entity:rsvp:field:email": { de: "E-Mail", en: "Email" }, "showpony:entity:rsvp:field:status": { de: "Status", en: "Status" }, "showpony:entity:rsvp:field:plusN": { de: "Begleitung", en: "Plus guests" }, "showpony:entity:rsvp:field:eventId": { de: "Event", en: "Event" }, "showpony:entity:rsvp:field:note": { de: "Notiz", en: "Note" },
"showpony:entity:rsvp:field:status:option:yes": { de: "Zusage", en: "Going" }, "showpony:entity:rsvp:field:status:option:no": { de: "Absage", en: "Not going" }, "showpony:entity:rsvp:field:status:option:maybe": { de: "Vielleicht", en: "Maybe" },
"showpony:public.event.missing": { de: "Dieses Event gibt es nicht.", en: "This event doesn't exist.", }, "showpony:public.event.invited": { de: "Du bist eingeladen", en: "You're invited" }, "showpony:public.event.guest-limit": { de: "Bis zu {limit} Gäste", en: "Up to {limit} guests", }, "showpony:public.event.add-to-calendar": { de: "📅 Zum Kalender hinzufügen", en: "📅 Add to calendar", }, "showpony:public.rsvp.thanks": { de: "Danke, {name}!", en: "Thanks, {name}!" }, "showpony:public.rsvp.heading": { de: "RSVP", en: "RSVP" }, "showpony:public.rsvp.subheading": { de: "Kein Account nötig — Name reicht.", en: "No account needed — just your name.", }, "showpony:public.rsvp.on-list": { de: "Du stehst auf der Liste.", en: "You're on the list." }, "showpony:public.rsvp.plus-guests": { de: "Begleitung?", en: "Bringing anyone?" }, "showpony:public.rsvp.name": { de: "Name", en: "Name" }, "showpony:public.rsvp.name-placeholder": { de: "Dein Name", en: "Your name" }, "showpony:public.rsvp.email": { de: "E-Mail (optional)", en: "Email (optional)" }, "showpony:public.rsvp.email-placeholder": { de: "E-Mail (optional, für die Bestätigung)", en: "Email (optional, for your confirmation)", }, "showpony:public.rsvp.status.yes": { de: "Ich komme", en: "I'm in" }, "showpony:public.rsvp.status.maybe": { de: "Vielleicht", en: "Maybe" }, "showpony:public.rsvp.status.no": { de: "Kann leider nicht", en: "Can't make it" }, "showpony:public.rsvp.submit": { de: "RSVP senden", en: "Send RSVP" }, "showpony:public.rsvp.error": { de: "Etwas ist schiefgelaufen ({reason}). Bitte nochmal versuchen.", en: "Something went wrong ({reason}). Try again.", },
"showpony:demo.login.title": { de: "Live-Demo", en: "Live demo" }, "showpony:demo.login.body": { de: "Melde dich an, um Dashboard und Plattform-Workspace zu erkunden:", en: "Sign in to explore the host dashboard and platform workspace:", }, "showpony:demo.login.read-only": { de: "Nur ansehen — Änderungen werden auf dieser Instanz nicht gespeichert.", en: "Browse only — changes are not saved on this instance.", }, "showpony:demo.banner": { de: "Live-Demo (read-only). Zum Bauen lokal klonen —", en: "Live demo (read-only). Clone locally to build your own —", }, "showpony:demo.banner.link": { de: "Tutorial", en: "tutorial" }, "showpony:demo.public.title": { de: "Live-Demo", en: "Live demo" }, "showpony:demo.public.body": { de: "RSVPs funktionieren nur in der lokalen Installation. Hier kannst du die Invite-Seite ansehen.", en: "RSVPs work in the local install only. Here you can browse the invite page.", }, "showpony:demo.public.host-link": { de: "Host-Login", en: "Host login" }, "showpony:demo.public.tutorial-link": { de: "Tutorial", en: "Tutorial" }, "showpony:demo.read-only.error": { de: "Live-Demo ist read-only — lokal klonen zum Ausprobieren.", en: "Live demo is read-only — clone locally to try writes.", },
"showpony:caps.usageTitle": { de: "Verbrauch", en: "Usage" }, "showpony:caps.events": { de: "Events", en: "Events" }, "showpony:caps.guests": { de: "RSVPs", en: "RSVPs" }, "showpony:caps.unlimited": { de: "(unbegrenzt)", en: "(unlimited)" }, "showpony:caps.upgradeHint": { de: "Tarif upgraden", en: "Upgrade plan" },
"showpony:billing.title": { de: "Tarif & Abrechnung", en: "Plan & billing" }, "showpony:billing.currentTier": { de: "Aktueller Tarif", en: "Current plan" }, "showpony:billing.notConfigured": { de: "Stripe-Checkout ist auf dieser Instanz nicht live (billingLive aus oder keine API-Keys).", en: "Stripe checkout is not live on this instance (billingLive off or no API keys).", }, "showpony:billing.upgradeTo": { de: "Wechseln zu", en: "Switch to" }, "showpony:billing.perMonth": { de: "€ / Monat", en: "€ / month" }, "showpony:billing.unlimited": { de: "Unbegrenzt", en: "Unlimited" }, "showpony:billing.benefit.events": { de: "Events", en: "events" }, "showpony:billing.benefit.guests": { de: "RSVPs gesamt", en: "total RSVPs" }, "showpony:billing.managePortal": { de: "Abrechnung verwalten", en: "Manage billing" }, "showpony:billing.portalHint": { de: "Rechnungen und Zahlungsmittel verwaltest du im Stripe-Portal.", en: "Manage invoices and payment methods in the Stripe portal.", }, "showpony:billing.redirecting": { de: "Weiterleitung zu Stripe …", en: "Redirecting to Stripe …", }, "showpony:billing.error": { de: "Checkout fehlgeschlagen", en: "Checkout failed" }, "showpony:billing.status.active": { de: "aktiv", en: "active" }, "showpony:billing.status.trialing": { de: "Testphase", en: "trial" }, "showpony:billing.status.past_due": { de: "Zahlung überfällig", en: "past due" }, "showpony:billing.status.canceled": { de: "gekündigt", en: "canceled" }, "showpony:billing.status.incomplete": { de: "unvollständig", en: "incomplete" }, "showpony:billing.status.incomplete_expired": { de: "abgelaufen", en: "expired", }, "showpony:billing.status.unpaid": { de: "unbezahlt", en: "unpaid" }, "showpony:billing.status.paused": { de: "pausiert", en: "paused" }, "showpony:billing.switchInPortal": { de: "Im Portal wechseln zu", en: "Switch in portal to" },
"showpony:errors.eventLimitReached": { de: "Event-Limit erreicht — Tarif upgraden.", en: "Event limit reached — upgrade your plan.", }, "showpony:errors.guestLimitReached": { de: "Die Gästeliste ist voll.", en: "The guest list is full.", },} as const satisfies Record<string, LocaleEntry>;📄 On GitHub: src/features/app-shell/feature.ts · bin/demo-tenants.ts · src/features/show-pony/i18n/keys.ts