Interface

Admin

Gives your staff one guarded admin area, where every installed module adds its own management pages.

Admin gives your staff one place to manage the app: a guarded area under /admin with a sidebar, a search palette and a dashboard. It has no features of its own. Users, Events, Forms and other modules add their pages, sidebar entries and dashboard cards when it is installed. Install it when staff need management screens; for a settings area of your own, use the App shell directly.

The package is @frigear-nu/admin. It draws the admin area from the components of UI, the sidebar from Menu and the dashboard from Widget.

What you get

  • Pages (on by default): a dashboard at /admin, and the admin layout for every page under /admin.
  • Admin area (on by default): a sidebar in three groups, a ⌘K palette and the account menu in the sidebar footer.
  • Access (on with Authorization): only the admin role can open the area.
  • Account menu (on by default): an "Admin" entry for users who may open the area.
  • Helpers for module authors: extendAdminNav, extendAdminDashboard and adminRoute.
ServerClientMobile
Installed for you:@frigear-nu/module-kit, UI, Menu, Widget and nuxt-auth-utils.You add:@nuxt/ui.Optional:@frigear-nu/authorization (guards the area by permission) and @frigear-nu/account (the account menu in the sidebar footer).

Quick setup

Install Admin

pnpm add @frigear-nu/admin @nuxt/ui @frigear-nu/authorization

Add it to nuxt.config.ts

Without Authorization, nothing guards the admin pages in the browser, and the build logs a warning. The server routes behind them stay guarded by the modules that own them.
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/ui', '@frigear-nu/authorization', '@frigear-nu/admin'],
  admin: {
    settings: {
      permission: 'admin:access', // the default
    },
  },
  authorization: {
    roles: {
      // `admin` already holds `admin:access`. Give it to a role of your own too:
      manager: { label: 'Coordinator', grant: ['admin:access', 'forms:*', 'users:read', 'users:list'] },
    },
  },
})

Give yourself a role that holds the permission

Only users who hold admin:access can open the area. The module registers it and grants it to the admin role. In the starter, NUXT_ACL_ADMINS lists the emails that get the admin role. See Roles and the first admin.

admin:access only opens the pages. The data behind them checks its own permissions, such as forms:manage, so a role that holds admin:access alone sees an empty area.

What you should see: signed in with a role that holds admin:access, /admin shows the dashboard with the sidebar, and the account menu has an "Admin" entry. Signed in without it, /admin shows a 403 error page and the entry is hidden. Signed out, you are sent to authorization.redirectTo, or get a 401 when it is unset.

Options

Options go under the admin key.

OptionTypeDefaultPurpose
settings.basestring'/admin'The path prefix of the area. A trailing slash is trimmed.
settings.dashboardboolean | stringtrueThe dashboard page at base. false turns it off; a string is a custom path.
settings.permissionstring | false'admin:access'The permission needed to enter the area. false sets no guard.
settings.layoutbooleantrueGives every page under base the admin layout.
settings.backTostring | false'/'Target of the sidebar's "Back to the site" link. false hides it.

settings.dashboard. With false, other modules' pages under base still get the layout.

