Appearance
Building Features
This guide is for teams building feature apps on Pilar Runtime — Pilar's runtime feature host.
Quick start
bash
# 1. Scaffold a new feature
pnpm run create-feature my-feature
# 2. Develop with hot-reload
pnpm run dev:feature examples/feature-my-feature
# 3. Build and publish locally
pnpm run build
pnpm run publish:local
# 4. Run the host
pnpm run dev:host
# Visit http://localhost:3000/my-featureProject structure
examples/feature-my-feature/
manifest.json # Feature contract (routes, auth, shared deps)
package.json # Standard npm package
vite.config.js # Vite build config
vite.config.server.js # Server functions build config
scripts/publish.js # Local publish script
src/
main.jsx # Entry point (bootstraps React)
App.jsx # Root component
actions.server.js # Server functions (RPC)Bootstrapping your feature
Every feature entry point follows this pattern:
jsx
// src/main.jsx
import { createRoot } from 'react-dom/client';
import {
getBootstrapConfig,
getMountElement,
registerReactUnmount,
createFeatureHostApi,
} from '@favn/feature-sdk';
import App from './App.jsx';
const bootstrap = getBootstrapConfig({
defaultFeatureId: 'feature-my-feature',
defaultBasename: '/my-feature',
defaultDomElementId: 'feature-root',
});
const container = getMountElement(bootstrap.domElementId);
const root = createRoot(container);
root.render(<App bootstrap={bootstrap} />);
registerReactUnmount(root, bootstrap.featureId);registerReactUnmount does two things:
- Signals to the host that bootstrap succeeded (disabling the fallback error handler)
- Registers cleanup so the host can unmount your feature on page transitions
Shared dependencies
Declare shared deps to avoid bundling React/ReactDOM in every feature:
json
{
"shared": {
"react": { "singleton": true, "requiredVersion": "^19.0.0" },
"react-dom": { "singleton": true, "requiredVersion": "^19.0.0" },
"react-router-dom": { "singleton": true, "requiredVersion": "^7.1.0" }
}
}The host provides these via import maps. Your Vite config should externalize them:
js
// vite.config.js
export default {
build: {
rollupOptions: {
external: ['react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', 'react-router-dom'],
output: { format: 'esm' },
},
},
};Reuse and data ownership
If you are building reusable UI modules, use featureType: "facet" and keep data orchestration in the consuming domain feature (the feature that owns the route/workflow and mounts the facet).
For patterns and flow diagrams, see Reuse and Data Flow.
Development workflow
Local development with HMR
bash
pnpm run dev:feature examples/feature-my-featureThis starts a standalone Vite dev server with:
- Hot module replacement
- Mock host event bus, store, and lifecycle
- RPC endpoint for server functions with mock ServerContext
- Shared deps served from workspace
Running against the full host
bash
pnpm run build
pnpm run publish:local
pnpm run dev:hostValidating compatibility
bash
pnpm run validate:compat # Standard check
pnpm run validate:compat:strict # Warnings become errorsError handling
The host provides three layers of error resilience:
- Resource load failures — If your JS/CSS fails to load, the host shows a fallback UI with a retry button.
- Bootstrap runtime errors — If your code throws during initial render, the host catches it and shows a fallback.
- React runtime errors — After successful mount, errors are handled by React's ErrorBoundary. Use
@favn/error-boundaryfor feature-level error boundaries.
The host also caches the last-known-good HTML for each feature path as a fallback.
Styling
Pilar Runtime does not enforce a specific design system, but features targeting the DIPS platform should use Puls design tokens — the same system used across Pilar Embedded and Arena. Avoid raw Tailwind color utilities; use semantic tokens like bg-surface-default instead.
If your feature uses Shadow DOM isolation (isolation.shadowDOM: true), Puls styles must be injected into the shadow root. See the SDK docs for details.
Arena Desktop integration
If your feature should appear in Arena Desktop's navigation, add the arena block to your manifest:
json
{
"arena": {
"pageId": "your-uuid",
"pageType": "PatientPage",
"elementId": 12345,
"iconKey": "your-icon"
}
}See Arena Desktop Integration for the full field reference.
CI checklist
Before deploying a feature:
- [ ]
pnpm run validate:compatpasses - [ ]
pnpm run lint:routespasses (no route conflicts) - [ ]
pnpm run doctorpasses (manifest schema validation) - [ ] Feature manifest has explicit
authRequiredsetting - [ ] Server functions enforce authorization checks for write operations
- [ ]
arenafields are set if the feature should appear in Arena Desktop - [ ]
infrastructurefields document backend service dependencies
Reference example
See examples/feature-reference/ for a comprehensive example demonstrating all host API integrations, server functions with ServerContext, and team-owned authorization.