GenerateSaaS

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.

FileExportRenders
app/config/navbar.tsnavbarItemsMarketing site top nav
app/config/sidebar.tssidebarConfigDashboard sidebar
app/config/user-menu.tsuserMenuItemsAvatar dropdown
@repo/config section-tabs.tssectionTabsConfigSub-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.

FieldTypeEffect
requiresSidebarFeatureFlag[]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.

FlagEnabled when
multiTenantconfig.tenancy.multiTenant
notificationsconfig.notifications.enabled
apiKeysconfig.apiKeys.enabled
creditsconfig.payment.enabled and pricingConfig.credits.enabled
aiconfig.ai.enabled
aiByokconfig.ai.enabled and config.ai.byok (users may bring their own keys)
companionconfig.companion.enabled
mcpServerconfig.mcpServer.enabled
emailTrackingconfig.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.

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"] },

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 to icon.
  • 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.

On this page