settings.permission. It becomes a route rule { permission } on <base>/**, when Authorization is installed. The default admin:access is registered by Admin and granted to the admin role. A custom permission must be registered by your app, and given to the roles that may enter. With false, guard the area yourself. A rule of your own for that path wins.

settings.layout. It sets a route rule { appLayout: 'admin' } on <base> and <base>/**, unless a page picks its own layout. Your own app/layouts/admin.vue, in the app or a layer, replaces the module's. false registers no layout and sets no rule.

settings.backTo. A path gets the locale prefix. An absolute URL to another host is used as is.

With @nuxtjs/i18n, every rule is repeated for each /<locale> prefix. The "Admin" entry in the account menu is added only when dashboard is on and the area is guarded (Authorization is installed and permission is not false). The entry itself carries the permission, so it is hidden from users who cannot open the area.

Runtime config

runtimeConfig.public.admin holds the resolved values. Set NUXT_PUBLIC_ADMIN_BACK_TO to change backTo per deploy; an empty value hides the link.

type PublicModuleOptions = {
  base: string
  dashboard?: string // undefined when the dashboard is off
  backTo?: string | false
}

App

Add a page to the sidebar

The sidebar is three groups, each read from its own menu path. Which group a page belongs in is explained on Layouts and navigation.

A module never imports Admin at runtime. It adds its entry to the group's menu path, which works whether or not Admin is installed. For an entry that needs runtime config, register a runtime menu file:

// module.ts
import { addOptionalMenuSource } from '@frigear-nu/module-kit/utils'

addOptionalMenuSource(nuxt, { file: resolver.resolve('./runtime/menus'), menu: 'admin' })
// runtime/menus.ts
import { defineMenuItem, defineMenuSource, extendMenuItem } from '@frigear-nu/menu/utils'

export default defineMenuSource(() => {
  const config = useRuntimeConfig().public.forms

  // Manage group (the root of `admin.nav`)
  extendMenuItem('admin.nav', defineMenuItem({
    id: 'forms-admin',
    label: 'Forms',
    labelKey: 'forms.menu.admin.label',
    icon: 'i-lucide-clipboard-list',
    to: config.pages.admin,
    order: 30,
  }))

  // Settings group
  extendMenuItem('admin.nav.settings', defineMenuItem({
    id: 'forms-settings',
    label: 'Form settings',
    icon: 'i-lucide-settings-2',
    to: '/admin/forms/settings',
  }))
})

For entries known at build time, call the helpers from @frigear-nu/admin/utils in setup():

import { extendAdminNav, extendAdminDashboard, adminRoute } from '@frigear-nu/admin/utils'

extendAdminNav(nuxt, { label: 'Forms', to: adminRoute(nuxt, 'forms'), icon: 'i-lucide-clipboard-list', order: 20 })
extendAdminNav(nuxt, { label: 'OAuth applications', to: adminRoute(nuxt, 'oauth'), icon: 'i-lucide-key-round' }, 'settings')
extendAdminDashboard(nuxt, { component: resolver.resolve('./runtime/app/widgets/Submissions.vue'), order: 10 })
extendAdminDashboard(nuxt, { component: resolver.resolve('./runtime/app/widgets/SubmissionCount.vue'), order: 10 }, 'top')
ExportSignaturePurpose
extendAdminNav(nuxt, items: MenuItem | MenuItem[], group?: 'overview' | 'manage' | 'settings')Adds sidebar entries. group defaults to 'manage'.
extendAdminDashboard(nuxt, widgets: WidgetDefinition | WidgetDefinition[], position?: 'top' | 'main' | 'side')Adds dashboard widgets. position defaults to 'main'.
adminRoute(nuxt, ...segments: string[]) => stringA path below the configured base: adminRoute(nuxt, 'events', ':id') is /admin/events/:id.
ADMIN_NAV_GROUPS{ overview, manage, settings }The menu path of each group: admin.nav.overview, admin.nav and admin.nav.settings.
ADMIN_NAV_MENU'admin.nav'The menu of the Manage group.
ADMIN_DASHBOARD_AREA, ADMIN_DASHBOARD_TOP_AREA, ADMIN_DASHBOARD_SIDE_AREA'admin.dashboard', '.top', '.side'The three dashboard widget areas.

Use adminRoute so a module's default paths follow the host's base. A module can also call extendMenu or extendWidgets directly, without depending on Admin.

Dashboard

The dashboard is three widget areas, so modules fill it without Admin knowing them:

Widget areapositionLayoutPut here
admin.dashboard.top'top'A row of up to four cardsOne StatCard per widget.
admin.dashboard'main'A wide column, full width when the side column is emptyLists and tables, such as a ListCard.
admin.dashboard.side'side'A narrow column next to itShort ListCards.

Widgets load their own data with useLazyFetch and receive no context. When no module adds a widget, the dashboard shows a shortcut card for every entry of the Manage and Settings groups.

Layout

The admin layout feeds the admin menus to the shared App shell, which draws the chrome. The account area uses the same shell.

  • Search. The ⌘K palette has one group per sidebar group, so everything in the sidebar is searchable.
  • Account menu. AccountMenu from Account sits in the sidebar footer, when installed.
  • Overrides. AccountMenu and LanguageSelect are resolved by name, so your own versions win and neither is required.

What the sidebar, navbar and width look like is on Layouts and navigation. The dashboard page (admin-dashboard) is skipped when your app already has a page at its path.

Components

Admin pages are built from the page components of UI, following the UX patterns. A module that ships admin pages declares Admin as an optional module dependency and UI as a dependency.

The old Admin* components have been removed. Replace them like this:

RemovedUse instead
AdminPage<div class="flex flex-col gap-4">
AdminPageHeaderPageHeading: back becomes breadcrumb, the links slot becomes actions
AdminSection, AdminToolbarSectionCard, PageToolbar
AdminEmpty<UEmpty variant="naked">
AdminTableTableCard around a UTable
AdminStatCard, AdminListCardStatCard, ListCard
AdminKeyValueDescriptionList; a missing value reads "Not set" instead of a dash

Translations

Override any string in your app's i18n/locales/<code>.json. The namespace is admin, in English and Danish.

KeyUsed for
admin.titleThe brand label and the start of the navbar line
admin.nav.labelThe sidebar navigation's accessible name
admin.nav.groups.overview, .manage, .settingsThe names of the sidebar groups
admin.search.goToThe title of the ⌘K palette groups
admin.dashboard.title, .description, .emptyThe dashboard page
admin.backToSiteThe link back to the public site
admin.menu.label, .descriptionThe "Admin" entry in the account menu

Next steps

Copyright © 2026