Navigation
Configure the navbar, sidebar, user menu, and section tabs in the Nuxt frontend.
Navigation is plain TypeScript config under apps/web-nuxt/app/config/ (marketing navbar, dashboard sidebar, user menu) plus the shared sectionTabsConfig in @repo/config. Items hide declaratively via enabled, roles, and feature flags, so disabled features and unauthorized links never render.
Config files
Each list is exported from its own file. Edit the typed structures directly.
| File | Export | Renders |
|---|---|---|
app/config/navbar.ts | navbarItems | Marketing site top nav |
app/config/sidebar.ts | sidebarConfig | Dashboard sidebar |
app/config/user-menu.ts | userMenuItems | Avatar dropdown |
@repo/config section-tabs.ts | sectionTabsConfig | Sub-tabs in /admin, /settings, /settings/organization |
Gating fields
Sidebar items, sidebar categories, and section tabs share these optional gating fields. An item renders only when all present conditions pass.
| Field | Type | Effect |
|---|---|---|
requires | SidebarFeatureFlag[] | Show only when every listed feature flag is enabled. |
roles | ("user" | "admin")[] | Show only for matching app roles (e.g. ["admin"]). |
orgRoles | ("member" | "admin" | "owner")[] | Show only for matching organization roles. |
The SidebarFeatureFlag type has eleven members, each resolved by the app's isSidebarFeatureEnabled helper (apps/web-nuxt/app/utils/sidebar-flags.ts) - the single source of truth for the sidebar and section tabs. Nine map to a config check; the other two (adminNotifications and agents) have no case, so they fall through to the resolver's default and always pass - never rely on them as a gate. Any unhandled or future flag defaults to visible the same way.
| Flag | Enabled when |
|---|---|
multiTenant | config.tenancy.multiTenant |
notifications | config.notifications.enabled |
apiKeys | config.apiKeys.enabled |
credits | config.payment.enabled and pricingConfig.credits.enabled |
ai | config.ai.enabled |
aiByok | config.ai.enabled and config.ai.byok (users may bring their own keys) |
companion | config.companion.enabled |
mcpServer | config.mcpServer.enabled |
emailTracking | config.email.tracking.enabled |
Sidebar categories support only requires and roles (no orgRoles). Navbar items gate differently - they use a single enabled boolean (see below).
This config only hides UI. It is not access control - the underlying API routes and pages enforce auth independently. Never rely on a missing nav link to protect a route.
Sidebar shape
sidebarConfig is an object with navMain, categories, and navSecondary. Each item carries a title translation key, a url, and a directly imported Vue Phosphor icon. The Organization item requires multiTenant; the Admin category requires the admin role.
Active state resolves via match ("exact" | "startsWith", default "startsWith") or activeMatch - a path string that highlights the item on any route below it (e.g. activeMatch: "/settings" keeps Settings active across all settings pages).
// apps/web-nuxt/app/config/sidebar.ts
import { PhGauge, PhBuilding, PhShieldChevron } from "@phosphor-icons/vue";
export const sidebarConfig: SidebarConfig<Component> = {
navMain: [
{ title: "sidebar.dashboard", url: "/dashboard", icon: PhGauge, match: "exact" },
],
categories: [
{
title: "sidebar.categories.account",
items: [
// Hidden unless multi-tenancy is enabled
{ title: "sidebar.organization", url: "/settings/organization",
icon: PhBuilding, requires: ["multiTenant"] },
],
},
{
// Whole category is admin-only
title: "sidebar.categories.admin",
roles: ["admin"],
items: [
{ title: "sidebar.admin", url: "/admin/users",
icon: PhShieldChevron, roles: ["admin"] },
],
},
],
navSecondary: [],
};Top bar title
The dashboard top bar shows the active section's title, resolved from the same sidebar config the highlight uses - the most specific activeMatch/match wins, so the bar always names the section you are in. Routes with no sidebar entry (for example /notifications) take their title from extraTitleRoutes, a small NavMatchable[] exported alongside sidebarConfig; when nothing matches, the bar falls back to the dashboard label.
// apps/web-nuxt/app/config/sidebar.ts
export const extraTitleRoutes: NavMatchable[] = [
{ title: "sidebar.notifications", url: "/notifications", match: "startsWith" }
];Promote a settings tab to the sidebar
Each AI configuration surface lives in Settings but is a real route, so any tab can also appear as its own top-level sidebar item. Add one item to navMain whose url is the tab's route; the tab keeps rendering inside Settings as well, and the same requires flag keeps it hidden when the feature is off.
// apps/web-nuxt/app/config/sidebar.ts
import { PhPlugsConnected } from "@phosphor-icons/vue";
// Add to navMain - the tab still resolves inside Settings too.
{ title: "sidebar.integrations", url: "/settings/integrations",
icon: PhPlugsConnected, activeMatch: "/settings/integrations", requires: ["ai"] },Navbar shape
Marketing navbarItems gate with enabled (consumers filter on enabled !== false), support subitems dropdowns (optionally laid out as a multi-column grid via columns, 1 | 2 | 3 | 4, default 1), and mark primary: true to surface in the mobile bottom nav. The Blog and Docs links bind enabled to their config flags. A dropdown parent may omit href to act as a pure trigger that only opens its submenu instead of navigating. Mark a link external for hard navigation to a URL served outside the app; external links open in a new tab by default, so set newTab: false to keep one in the same tab.
// apps/web-nuxt/app/config/navbar.ts
{ title: "nav.blog", href: "/blog", icon: PhNewspaper,
match: "startsWith", enabled: config.blog.enabled },
{ title: "nav.docs", href: config.docs?.url ?? "/docs", icon: PhBookOpen,
match: "startsWith", enabled: config.docs?.enabled ?? false },Add columns to an item with subitems to spread its dropdown across multiple columns on desktop, filling left to right; the mobile menu stays single-column. Four subitems with columns: 2 render as a 2x2 grid:
// apps/web-nuxt/app/config/navbar.ts
{ title: "nav.product", href: "/#features", icon: PhStackSimple, columns: 2,
subitems: [ /* 4 items render as a 2x2 grid */ ] },Section tabs
The shared sectionTabsConfig drives the horizontal sub-tabs and uses the same gating fields plus match: "exact".
// packages/config/src/section-tabs.ts
admin: [
{ title: "sidebar.users", url: "/admin/users" },
{ title: "sidebar.api_keys", url: "/admin/api-keys", requires: ["apiKeys"] },
],
organization: [
{ title: "sidebar.overview", url: "/settings/organization", match: "exact" },
{ title: "sidebar.activity", url: "/settings/organization-activity",
orgRoles: ["owner", "admin"] },
],Icons
Icons are imported directly from @phosphor-icons/vue (Ph* prefix) - there is no string-key registry.
- Import the
Ph*component (e.g.PhGauge) and assign it toicon. - Each config file is self-contained: its imports sit at the top, keeping the structure buyer-editable.
- Browse names at phosphoricons.com.
Labels
title is always a translation key, never raw text - rendered with $t(). Define the key in packages/i18n/translations/en/web.json (e.g. sidebar.users); other locales are generated automatically.
Frequently asked questions
How do I add a sidebar link?
Push an object onto a category's items (or navMain) in app/config/sidebar.ts with a title key, a url, and an imported Ph* icon. Add requires, roles, or orgRoles to make it conditional.
Why is a nav item not showing?
A gate failed - confirm every flag in requires is enabled and the session has a matching roles/orgRoles entry. For navbar items, check enabled !== false. Disabled-feature links are hidden by design.
Are navbar and sidebar config shared with other frontends?
No. navbar.ts, sidebar.ts, and user-menu.ts are per-app under apps/web-nuxt/app/config/. Only sectionTabsConfig is shared via @repo/config.
What's the difference between roles and orgRoles?
roles is the global app role; orgRoles is the user's role within their active organization. See Authorization.