Module Kit

Module Kit registries and menus

Let your module add menu items, registry entries and account data to other modules without importing them.

This page shows how your module adds to another module without importing it: menu items, entries in a registry, and data for an area's single bootstrap request. It is for module authors. Set up the module first: Overview.

Extension points explains when to use a registry and when a hook.

These helpers add items to a Menu without importing it.

HelperSignaturePurpose
addMenuSource(nuxt: Nuxt, options: MenuSourceOptions) => voidRegisters a menu source file. Duplicates are ignored.
addOptionalMenuSource(nuxt: Nuxt, options: MenuSourceOptions) => booleanThe same, only when Menu is installed. Returns whether it registered.
interface MenuSourceOptions {
  /** The menu the items belong to, for example `dashboard.account`. */
  menu: string
  /** Absolute path of the source file (a `defineMenuSource` default export). */
  file: string
}

addOptionalMenuSource(nuxt, {
  menu: 'dashboard.account',
  file: resolver.resolve('./runtime/menus'),
})

Menu reads the sources once every module is done. The source file imports @frigear-nu/menu/utils itself, which resolves only when the file runs.

Use addOptionalMenuSource, and declare @frigear-nu/menu as an optional moduleDependencies entry and an optional peer dependency. Your module then works without Menu.

Module Kit depends on neither Menu nor Widget. For widgets and tabs, guard the call in setup() yourself: check hasNuxtModule('@frigear-nu/widget'), then await import('@frigear-nu/widget/utils') and call extendWidgets or extendTabs.

Registries

defineVirtualRegistry lets your module collect entries from other modules at build time. Other modules register entries during their own setup(), in any order. Once every module is done (modules:done), the entries are bundled into one virtual module that your runtime code imports. Nothing is scanned at runtime, so it works on Cloudflare Workers. Call it from the setup() of the module that owns the registry.

function defineVirtualRegistry<T>(nuxt: Nuxt, options: VirtualRegistryOptions<T>): void

interface VirtualRegistryOptions<T> {
  /** Template file name, for example `#widgets/registry.mjs`. */
  filename: string
  /** `app` writes the template and aliases it; `server` adds a Nitro virtual module. Defaults to `app`. */
  target?: 'app' | 'server'
  /** The import path of the virtual module (`app` target only), for example `#widgets/registry`. */
  alias?: string
  /** Reads what modules registered during their own `setup()`. */
  collect: () => T
  /** The source of the virtual module. */
  contents: (data: T) => string
  /** Called with the collected data after the template is registered. */
  resolved?: (data: T) => void | Promise<void>
}

Bootstrap providers

A bootstrap provider adds your module's data to an area's single request, so your composable needs no request of its own. An area such as the account pages shows data from several modules for the signed-in user. Without providers, each module's composable makes its own request.

The area loads GET /api/auth/bootstrap?scope=<scope> before its pages set up. That request primes the useCachedFetch cache under each provider's key.

function registerBootstrapProvider(nuxt: Nuxt, provider: {
  /** The area, e.g. `account`. */
  scope: string
  /** The response key, and the `useCachedFetch` key your composable already uses for the same data. */
  key: string
  /** Server file whose default export is `(event: H3Event) => Promise<data>`; absolute or relative to rootDir. */
  handler: string
}): void

getBootstrapProviders(nuxt: Nuxt, scope?: string): BootstrapProvider[] // what is registered so far
defineBootstrapRegistry(nuxt: Nuxt): void // bundles them into `#bootstrap/providers` (called by @frigear-nu/auth)

Share one function between your endpoint and the provider, so both return the same shape. Events does it like this:

runtime/server/bootstrap/my-tickets.ts
export default async function myTickets(event: H3Event): Promise<TicketDto[]> {
  const { user } = await authorize(event, 'tickets:read')
  return listUserTickets(user.id)
}
runtime/server/routes/public/tickets-me.get.ts
import myTickets from '../../bootstrap/my-tickets'

export default defineEventHandler(myTickets)
module.ts
registerBootstrapProvider(nuxt, {
  scope: 'account',
  key: 'events:tickets:mine', // what useMyTickets() passes to useCachedFetch
  handler: resolver.resolve('./runtime/server/bootstrap/my-tickets'),
})

Your composable does not change. useCachedFetch finds the primed entry under its key, on the server, while hydrating and on client navigation.

How it fits together

PartBehaviour
The routeAuth serves it. It calls requireAuth once (authentication only), runs the scope's providers side by side and returns { [key]: data }.
Route errors401 when signed out, 400 without scope, 404 for a scope with no providers. The response is private, no-store.
AuthorizationThe principal is cached on the event. Call authorize(event, '<permission>') in the provider, so a token without the permission gets nothing, as on your own route.
FailuresA provider that throws is left out and logged. Your composable then fetches from your own endpoint.
The clientThe area's owner calls loadBootstrap(scope) from a route middleware. It refetches when the last load is over a minute old.
The account scopeAccount adds the middleware to every account page and resets it when the signed-in user changes.

Module Kit cannot serve the route itself, because Auth already depends on Module Kit. Today Subscriptions (subscriptions:me), Events (events:tickets:mine) and OAuth Server (oauth-server:mine) provide account data. One request for an area covers the client.

Providers share one response. A provider that sets a status or headers changes them for every other provider.
Register only data that a page of the area shows on first render. Data loaded later, and lists that grow without bound, belong in your module's own endpoint.

Next steps

  • Module Kit server toolkit – serve data from your own routes.
  • Menu – write the menu source file your module registers.
  • Widget – add widgets and tabs to another module's pages.
Copyright © 2026