Feature contract reference
Feature layers integrate with the application shell through PortalFeatureDefinition. Import the type from @nuxt-customer-portal/core/feature and register a definition from a client plugin with usePortalFeatures().
interface PortalFeatureDefinition<Action extends string = string> {
id: string
navigation?: readonly PortalNavigationItem[]
modules?: readonly PortalModuleContribution[]
dashboardWidgets?: readonly PortalDashboardWidget[]
surfaces?: readonly PortalSurfaceContribution[]
policy: PortalFeaturePolicy<Action>
}
Feature fields
| Field | Required | Contract |
|---|---|---|
id | Yes | Stable, globally unique feature identifier. Registering the same ID again replaces the previous definition. |
navigation | No | Top-level destinations contributed to the portal shell. |
modules | No | Cohesive module areas and their sidebar menus. One feature may contribute multiple modules. |
dashboardWidgets | No | Components aggregated into the shared dashboard. |
surfaces | No | Generic component-name contributions to a documented host surface. |
policy | Yes | Allowed feature actions for each organization role. |
The registry derives sorted navigation, modules, and dashboardWidgets collections. Navigation and modules default to order 100; widgets require an explicit order.
Audiences and roles
type PortalAudience =
| 'public'
| 'authenticated'
| 'organizationAdmin'
| 'admin'
type PortalOrganizationRole = 'owner' | 'admin' | 'member'
Audiences control shell visibility. Policies control server authorization. A hidden link does not protect an API route.
Navigation item
| Field | Type | Notes |
|---|---|---|
id | string | Stable identifier within the registry. |
labelKey | string | Translation key, normally below features.<feature>. |
icon | string? | Nuxt Icon name. |
to | string | Destination route. |
audiences | PortalAudience[] | People who may see the destination. |
location | 'main' | 'admin'? | Defaults to the main application navigation. |
order | number? | Lower values appear first; default is 100. |
badge | PortalBadgeValue? | Package-owned serializable label, color, and variant contract. |
Module contribution
A module describes one selectable product area:
| Field | Type | Notes |
|---|---|---|
id | string | Stable module identifier. |
labelKey | string | Translated module label. |
icon | string? | Module icon. |
to | string | Landing route for the current user. |
routePrefixes | string[] | Prefixes that keep the module active while navigating. |
audiences | PortalAudience[] | Shell visibility. |
order | number? | Module ordering; default is 100. |
badge | PortalBadgeValue? | Optional serializable module indicator. |
menuItems | PortalModuleMenuItem[]? | Sidebar destinations belonging to the module. |
A menu item uses id, labelKey, to, audiences, and optional icon, exact, order, and badge fields. Use exact: true when a parent route must not remain active on child pages.
Dashboard widget
| Field | Allowed values |
|---|---|
id | Stable string identifier |
component | Registered component name string |
area | attention, main, or aside |
size | full, half, or third |
order | Required number |
Widgets sort by area, then order, then ID. A feature may register an initial definition and replace it after loading server capabilities; the timesheets layer uses this to remove widgets and menu items a user cannot access.
Surface contribution
PortalSurfaceContribution has a unique id, a documented surface, a registered component-name string, and optional numeric order. The built-in cross-feature surface is administration.organization.detail. The contributing package owns the rendered component.
Policy
Define a literal action list and map every organization role deliberately:
const actions = ['read', 'create', 'update', 'manage'] as const
type Action = typeof actions[number]
const policy: PortalFeaturePolicy<Action> = {
owner: actions,
admin: actions,
member: ['read', 'create', 'update']
}
System administrators bypass an organization policy. Feature routes should normally call requireFeatureAccess(event, policy, action) from @nuxt-customer-portal/core/server. It returns the authenticated session and active organization ID after enforcing both membership and the policy.
The server adapter also exposes getSession, requireSession, requireActiveOrganization, authorize, hasFeatureAccess, requireActiveOrganizationRole, and owner-only organization email-credential access. Prefer these adapters over importing Better Auth or core database internals into a feature.
For an end-to-end implementation, continue with Create a feature layer.