Portal-core contracts
PortalFeatureDefinition is the public contract a feature registers with portal core.
interface PortalFeatureDefinition<Action extends string = string> {
id: string
navigation?: readonly PortalNavigationItem[]
modules?: readonly PortalModuleContribution[]
dashboardWidgets?: readonly PortalDashboardWidget[]
surfaces?: readonly PortalSurfaceContribution[]
policy: PortalFeaturePolicy<Action>
}
Audiences
Contributions declare who may see them:
| Audience | Meaning |
|---|---|
public | No authenticated session required |
authenticated | Any signed-in user |
organizationAdmin | Organization owner or administrator |
admin | System administrator |
Visibility is not authorization. Server routes must still enforce the feature policy.
Navigation and modules
navigation contributes top-level destinations. modules describe a cohesive area with:
- a landing route;
- route prefixes used to detect the active module;
- an audience and order;
- a module-specific sidebar menu.
Each business feature owns its module and data. Optional integrations use narrow contracts: the invoice-timesheets bridge implements the invoice-source provider contract and owns cross-module source links.
SaaS sidebar and global search
SaaS Portal opts into the unified sidebar with portal.sidebarNavigation: true in its host app.config.ts. Other hosts retain their module header and contextual sidebar unless they opt in.
The shared sidebar shows all accessible modules, with expandable subitems. The active module opens automatically on navigation. Collapsing the sidebar preserves its width preference and replaces groups with flyouts: hover or click the icon, or focus it and press Enter, Arrow Right, or Arrow Down. Escape closes the flyout. The expand/collapse control stays in the sidebar footer while menus scroll. On smaller screens, the header menu provides the same module groups.
Global search combines module landing pages, every submenu destination, and standalone navigation links, deduplicated by URL. It uses the same enabled-feature, audience, organization-role, and capability filtering as navigation. Translated module names appear beside results and are searchable along with page labels.
For browser verification, run PORTAL_NAVIGATION_E2E=1 pnpm exec playwright test --config test/e2e/navigation.config.ts against the isolated Products browser fixture documented in packages/products/README.md. Add a verified ordinary user navigation-member@example.test with the same fixture password and member membership of media-store; this checks that management menus and search results remain hidden. Set PORTAL_MEDIA_E2E_URL to the disposable portal's localhost origin. Never add these accounts to a real portal.
Dashboard widgets
A widget declares a registered component plus its placement:
- area:
attention,main, oraside; - size:
full,half, orthird; - numeric order.
The UI package sorts all installed widgets, so the dashboard does not import feature components directly. Components are registered by serializable name.
Surface contributions
Surfaces let an optional package contribute a named panel to another package's screen. Timesheets registers an organization panel for administration.organization.detail; Administration renders registered panels without importing Timesheets.
Typed policies
Features define their own action vocabulary and map actions to organization roles:
const actions = ['read', 'create', 'update', 'submit', 'approve'] as const
const policy = {
owner: actions,
admin: actions,
member: ['read', 'create', 'update', 'submit']
}
Server routes pass the policy and requested action to requireFeatureAccess. That adapter authenticates the request, resolves the active organization, checks membership, evaluates the role, and returns tenant context for the query.
Registration behavior
Feature IDs are unique. Registering the same ID again replaces the previous definition, which supports Nuxt development reloads without duplicate navigation or widgets.
For every field and allowed value, see the feature contract reference. For a complete layer workflow, continue with Create a feature layer.
