Feature Development Guide
Documentation for Feature Development Guide
Audience: Developers building @neutrino-io/feature-* packages for the NNO console shell
Last updated: 2026-03-30
Contracts first: Before implementing, read the Feature Package SDK spec for the full
FeatureDefinitioninterface and the Shell Config spec for auto-discovery mechanics. This guide is a practical walkthrough — those specs are the authoritative contracts.
1. Overview
A feature package is a self-contained npm package that plugs into the NNO console shell. It declares its routes, sidebar navigation, and permissions in a FeatureDefinition object. The shell discovers it at build time via the neutrino-feature-discovery Vite plugin — no manual registration required.
Integration flow:
Install @neutrino-io/feature-my-feature
│
▼
Vite plugin scans package.json → finds "neutrino": {"type": "feature"}
│
▼
Generates virtual:feature-registry with static import
│
▼
Shell reads featureManifest → merges with features.config.ts
│
▼
FeatureRegistry loads FeatureDefinition → builds routes + sidebarTwo exports drive this integration:
| Export | File | Purpose |
|---|---|---|
featureManifest | src/manifest.ts | Auto-discovery metadata (load priority, groups, sub-routes) |
\{id\}FeatureDefinition | src/feature.ts | Full contract: routes, navigation hierarchy, permissions |
2. Prerequisites
- Working
nno-app-buildercheckout withpnpm installcompleted - Node 20+, pnpm 9+
- Familiarity with React 19 and TypeScript
- Read the Feature Package SDK spec — especially
FeatureDefinition,FeatureRoute, andFeatureNavItem
3. Scaffold a Feature Package
Create your package directory under features/:
features/my-feature/
├── package.json # Must declare "neutrino": {"type": "feature"}
├── tsconfig.json
├── tsup.config.ts
└── src/
├── index.ts # Public barrel — exports featureManifest + FeatureDefinition
├── manifest.ts # FeatureManifest for auto-discovery
├── feature.ts # FeatureDefinition (routes, navigation, permissions)
├── routes/ # Page-level components
│ └── MyFeaturePage.tsx
└── components/ # Feature-scoped UI componentspackage.json
The "neutrino": \{"type": "feature"\} field is required for auto-discovery. Follow the pattern from features/settings/package.json and features/billing/package.json:
{
"name": "@neutrino-io/feature-my-feature",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit",
"lint": "eslint",
"test": "vitest run"
},
"dependencies": {
"@neutrino-io/sdk": "workspace:*",
"@neutrino-io/ui-core": "workspace:*"
},
"devDependencies": {
"@neutrino-io/eslint-config": "workspace:*",
"@neutrino-io/tsconfig": "workspace:*",
"@types/react": "catalog:react19",
"tsup": "catalog:",
"typescript": "catalog:"
},
"peerDependencies": {
"react": "catalog:react19",
"react-dom": "catalog:react19"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "restricted"
},
"neutrino": {
"type": "feature"
},
"repository": {
"type": "git",
"url": "git+https://github.com/neutrino-io/nno-app-builder.git",
"directory": "features/my-feature"
}
}tsconfig.json
{
"extends": "@neutrino-io/tsconfig/base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"composite": false,
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
}tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: { index: "src/index.ts" },
format: ["cjs", "esm"],
dts: false,
sourcemap: false,
clean: true,
esbuildOptions(options) {
options.jsx = "automatic";
options.jsxImportSource = "react";
},
external: [
"react",
"react-dom",
"react/jsx-runtime",
"@neutrino-io/sdk",
"@neutrino-io/ui-core",
"zod",
],
treeshake: true,
bundle: true,
});4. Implement featureManifest
src/manifest.ts provides the auto-discovery metadata consumed by the Vite plugin and features.config.ts. Keep it minimal — it is not the routing contract.
// src/manifest.ts
import type { FeatureManifest } from "@neutrino-io/sdk/types";
export const featureManifest: FeatureManifest = {
id: "my-feature",
name: "My Feature",
package: "@neutrino-io/feature-my-feature",
module: "features/my-feature",
enabledByDefault: false, // false = opt-in; true = active on install
loadPriority: 90, // 10–89 = domain features; 90–98 = client features
lazyLoad: true,
environment: "all",
domain: "my-feature",
type: "business",
group: "Overview", // Sidebar group heading
defaultComponent: "MyFeaturePage",
};enabledByDefault: false is the safe default for new features. The platform operator activates it via the NNO Portal or features.config.ts.
See the Shell Config spec §FeatureManifest for the full field reference.
5. Define Routes
src/feature.ts exports the canonical FeatureDefinition. The shell's FeatureRegistry resolves \{id\}FeatureDefinition (e.g. myFeatureFeatureDefinition) first; fall back to \{id\}FeatureConfig is for legacy packages only.
// src/feature.ts
import type { FeatureDefinition } from "@neutrino-io/sdk/feature";
import type { FeatureRoute } from "@neutrino-io/sdk/types";
const routes: FeatureRoute[] = [
{
path: "/my-feature",
component: "MyFeaturePage",
auth: true,
layout: "default",
permissions: ["my-feature:read"],
meta: { title: "My Feature" },
},
{
// Dynamic segment — TanStack Router $param syntax
path: "/my-feature/detail/$id",
component: "MyFeatureDetailPage",
auth: true,
permissions: ["my-feature:read"],
meta: { title: "Detail" },
},
];
export const myFeatureFeatureDefinition: FeatureDefinition = {
id: "my-feature",
version: "0.1.0",
displayName: "My Feature",
description: "Short description shown in the NNO Portal",
icon: "Zap", // Lucide icon name
routes,
navigation: [/* see Section 6 */],
permissions: ["my-feature:read"],
requiresService: false, // true if a backend Worker is needed
};
export default myFeatureFeatureDefinition;Path conventions (paths are registered as-is — no shell prefix added for FeatureDefinition-based routes):
| Path | Use case |
|---|---|
/my-feature | Feature root |
/my-feature/detail/$id | Dynamic segment |
/my-feature/settings | Nested sub-page |
See Feature SDK spec §1.2 for the full FeatureRoute interface.
6. Add Navigation
Declare navigation inside the FeatureDefinition. The shell's sidebar generator reads navigation[0].children to build collapsible sub-menus. Icon strings are resolved to lucide-react components by the shell — feature packages do not need to import lucide-react for icon declarations.
Flat sidebar entry:
navigation: [
{
label: "My Feature",
path: "/my-feature",
icon: "Zap", // Resolved to <Zap /> by the shell
order: 90,
group: "Overview",
permissions: ["my-feature:read"],
},
],Collapsible sub-menu (modelled on features/settings/src/feature.ts):
navigation: [
{
label: "My Feature",
path: "/my-feature",
icon: "Zap",
order: 90,
children: [
{ label: "Dashboard", path: "/my-feature", icon: "LayoutDashboard", order: 1 },
{ label: "Detail", path: "/my-feature/detail", icon: "FileText", order: 2 },
{ label: "Settings", path: "/my-feature/settings", icon: "Settings", order: 3 },
],
},
],order controls sort position within the group (ascending, default 99). children are sorted independently.
See Feature SDK spec §1.3 for the full FeatureNavItem interface including badge and per-item permissions.
7. Use SDK Hooks
Import hooks from @neutrino-io/sdk. The SDK has zero runtime dependencies on the shell itself, so features remain independently testable.
import { useNnoSession } from "@neutrino-io/sdk";
// Access the authenticated user and their permissions
const { user } = useNnoSession();
const canWrite = user.permissions.includes("my-feature:write");import { useServiceUrl } from "@neutrino-io/sdk";
// Resolve the backend Worker URL (reads VITE_* env vars, then env.config.ts)
const apiUrl = useServiceUrl("VITE_MY_FEATURE_API_URL");For features that need a backend Worker, set requiresService: true and serviceEnvKey: "VITE_MY_FEATURE_API_URL" in the FeatureDefinition. The NNO CLI Service provisions the Worker and injects the URL.
Pair server state fetching with TanStack Query — do not aggregate data directly in components:
import { useQuery } from "@tanstack/react-query";
import { useServiceUrl } from "@neutrino-io/sdk";
function useMyFeatureData() {
const apiUrl = useServiceUrl("VITE_MY_FEATURE_API_URL");
return useQuery({
queryKey: ["my-feature"],
queryFn: () => fetch(`${apiUrl}/items`).then((r) => r.json()),
});
}8. Use ui-core Components
Import shared components from @neutrino-io/ui-core. Do not copy or reimplement them in your feature package.
import { PageHeader, DataTable } from "@neutrino-io/ui-core";
export function MyFeaturePage() {
return (
<div>
<PageHeader
title="My Feature"
description="Feature description shown below the title"
/>
<DataTable columns={columns} data={data} />
</div>
);
}Common components: PageHeader, DataTable, Card, Button, Badge, EmptyState. Check the @neutrino-io/ui-core exports for the full list — add @neutrino-io/ui-core as a dependency (not devDependency) and mark it external in tsup.config.ts.
9. Wire Up the Barrel
src/index.ts is the package's public API. Export both the featureManifest (required for auto-discovery) and the FeatureDefinition:
// src/index.ts
// Page components consumed by the shell's route loader
export { MyFeaturePage } from "./routes/MyFeaturePage";
export { MyFeatureDetailPage } from "./routes/MyFeatureDetailPage";
// Canonical FeatureDefinition — shell resolves myFeatureFeatureDefinition first
export { myFeatureFeatureDefinition, default } from "./feature";
// Auto-discovery manifest — required for Vite plugin
export { featureManifest } from "./manifest";10. Test Locally
-
Add the package to
apps/console/package.jsonas a workspace dependency:"@neutrino-io/feature-my-feature": "workspace:*" -
Build the feature package:
pnpm --filter @neutrino-io/feature-my-feature build -
Start the console:
cd apps/console && pnpm dev -
Verify auto-discovery picked it up — open the browser console and look for the feature registry log, or navigate directly to
/my-feature. -
Confirm the sidebar entry appears under the declared
group.
Watch mode for active development:
# Terminal 1 — rebuild feature on change
pnpm --filter @neutrino-io/feature-my-feature dev
# Terminal 2 — console dev server (HMR picks up rebuilt dist)
cd apps/console && pnpm dev11. Publish
Follow the Package Registry Guide for authentication setup and CI publishing. Summary:
- Bump
versioninpackage.json(semver). - Build:
pnpm --filter @neutrino-io/feature-my-feature build - Publish:
pnpm --filter @neutrino-io/feature-my-feature publish
Packages are published to https://npm.pkg.github.com under the @neutrino-io scope. CI publishes automatically on push to main when package files change.
12. Activate on a Platform
In the consumer platform repo (e.g. nno-stack-starter):
-
Install the package:
pnpm add @neutrino-io/feature-my-feature -
Auto-discovery handles registration — no edits to
features.config.tsare needed. The shell merges thefeatureManifeston next build. -
To override settings (load priority, sidebar visibility, permissions), add an explicit entry to
apps/console/src/config/features.overrides.ts. -
To enable a feature that ships with
enabledByDefault: false, setenabled: trueinfeatures.config.tsor via the NNO Portal.
13. Feature Development Checklist
-
package.jsondeclares"neutrino": \{"type": "feature"\}and follows@neutrino-io/feature-*naming -
src/manifest.tsexportsfeatureManifestwith correctid,package,loadPriority,group -
src/feature.tsexports\{id\}FeatureDefinition(camelCase) as the canonical export and asdefault -
src/index.tsre-exports bothfeatureManifestand\{id\}FeatureDefinition - All route
pathvalues matchnavigationpaths exactly -
permissionsdeclared inFeatureDefinitionfollow\{feature-id\}:\{action\}format -
tsup.config.tsmarksreact,react-dom,@neutrino-io/sdk,@neutrino-io/ui-coreasexternal -
tsconfig.jsonextends@neutrino-io/tsconfig/base.jsonwith"jsx": "react-jsx" - Feature builds without TypeScript errors:
pnpm --filter @neutrino-io/feature-my-feature typecheck - Sidebar entry appears and routes resolve in
pnpm dev - Unit tests cover key components and hooks
-
publishConfig.registrypoints tohttps://npm.pkg.github.com
Related specs
- Feature Package SDK —
FeatureDefinition,FeatureRoute,FeatureNavItem,ShellContext - Shell Config & Auto-Discovery —
FeatureManifest, boot sequence, permission gates - Package Registry Guide — publishing to GitHub Packages