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.