Skip to content

Mount the foundation

Kumiko ships identity, auth, and multi-tenancy as bundled features. You do not wire login, user tables, or tenant schemas by hand. List your own features in one function and flip one flag for the schema CLI.

src/run-config.ts is the single source of truth for what your app mounts. The sample no longer exports a static APP_FEATURES array. It builds the list from tenant routing (base domain) so dev, prod, and schema generation stay aligned:

export function resolveBaseDomainFromEnv(): string {
return process.env["BASE_DOMAIN"] ?? "show-pony.localhost";
}
export function buildAppFeatures(routing: AppFeaturesRouting): FeatureDefinition[] {
return [
localeDe(),
authFoundationFeature,
createShowPonyTenantRoutingFeature({ baseDomain: routing.baseDomain }),
createTierEngineFeature({ defaultTier: DEFAULT_TIER, tierMap: SHOWPONY_TIER_MAP }),
createShowPonyBillingFoundationFeature(routing.appBaseUrl ?? `https://${routing.baseDomain}`),
// mail, admin shell, app shell, showPonyFeature, …
];
}
export const HAS_AUTH = true;
From src/run-config.ts.

buildAppFeatures pulls in tenant routing, billing, tier limits, and the operational plugins (mail, jobs, audit, …) alongside showPonyFeature. routing.appBaseUrl is optional; billing links fall back to https:// plus the base domain when it is unset. HAS_AUTH = true is consumed by bin/kumiko.ts when generating schema: composeFeatures([...buildAppFeatures(...)], { includeBundled: HAS_AUTH }). At runtime, runDevApp / runProdApp call composeFeatures automatically when you pass an auth: block. That adds user, tenant, and email/password login without listing them in run-config.ts.

Login on the apex host dashboard
Email/password gate before the dashboard loads

After bun dev:

  • Email/password login at /login on the apex (show-pony.localhost:4180): chapter 12 adds the public landing on /
  • A seeded demo host: admin@show-pony.local / changeme
  • A seeded sysadmin: sysadmin@show-pony.local / changeme (platform workspace): chapter 8)
  • Tenant-scoped writes and queries, with isolation from the framework rather than custom middleware
  • Admin shell (sidebar, topbar) ready for schema-driven screens in chapter 8

bin/server.ts spreads buildAppFeatures({ baseDomain: BASE_DOMAIN }) into runDevApp, seeds the demo admin, and sets up host routing (apex vs subdomain; detailed in chapter 4).

Demo events + RSVPs are seeded only in production (seedsDir in bin/main.ts, not on bun dev). Locally you create events in the dashboard or follow chapter 11.

// Single source of truth for show-pony feature composition.
// Both bin/server.ts (dev) and the kumiko-schema CLI build on this,
// so the runtime registry and generated schema never drift apart.
//
// HAS_AUTH=true → composeFeatures automatically pulls in the bundled auth chain
// (config/user/tenant/auth-email-password/secrets).
import { createAdminShellFeature } from "@cosmicdrift/kumiko-bundled-features/admin-shell";
import { createAuthFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
import {
createChannelEmailFeature,
createInMemoryTransport,
} from "@cosmicdrift/kumiko-bundled-features/channel-email";
import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
import { createDeliveryFeature } from "@cosmicdrift/kumiko-bundled-features/delivery";
import {
createHostRoutingFeature,
type HostRoutingOptions,
} from "@cosmicdrift/kumiko-bundled-features/host-routing";
import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";
import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
import { createManagedPagesFeature } from "@cosmicdrift/kumiko-bundled-features/managed-pages";
import {
createMetricsFeature,
createSystemMetricsFeature,
DEFAULT_METRICS,
} from "@cosmicdrift/kumiko-bundled-features/metrics";
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
import { createTierEngineFeature } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
import {
composeRendererStack,
composeSecurityStack,
} from "@cosmicdrift/kumiko-dev-server/compose-stacks";
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
import { localeDe } from "@cosmicdrift/kumiko-locale-de";
import { appShellFeature } from "./features/app-shell/feature";
import { createShowPonyBillingFoundationFeature } from "./features/show-pony/billing/plan-catalog";
import { showPonyFeature } from "./features/show-pony/feature";
import { DEFAULT_TIER, SHOWPONY_TIER_MAP } from "./features/show-pony/tier-map";
import { showPonyLegalLayout } from "./marketing/layouts/legal";
import { resolveSubdomainPageTenant } from "./tenant-routing";
/** Overview screens + nav only — app-shell owns workspaces host/platform. */
const adminShellFeature = createAdminShellFeature({
registerWorkspaces: false,
includeTierAdmin: true,
metrics: DEFAULT_METRICS,
});
export type AppFeaturesRouting = {
readonly baseDomain: string;
/** Absolute origin the billing-plans catalog redirects back to after
* checkout/portal — defaults to `https://${baseDomain}` when omitted. */
readonly appBaseUrl?: string;
};
/** $BASE_DOMAIN or show-pony.localhost — the one place that default lives. */
export function resolveBaseDomainFromEnv(
env: Record<string, string | undefined> = process.env,
): string {
return env["BASE_DOMAIN"] ?? "show-pony.localhost";
}
/** The host decides the tenant: apex = host surface, `<key>.<baseDomain>` = guest pages.
* No apexAnonymousTenantId, so anonymous writes on the apex keep answering tenant_required. */
export function showPonyHostRoutingOptions(baseDomain: string): HostRoutingOptions {
return {
tenantBy: "host",
baseDomain,
apex: { file: "admin.html" },
tenant: { file: "index.html" },
unknownTenant: { file: "index.html" },
};
}
export function buildAppFeatures(routing: AppFeaturesRouting): FeatureDefinition[] {
return [
localeDe(),
createAuthFoundationFeature(),
...composeSecurityStack(),
createHostRoutingFeature(showPonyHostRoutingOptions(routing.baseDomain)),
...composeRendererStack(),
createLegalPagesFeature({ wrapLayout: showPonyLegalLayout, terms: true }),
createManagedPagesFeature({
resolveApexTenant: resolveSubdomainPageTenant,
allowCustomCss: false,
}),
createDeliveryFeature(),
// Guests are addressed by the email they typed, so no user lookup exists.
// The in-memory transport keeps the demo behaviour: no RSVP mail leaves the process.
createChannelEmailFeature({
transport: createInMemoryTransport(),
resolveEmail: async () => null,
}),
createJobsFeature(),
createTierEngineFeature({ defaultTier: DEFAULT_TIER, tierMap: SHOWPONY_TIER_MAP }),
createComplianceProfilesFeature(),
createTenantLifecycleFeature(),
createShowPonyBillingFoundationFeature(routing.appBaseUrl ?? `https://${routing.baseDomain}`),
createSecretsFeature(),
createMetricsFeature({ metrics: DEFAULT_METRICS }),
createSystemMetricsFeature({ metrics: DEFAULT_METRICS }),
adminShellFeature,
appShellFeature,
showPonyFeature,
];
}
export const HAS_AUTH = true;

📄 On GitHub: src/run-config.ts