FeatureModule contract

Every screen in an app is contributed by a FeatureModule. The full contract, verbatim from packages/shell/src/shell/registry/types.ts:

import type { ComponentType } from "react";

/** A sidebar entry. Derived automatically from pages that declare `nav`. */
export interface NavItem {
  to: string;
  label: string;
  icon?: string;
  group?: string;
}

/** A single routed screen contributed by a feature module. */
export interface PageSpec {
  /** Route path relative to root, e.g. "/", "/tasks", "/tasks/$taskId". */
  path: string;
  component: ComponentType;
  /** Present ⇒ this page also appears in the sidebar (keyed on `path`). */
  nav?: { label: string; icon?: string; group?: string };
}

/**
 * The plug-and-play unit. Drop a folder under `src/features/<id>`, export a
 * FeatureModule, list it in the app's `features` array — routes + nav wire up
 * automatically. Remove it from the array to fully unplug the feature.
 */
export interface FeatureModule {
  id: string;
  title: string;
  pages: PageSpec[];
}

How the registry wires routes and nav

The app is literally the features array in src/features/index.tsx. Each module's pages become TanStack Router routes (paths support params like /tasks/$taskId); every page that declares nav also becomes a sidebar/nav entry with its label, icon, and optional group (grouped layouts render group headings). Array order = nav order. Reorder the array to reorder nav; delete a line to unplug a feature entirely — there is no other registration step.

Shell factories vs template features

Features come from two places, visible in the template's src/features/index.tsx import block:

  • From @geenius-mvp/shell (drop-in factories, called with the app's generated api): apiKeysFeature(api), teamFeature(api), billingFeature(api), inboxFeature(api), feedbackFeature(api), adminFeature(api), settingsFeature(api). These are shell-surface features — the shell owns their UI, the app passes its own backend api.
  • From the app's own src/features folders (domain features): dashboardFeature, exampleFeature, recordsFeature, aiFeature, uploadsFeature, activityFeature, mcpFeature. These are stamped from the template and are yours to edit.

Always-on features

dashboardFeature and settingsFeature(api) are not wrapped in @feature: markers — every webapp gets them regardless of the --features selection. The other 12 (example, records, activity, api-keys, team, billing, admin, inbox, feedback, ai, uploads, mcp) are optional and marker-gated; mcp implies api-keys (FEATURE_IMPLIES in tools/lib/features.mjs).

Adding a domain feature

// src/features/invoices/index.tsx
import { PageHeader, type FeatureModule } from "@geenius-mvp/shell";

function InvoicesPage() {
  return <><PageHeader title="Invoices" />{/* ... */}</>;
}

export const invoicesFeature: FeatureModule = {
  id: "invoices",
  title: "Invoices",
  pages: [{ path: "/invoices", component: InvoicesPage,
    nav: { label: "Invoices", icon: "⧉" } }],
};

Then one import + one line in the features array of src/features/index.tsx. Render Skeleton while useQuery is undefined and EmptyState when the list is empty.