A panel is an independently routed admin surface. definePanel(id) defaults to /<id> and returns an immutable builder: keep the returned value from every chain. You can create multiple panel modules for different domains, guards or audiences.
import { definePanel, nav } from '@rikology/pupitre'
export default definePanel('staff')
.path('/staff')
.domain('staff.example.com')
.guard('web')
.brand({ name: 'Operations' })
.timezone('Asia/Jakarta')
.navigation([nav.group('Content').items(['posts'])])
.notifications({ interval: 30_000 })
.globalSearch({ keyBinding: 'mod+k' })Complete builder surface
| Method | Purpose |
|---|---|
path(string), domain(string) |
URL prefix and optional host restriction |
guard(string) |
Host authentication guard |
middleware(array) |
Panel middleware, in declaration order |
resources(arrayOrRecord), pages(arrayOrRecord) |
Register definitions/builders or discovered modules |
brand(object) |
Name, logos and favicon; exact fields in panel reference |
theme(object) |
Validated theme |
timezone(string) |
Panel display/parse timezone |
locale('en') |
English, the supported locale |
breadcrumbs(boolean) |
Disable or enable generated breadcrumbs |
login(enabled?), registration(enabled?) |
Built-in sign-in and sign-up |
passwordReset(enabled?), emailVerification(enabled?) |
Mail-backed reset/verification |
| `mfa({ required?, issuer? } | false)` |
passkeys(enabled?) |
WebAuthn login/profile management |
| `profile(boolean | { avatar? })` |
| `social(string[] | { providers, registration? })` |
navigation(entries) |
Explicit nav.group() / nav.item() tree |
userMenu(items) |
Host-defined account menu links/actions |
| `tenancy(options | false)` |
| `notifications({ interval? } | false)` |
| `globalSearch({ keyBinding? } | false)` |
widgets(arrayOrRecord) |
Dashboard widgets |
filters(definitions) |
Dashboard filter definitions |
plugins(plugin[]) |
Ordered plugin list; repeated calls replace the list |
toDefinition(), toJSON() |
Inspect the definition or deterministic serialized representation |
strictAuthorization(enabled?) |
Require resource/related-model policies at boot and deny missing policies at runtime |
environmentIndicator(options?) |
Opt-in environment badge and top strip; hidden in production by default |
Auth methods that take an optional boolean enable their feature when called without arguments. Built-in auth is disabled initially. Notifications are disabled initially; global search is enabled but only resources declaring searchable fields participate.
Environment indicator
Call .environmentIndicator() to show the Adonis application environment name outside production. The shared pupitre.environment prop is omitted when hidden. Options are { visible?: boolean | ((ctx) => boolean | Promise<boolean>), label?: string, color?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'gray' }; color defaults to warning. Explicit visibility or an async callback overrides the production default.
definePanel('admin').environmentIndicator({
label: 'Staging',
color: 'info',
visible: async (ctx) => ctx.auth.user?.email === 'admin@example.com',
})The badge includes text and an accessible environment title; the thin shell-top strip is decorative. It works with expanded/collapsed sidebars and top navigation.
Navigation and menus
Without explicit navigation, resources and pages contribute their metadata. Resource .navigation({ group, sort, badge }) and page .navigation(...) control placement; badges are server callbacks. Bouncer viewAny, page canAccess, and visibility checks filter entries. Nested resources do not get top-level navigation entries.
Use nav.group(label).items([...]) for ordered groups and nav.item(label) for host links. The generated navigation reference lists exact link, icon, sort, visibility and collapsibility signatures. userMenu() items support server visibility, sorting, methods and external destinations.
Plugins append their navigation contributions after host contributions; equal group labels merge. See plugin ordering before combining explicit and derived navigation.