[{"data":1,"prerenderedAt":2386},["ShallowReactive",2],{"navigation_docs":3,"-guide-concepts-extension-points":587,"-guide-concepts-extension-points-surround":2381},[4,50,155,441,504],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":49},"Get Started","i-lucide-rocket","\u002Fget-started","1.get-started",[10,15,20,25,30,35,40,45],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fget-started\u002Fintroduction","1.get-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Installation","\u002Fget-started\u002Finstallation","1.get-started\u002F2.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":24},"Project structure","\u002Fget-started\u002Fproject-structure","1.get-started\u002F3.project-structure","i-lucide-folder-tree",{"title":26,"path":27,"stem":28,"icon":29},"Configuration","\u002Fget-started\u002Fconfiguration","1.get-started\u002F4.configuration","i-lucide-settings",{"title":31,"path":32,"stem":33,"icon":34},"Authentication","\u002Fget-started\u002Fauthentication","1.get-started\u002F5.authentication","i-lucide-lock",{"title":36,"path":37,"stem":38,"icon":39},"Adding modules","\u002Fget-started\u002Fadding-modules","1.get-started\u002F6.adding-modules","i-lucide-puzzle",{"title":41,"path":42,"stem":43,"icon":44},"Database","\u002Fget-started\u002Fdatabase","1.get-started\u002F7.database","i-lucide-database",{"title":46,"path":47,"stem":48,"icon":6},"Deployment","\u002Fget-started\u002Fdeployment","1.get-started\u002F8.deployment",false,{"title":51,"icon":52,"path":53,"stem":54,"children":55,"page":49},"Guide","i-lucide-book-open","\u002Fguide","2.guide",[56,90,115,125],{"title":57,"icon":49,"path":58,"stem":59,"children":60,"page":49},"Concepts","\u002Fguide\u002Fconcepts","2.guide\u002F1.concepts",[61,66,71,76,80,85],{"title":62,"path":63,"stem":64,"icon":65},"Architecture","\u002Fguide\u002Fconcepts\u002Farchitecture","2.guide\u002F1.concepts\u002F1.architecture","i-lucide-blocks",{"title":67,"path":68,"stem":69,"icon":70},"Access control","\u002Fguide\u002Fconcepts\u002Fauth-and-scopes","2.guide\u002F1.concepts\u002F2.auth-and-scopes","i-lucide-shield-check",{"title":72,"path":73,"stem":74,"icon":75},"Extension points","\u002Fguide\u002Fconcepts\u002Fextension-points","2.guide\u002F1.concepts\u002F3.extension-points","i-lucide-plug",{"title":77,"path":78,"stem":79,"icon":44},"Database extensions","\u002Fguide\u002Fconcepts\u002Fdatabase-extensions","2.guide\u002F1.concepts\u002F4.database-extensions",{"title":81,"path":82,"stem":83,"icon":84},"Identity","\u002Fguide\u002Fconcepts\u002Fidentity","2.guide\u002F1.concepts\u002F5.identity","i-lucide-fingerprint",{"title":86,"path":87,"stem":88,"icon":89},"Glossary","\u002Fguide\u002Fconcepts\u002Fglossary","2.guide\u002F1.concepts\u002F6.glossary","i-lucide-book-a",{"title":91,"icon":49,"path":92,"stem":93,"children":94,"page":49},"UX patterns","\u002Fguide\u002Fux-patterns","2.guide\u002F2.ux-patterns",[95,100,105,110],{"title":96,"path":97,"stem":98,"icon":99},"Layouts and navigation","\u002Fguide\u002Fux-patterns\u002Flayouts-and-navigation","2.guide\u002F2.ux-patterns\u002F1.layouts-and-navigation","i-lucide-layout-template",{"title":101,"path":102,"stem":103,"icon":104},"Pages","\u002Fguide\u002Fux-patterns\u002Fpages","2.guide\u002F2.ux-patterns\u002F2.pages","i-lucide-table",{"title":106,"path":107,"stem":108,"icon":109},"Forms and actions","\u002Fguide\u002Fux-patterns\u002Fforms-and-actions","2.guide\u002F2.ux-patterns\u002F3.forms-and-actions","i-lucide-square-pen",{"title":111,"path":112,"stem":113,"icon":114},"Text and formatting","\u002Fguide\u002Fux-patterns\u002Ftext-and-formatting","2.guide\u002F2.ux-patterns\u002F4.text-and-formatting","i-lucide-type",{"title":116,"icon":49,"path":117,"stem":118,"children":119,"page":49},"Best practices","\u002Fguide\u002Fbest-practices","2.guide\u002F3.best-practices",[120],{"title":121,"path":122,"stem":123,"icon":124},"HTTP routes","\u002Fguide\u002Fbest-practices\u002Fhttp-routes","2.guide\u002F3.best-practices\u002F1.http-routes","i-lucide-route",{"title":126,"icon":49,"path":127,"stem":128,"children":129,"page":49},"Going further","\u002Fguide\u002Fgoing-further","2.guide\u002F4.going-further",[130,135,140,145,150],{"title":131,"path":132,"stem":133,"icon":134},"Use your own OAuth server","\u002Fguide\u002Fgoing-further\u002Fown-oauth-server","2.guide\u002F4.going-further\u002F1.own-oauth-server","i-lucide-key-round",{"title":136,"path":137,"stem":138,"icon":139},"Create an OAuth application","\u002Fguide\u002Fgoing-further\u002Foauth-application","2.guide\u002F4.going-further\u002F2.oauth-application","i-lucide-app-window",{"title":141,"path":142,"stem":143,"icon":144},"Signing keys","\u002Fguide\u002Fgoing-further\u002Fsigning-keys","2.guide\u002F4.going-further\u002F3.signing-keys","i-lucide-key-square",{"title":146,"path":147,"stem":148,"icon":149},"Stripe setup","\u002Fguide\u002Fgoing-further\u002Fstripe-setup","2.guide\u002F4.going-further\u002F4.stripe-setup","i-lucide-credit-card",{"title":151,"path":152,"stem":153,"icon":154},"Migrate to the permission model","\u002Fguide\u002Fgoing-further\u002Fmigrate-access-control","2.guide\u002F4.going-further\u002F5.migrate-access-control","i-lucide-arrow-right-left",{"title":156,"icon":157,"path":158,"stem":159,"children":160},"Modules","i-lucide-boxes","\u002Fmodules","3.modules",[161,163,244,273,320,375],{"title":156,"path":158,"stem":162},"3.modules\u002Findex",{"title":81,"icon":49,"path":164,"stem":165,"children":166,"page":49},"\u002Fmodules\u002Fidentity","3.modules\u002F1.identity",[167,190,194,198,220,224,240],{"title":168,"path":169,"stem":170,"children":171},"Auth","\u002Fmodules\u002Fidentity\u002Fauth","3.modules\u002F1.identity\u002F1.auth\u002Findex",[172,174,178,182,186],{"title":173,"path":169,"stem":170},"Overview",{"title":175,"path":176,"stem":177},"Server API","\u002Fmodules\u002Fidentity\u002Fauth\u002Fserver","3.modules\u002F1.identity\u002F1.auth\u002F2.server",{"title":179,"path":180,"stem":181},"Reference","\u002Fmodules\u002Fidentity\u002Fauth\u002Freference","3.modules\u002F1.identity\u002F1.auth\u002F3.reference",{"title":183,"path":184,"stem":185},"Client mode","\u002Fmodules\u002Fidentity\u002Fauth\u002Fclient-mode","3.modules\u002F1.identity\u002F1.auth\u002F4.client-mode",{"title":187,"path":188,"stem":189},"OAuth providers","\u002Fmodules\u002Fidentity\u002Fauth\u002Foauth-providers","3.modules\u002F1.identity\u002F1.auth\u002F5.oauth-providers",{"title":191,"path":192,"stem":193},"Users","\u002Fmodules\u002Fidentity\u002Fusers","3.modules\u002F1.identity\u002F2.users",{"title":195,"path":196,"stem":197},"Account","\u002Fmodules\u002Fidentity\u002Faccount","3.modules\u002F1.identity\u002F3.account",{"title":199,"path":200,"stem":201,"children":202},"Authorization","\u002Fmodules\u002Fidentity\u002Fauthorization","3.modules\u002F1.identity\u002F4.authorization\u002Findex",[203,204,208,212,216],{"title":173,"path":200,"stem":201},{"title":205,"path":206,"stem":207},"Roles and requirements","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Froles-and-requirements","3.modules\u002F1.identity\u002F4.authorization\u002F2.roles-and-requirements",{"title":209,"path":210,"stem":211},"Policies","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Fpolicies","3.modules\u002F1.identity\u002F4.authorization\u002F3.policies",{"title":213,"path":214,"stem":215},"Client","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Fclient","3.modules\u002F1.identity\u002F4.authorization\u002F4.client",{"title":217,"path":218,"stem":219},"Admin","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Fadmin","3.modules\u002F1.identity\u002F4.authorization\u002F5.admin",{"title":221,"path":222,"stem":223},"Preferences","\u002Fmodules\u002Fidentity\u002Fpreferences","3.modules\u002F1.identity\u002F5.preferences",{"title":225,"path":226,"stem":227,"children":228},"OAuth Server","\u002Fmodules\u002Fidentity\u002Foauth-server","3.modules\u002F1.identity\u002F6.oauth-server\u002Findex",[229,230,233,237],{"title":173,"path":226,"stem":227},{"title":175,"path":231,"stem":232},"\u002Fmodules\u002Fidentity\u002Foauth-server\u002Fserver","3.modules\u002F1.identity\u002F6.oauth-server\u002F2.server",{"title":234,"path":235,"stem":236},"Hooks","\u002Fmodules\u002Fidentity\u002Foauth-server\u002Fhooks","3.modules\u002F1.identity\u002F6.oauth-server\u002F3.hooks",{"title":179,"path":238,"stem":239},"\u002Fmodules\u002Fidentity\u002Foauth-server\u002Froutes-and-clients","3.modules\u002F1.identity\u002F6.oauth-server\u002F4.routes-and-clients",{"title":241,"path":242,"stem":243},"OAuth Resource","\u002Fmodules\u002Fidentity\u002Foauth-resource","3.modules\u002F1.identity\u002F8.oauth-resource",{"title":245,"icon":49,"path":246,"stem":247,"children":248,"page":49},"Commerce","\u002Fmodules\u002Fcommerce","3.modules\u002F2.commerce",[249,253,257],{"title":250,"path":251,"stem":252},"Stripe","\u002Fmodules\u002Fcommerce\u002Fstripe","3.modules\u002F2.commerce\u002F1.stripe",{"title":254,"path":255,"stem":256},"Payments","\u002Fmodules\u002Fcommerce\u002Fpayments","3.modules\u002F2.commerce\u002F2.payments",{"title":258,"path":259,"stem":260,"children":261},"Subscriptions","\u002Fmodules\u002Fcommerce\u002Fsubscriptions","3.modules\u002F2.commerce\u002F3.subscriptions\u002Findex",[262,263,266,270],{"title":173,"path":259,"stem":260},{"title":175,"path":264,"stem":265},"\u002Fmodules\u002Fcommerce\u002Fsubscriptions\u002Fserver","3.modules\u002F2.commerce\u002F3.subscriptions\u002F2.server",{"title":267,"path":268,"stem":269},"App","\u002Fmodules\u002Fcommerce\u002Fsubscriptions\u002Fapp","3.modules\u002F2.commerce\u002F3.subscriptions\u002F3.app",{"title":179,"path":271,"stem":272},"\u002Fmodules\u002Fcommerce\u002Fsubscriptions\u002Freference","3.modules\u002F2.commerce\u002F3.subscriptions\u002F4.reference",{"title":274,"icon":49,"path":275,"stem":276,"children":277,"page":49},"Features","\u002Fmodules\u002Ffeatures","3.modules\u002F3.features",[278,293,312,316],{"title":279,"path":280,"stem":281,"children":282},"Events","\u002Fmodules\u002Ffeatures\u002Fevents","3.modules\u002F3.features\u002F1.events\u002Findex",[283,284,287,290],{"title":173,"path":280,"stem":281},{"title":175,"path":285,"stem":286},"\u002Fmodules\u002Ffeatures\u002Fevents\u002Fserver","3.modules\u002F3.features\u002F1.events\u002F2.server",{"title":267,"path":288,"stem":289},"\u002Fmodules\u002Ffeatures\u002Fevents\u002Fapp","3.modules\u002F3.features\u002F1.events\u002F3.app",{"title":179,"path":291,"stem":292},"\u002Fmodules\u002Ffeatures\u002Fevents\u002Freference","3.modules\u002F3.features\u002F1.events\u002F4.reference",{"title":294,"path":295,"stem":296,"children":297},"Forms","\u002Fmodules\u002Ffeatures\u002Fforms","3.modules\u002F3.features\u002F2.forms\u002Findex",[298,299,303,306,309],{"title":173,"path":295,"stem":296},{"title":300,"path":301,"stem":302},"Definition","\u002Fmodules\u002Ffeatures\u002Fforms\u002Fdefinition","3.modules\u002F3.features\u002F2.forms\u002F2.definition",{"title":175,"path":304,"stem":305},"\u002Fmodules\u002Ffeatures\u002Fforms\u002Fserver","3.modules\u002F3.features\u002F2.forms\u002F3.server",{"title":267,"path":307,"stem":308},"\u002Fmodules\u002Ffeatures\u002Fforms\u002Fapp","3.modules\u002F3.features\u002F2.forms\u002F4.app",{"title":179,"path":310,"stem":311},"\u002Fmodules\u002Ffeatures\u002Fforms\u002Freference","3.modules\u002F3.features\u002F2.forms\u002F5.reference",{"title":313,"path":314,"stem":315},"Cookie Consent","\u002Fmodules\u002Ffeatures\u002Fcookie-consent","3.modules\u002F3.features\u002F3.cookie-consent",{"title":317,"path":318,"stem":319},"MCP","\u002Fmodules\u002Ffeatures\u002Fmcp","3.modules\u002F3.features\u002F4.mcp",{"title":321,"icon":49,"path":322,"stem":323,"children":324,"page":49},"Interface","\u002Fmodules\u002Finterface","3.modules\u002F4.interface",[325,350,353,357,361],{"title":326,"path":327,"stem":328,"children":329},"UI","\u002Fmodules\u002Finterface\u002Fui","3.modules\u002F4.interface\u002F1.ui\u002Findex",[330,331,335,339,343,347],{"title":173,"path":327,"stem":328},{"title":332,"path":333,"stem":334},"Page components","\u002Fmodules\u002Finterface\u002Fui\u002Fpage-components","3.modules\u002F4.interface\u002F1.ui\u002F2.page-components",{"title":336,"path":337,"stem":338},"App shell","\u002Fmodules\u002Finterface\u002Fui\u002Fapp-shell","3.modules\u002F4.interface\u002F1.ui\u002F3.app-shell",{"title":340,"path":341,"stem":342},"Form inputs","\u002Fmodules\u002Finterface\u002Fui\u002Fform-inputs","3.modules\u002F4.interface\u002F1.ui\u002F4.form-inputs",{"title":344,"path":345,"stem":346},"Composables and helpers","\u002Fmodules\u002Finterface\u002Fui\u002Fcomposables","3.modules\u002F4.interface\u002F1.ui\u002F5.composables",{"title":179,"path":348,"stem":349},"\u002Fmodules\u002Finterface\u002Fui\u002Freference","3.modules\u002F4.interface\u002F1.ui\u002F6.reference",{"title":217,"path":351,"stem":352},"\u002Fmodules\u002Finterface\u002Fadmin","3.modules\u002F4.interface\u002F2.admin",{"title":354,"path":355,"stem":356},"Menu","\u002Fmodules\u002Finterface\u002Fmenu","3.modules\u002F4.interface\u002F3.menu",{"title":358,"path":359,"stem":360},"Widget","\u002Fmodules\u002Finterface\u002Fwidget","3.modules\u002F4.interface\u002F4.widget",{"title":362,"path":363,"stem":364,"children":365},"Mobile","\u002Fmodules\u002Finterface\u002Fmobile","3.modules\u002F4.interface\u002F5.mobile\u002Findex",[366,367,371],{"title":173,"path":363,"stem":364},{"title":368,"path":369,"stem":370},"CLI and devices","\u002Fmodules\u002Finterface\u002Fmobile\u002Fcli-and-devices","3.modules\u002F4.interface\u002F5.mobile\u002F2.cli-and-devices",{"title":372,"path":373,"stem":374},"Splash screen","\u002Fmodules\u002Finterface\u002Fmobile\u002Fsplash-screen","3.modules\u002F4.interface\u002F5.mobile\u002F3.splash-screen",{"title":376,"icon":49,"path":377,"stem":378,"children":379,"page":49},"Platform","\u002Fmodules\u002Fplatform","3.modules\u002F5.platform",[380,402,406,423],{"title":381,"path":382,"stem":383,"children":384},"Module Kit","\u002Fmodules\u002Fplatform\u002Fmodule-kit","3.modules\u002F5.platform\u002F1.module-kit\u002Findex",[385,386,390,394,398],{"title":173,"path":382,"stem":383},{"title":387,"path":388,"stem":389},"Server toolkit","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fserver-toolkit","3.modules\u002F5.platform\u002F1.module-kit\u002F2.server-toolkit",{"title":391,"path":392,"stem":393},"Registries and menus","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fregistries-and-menus","3.modules\u002F5.platform\u002F1.module-kit\u002F3.registries-and-menus",{"title":395,"path":396,"stem":397},"Translations","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fi18n","3.modules\u002F5.platform\u002F1.module-kit\u002F4.i18n",{"title":399,"path":400,"stem":401},"Database helpers","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fdatabase","3.modules\u002F5.platform\u002F1.module-kit\u002F5.database",{"title":403,"path":404,"stem":405},"Notify","\u002Fmodules\u002Fplatform\u002Fnotify","3.modules\u002F5.platform\u002F2.notify",{"title":407,"path":408,"stem":409,"children":410},"Email Templates","\u002Fmodules\u002Fplatform\u002Femail-templates","3.modules\u002F5.platform\u002F3.email-templates\u002Findex",[411,412,416,419],{"title":173,"path":408,"stem":409},{"title":413,"path":414,"stem":415},"Email components","\u002Fmodules\u002Fplatform\u002Femail-templates\u002Fcomponents","3.modules\u002F5.platform\u002F3.email-templates\u002F2.components",{"title":175,"path":417,"stem":418},"\u002Fmodules\u002Fplatform\u002Femail-templates\u002Fserver","3.modules\u002F5.platform\u002F3.email-templates\u002F3.server",{"title":420,"path":421,"stem":422},"Module emails","\u002Fmodules\u002Fplatform\u002Femail-templates\u002Fmodule-emails","3.modules\u002F5.platform\u002F3.email-templates\u002F4.module-emails",{"title":424,"path":425,"stem":426,"children":427},"Base Layer","\u002Fmodules\u002Fplatform\u002Fbase-layer","3.modules\u002F5.platform\u002F4.base-layer\u002Findex",[428,429,433,437],{"title":173,"path":425,"stem":426},{"title":430,"path":431,"stem":432},"Content collections","\u002Fmodules\u002Fplatform\u002Fbase-layer\u002Fcontent-collections","3.modules\u002F5.platform\u002F4.base-layer\u002F2.content-collections",{"title":434,"path":435,"stem":436},"Layouts and routing","\u002Fmodules\u002Fplatform\u002Fbase-layer\u002Flayouts-and-routing","3.modules\u002F5.platform\u002F4.base-layer\u002F3.layouts-and-routing",{"title":438,"path":439,"stem":440},"Composables and utils","\u002Fmodules\u002Fplatform\u002Fbase-layer\u002Fcomposables-and-utils","3.modules\u002F5.platform\u002F4.base-layer\u002F4.composables-and-utils",{"title":442,"icon":443,"path":444,"stem":445,"children":446,"page":49},"Examples","i-lucide-code-xml","\u002Fexamples","4.examples",[447,466,476],{"title":448,"icon":49,"path":449,"stem":450,"children":451,"page":49},"Starters","\u002Fexamples\u002Fstarters","4.examples\u002F1.starters",[452,456,461],{"title":453,"path":454,"stem":455,"icon":6},"Workspace starter","\u002Fexamples\u002Fstarters\u002Fworkspace","4.examples\u002F1.starters\u002F1.workspace",{"title":457,"path":458,"stem":459,"icon":460},"Mobile starter","\u002Fexamples\u002Fstarters\u002Fmobile","4.examples\u002F1.starters\u002F2.mobile","i-lucide-smartphone",{"title":462,"path":463,"stem":464,"icon":465},"Standalone mobile starter","\u002Fexamples\u002Fstarters\u002Fmobile-standalone","4.examples\u002F1.starters\u002F3.mobile-standalone","i-lucide-tablet-smartphone",{"title":467,"icon":49,"path":468,"stem":469,"children":470,"page":49},"Apps","\u002Fexamples\u002Fapps","4.examples\u002F2.apps",[471],{"title":472,"path":473,"stem":474,"icon":475},"Frigear website app","\u002Fexamples\u002Fapps\u002Ffrigear-nu-website","4.examples\u002F2.apps\u002F1.frigear-nu-website","i-lucide-layout-dashboard",{"title":477,"icon":49,"path":478,"stem":479,"children":480,"page":49},"Tasks","\u002Fexamples\u002Ftasks","4.examples\u002F3.tasks",[481,485,489,494,499],{"title":482,"path":483,"stem":484,"icon":70},"Protect a route","\u002Fexamples\u002Ftasks\u002Fprotect-a-route","4.examples\u002F3.tasks\u002F1.protect-a-route",{"title":486,"path":487,"stem":488,"icon":475},"Add an admin page","\u002Fexamples\u002Ftasks\u002Fadd-an-admin-page","4.examples\u002F3.tasks\u002F2.add-an-admin-page",{"title":490,"path":491,"stem":492,"icon":493},"Sell event tickets","\u002Fexamples\u002Ftasks\u002Fsell-event-tickets","4.examples\u002F3.tasks\u002F3.sell-event-tickets","i-lucide-ticket",{"title":495,"path":496,"stem":497,"icon":498},"Build a form","\u002Fexamples\u002Ftasks\u002Fbuild-a-form","4.examples\u002F3.tasks\u002F4.build-a-form","i-lucide-clipboard-list",{"title":500,"path":501,"stem":502,"icon":503},"Send an email","\u002Fexamples\u002Ftasks\u002Fsend-an-email","4.examples\u002F3.tasks\u002F5.send-an-email","i-lucide-mail",{"title":505,"icon":506,"path":507,"stem":508,"children":509,"page":49},"Contribute","i-lucide-git-pull-request","\u002Fcontribute","5.contribute",[510,513,518,544,549,552,556,571],{"title":173,"path":511,"stem":512,"icon":506},"\u002Fcontribute\u002Foverview","5.contribute\u002F1.overview",{"title":514,"path":515,"stem":516,"icon":517},"Local development","\u002Fcontribute\u002Flocal-development","5.contribute\u002F2.local-development","i-lucide-terminal",{"title":519,"icon":49,"path":520,"stem":521,"children":522,"page":49},"Writing a module","\u002Fcontribute\u002Fwriting-a-module","5.contribute\u002F3.writing-a-module",[523,527,532,536,541],{"title":524,"path":525,"stem":526,"icon":39},"Scaffold a module","\u002Fcontribute\u002Fwriting-a-module\u002Fscaffold","5.contribute\u002F3.writing-a-module\u002F1.scaffold",{"title":528,"path":529,"stem":530,"icon":531},"Setup checklist","\u002Fcontribute\u002Fwriting-a-module\u002Fsetup-checklist","5.contribute\u002F3.writing-a-module\u002F2.setup-checklist","i-lucide-list-checks",{"title":533,"path":534,"stem":535,"icon":44},"Database tables","\u002Fcontribute\u002Fwriting-a-module\u002Fdatabase-tables","5.contribute\u002F3.writing-a-module\u002F3.database-tables",{"title":537,"path":538,"stem":539,"icon":540},"Testing","\u002Fcontribute\u002Fwriting-a-module\u002Ftesting","5.contribute\u002F3.writing-a-module\u002F4.testing","i-lucide-flask-conical",{"title":67,"path":542,"stem":543,"icon":70},"\u002Fcontribute\u002Fwriting-a-module\u002Faccess-control","5.contribute\u002F3.writing-a-module\u002F5.access-control",{"title":545,"path":546,"stem":547,"icon":548},"Publishing","\u002Fcontribute\u002Fpublishing","5.contribute\u002F4.publishing","i-lucide-package-check",{"title":448,"path":550,"stem":551,"icon":6},"\u002Fcontribute\u002Fstarters","5.contribute\u002F5.starters",{"title":553,"path":554,"stem":555,"icon":52},"Documentation","\u002Fcontribute\u002Fdocumentation","5.contribute\u002F6.documentation",{"title":557,"icon":49,"path":558,"stem":559,"children":560,"page":49},"Tooling","\u002Fcontribute\u002Ftooling","5.contribute\u002F7.tooling",[561,566],{"title":562,"path":563,"stem":564,"icon":565},"CLI","\u002Fcontribute\u002Ftooling\u002Fcli","5.contribute\u002F7.tooling\u002F1.cli","i-lucide-square-terminal",{"title":567,"path":568,"stem":569,"icon":570},"ESLint config","\u002Fcontribute\u002Ftooling\u002Feslint-config","5.contribute\u002F7.tooling\u002F2.eslint-config","i-lucide-spell-check",{"title":572,"icon":49,"path":573,"stem":574,"children":575},"Decisions","\u002Fcontribute\u002Fdecisions","5.contribute\u002F8.decisions\u002Findex",[576,578,583],{"title":572,"path":573,"stem":574,"icon":577},"i-lucide-scale",{"title":579,"path":580,"stem":581,"icon":582},"Session lifetimes per OAuth client","\u002Fcontribute\u002Fdecisions\u002Foauth-token-lifetimes","5.contribute\u002F8.decisions\u002F1.oauth-token-lifetimes","i-lucide-timer",{"title":584,"path":585,"stem":586,"icon":70},"One permission model","\u002Fcontribute\u002Fdecisions\u002Fpermission-model","5.contribute\u002F8.decisions\u002F2.permission-model",{"id":588,"title":72,"body":589,"description":2374,"extension":2375,"links":2376,"meta":2377,"navigation":2378,"path":73,"seo":2379,"stem":74,"__hash__":2380},"docs\u002F2.guide\u002F1.concepts\u002F3.extension-points.md",{"type":590,"value":591,"toc":2357},"minimark",[592,596,615,620,802,806,842,846,849,908,914,929,934,944,961,964,979,983,986,1097,1122,1132,1270,1273,1302,1308,1311,1441,1450,1453,1466,1493,1658,1665,1680,1683,1688,1742,1745,1795,1944,1954,1958,1963,1994,1997,2017,2123,2163,2166,2174,2285,2292,2295,2307,2310,2314,2320,2332,2336,2353],[593,594,595],"p",{},"Your module has a Billing page. It should appear in the account menu, put a widget on the admin dashboard and offer a\ntool to AI agents, without you editing the Account, Admin or MCP modules. After this page you can decide which extension\npoint to use for each of those, and where its full reference lives.",[597,598,600,604,605,610,611,614],"callout",{"icon":599},"i-lucide-users",[601,602,603],"strong",{},"For:"," module authors. App developers can read ",[606,607,609],"a",{"href":608},"#widget-areas-and-tabs","Override a module's widget"," and\n",[606,612,234],{"href":613},"#hooks-ask-the-host-app",".",[616,617,619],"h2",{"id":618},"which-one-do-i-use","Which one do I use?",[621,622,623,639],"table",{},[624,625,626],"thead",{},[627,628,629,633,636],"tr",{},[630,631,632],"th",{},"You want to",[630,634,635],{},"Use",[630,637,638],{},"Read",[640,641,642,660,680,695,709,723,742,756,772,788],"tbody",{},[627,643,644,648,654],{},[645,646,647],"td",{},"Add a link to a menu, such as the account menu",[645,649,650],{},[651,652,653],"code",{},"extendMenu",[645,655,656],{},[606,657,659],{"href":658},"#menus","Menus",[627,661,662,665,674],{},[645,663,664],{},"Add a link to the admin sidebar or a card to the admin dashboard",[645,666,667,670,671],{},[651,668,669],{},"extendAdminNav",", ",[651,672,673],{},"extendAdminDashboard",[645,675,676],{},[606,677,679],{"href":678},"#admin-navigation-and-dashboard","Admin navigation and dashboard",[627,681,682,685,690],{},[645,683,684],{},"Show a component in a named slot, such as the account overview",[645,686,687],{},[651,688,689],{},"extendWidgets",[645,691,692],{},[606,693,694],{"href":608},"Widget areas and tabs",[627,696,697,700,705],{},[645,698,699],{},"Add a tab to a detail page another module owns",[645,701,702],{},[651,703,704],{},"extendTabs",[645,706,707],{},[606,708,694],{"href":608},[627,710,711,714,719],{},[645,712,713],{},"Let the host app override who may do something",[645,715,716],{},[651,717,718],{},"authorization:before",[645,720,721],{},[606,722,234],{"href":613},[627,724,725,728,736],{},[645,726,727],{},"Say what your routes need, and who holds it by default",[645,729,730,670,733],{},[651,731,732],{},"addPermission",[651,734,735],{},"addRole",[645,737,738],{},[606,739,741],{"href":740},"#auth-extension-points","Auth extension points",[627,743,744,747,752],{},[645,745,746],{},"Let users give a token access to your routes",[645,748,749],{},[651,750,751],{},"addScope",[645,753,754],{},[606,755,741],{"href":740},[627,757,758,761,766],{},[645,759,760],{},"Offer tools to AI agents",[645,762,763],{},[651,764,765],{},"addMcpDefinitions",[645,767,768],{},[606,769,771],{"href":770},"#mcp-tools","MCP tools",[627,773,774,777,782],{},[645,775,776],{},"Load your data in one request with other modules' data",[645,778,779],{},[651,780,781],{},"registerBootstrapProvider",[645,783,784],{},[606,785,787],{"href":786},"#bootstrap-providers","Bootstrap providers",[627,789,790,793,798],{},[645,791,792],{},"Add a column to another module's table",[645,794,795],{},[651,796,797],{},"extendDatabaseTable",[645,799,800],{},[606,801,77],{"href":78},[616,803,805],{"id":804},"terms","Terms",[807,808,809,818,826,834],"ul",{},[810,811,812,817],"li",{},[601,813,814,614],{},[606,815,816],{"href":87},"Extension point"," A place where one module adds things to another without importing\nit, either a registry or a hook.",[810,819,820,825],{},[601,821,822,614],{},[606,823,824],{"href":87},"Registry"," A build-time list that modules add entries to, such as menus, widgets, tabs,\npermissions, roles, scopes and Model Context Protocol (MCP) tools.",[810,827,828,833],{},[601,829,830,614],{},[606,831,832],{"href":87},"Hook"," A runtime hook (Nitro or app) that a module calls and that listeners in the host\napp or other modules answer.",[810,835,836,841],{},[601,837,838,614],{},[606,839,840],{"href":359},"Widget area"," A named slot where the Widget module renders the components modules add.",[616,843,845],{"id":844},"how-it-works","How it works",[593,847,848],{},"Extension points come in two kinds:",[621,850,851,864],{},[624,852,853],{},[627,854,855,858,861],{},[630,856,857],{},"Kind",[630,859,860],{},"When it runs",[630,862,863],{},"Example",[640,865,866,889],{},[627,867,868,872,879],{},[645,869,870],{},[601,871,824],{},[645,873,874,875,878],{},"In a module's ",[651,876,877],{},"setup()",", collected once every module is done",[645,880,881,670,883,670,885,670,887],{},[651,882,653],{},[651,884,689],{},[651,886,732],{},[651,888,765],{},[627,890,891,895,898],{},[645,892,893],{},[601,894,832],{},[645,896,897],{},"At run time, a Nitro or app hook answered by the host app or another module",[645,899,900,670,902,670,905],{},[651,901,718],{},[651,903,904],{},"auth:email",[651,906,907],{},"widgets:filter",[593,909,910,911,614],{},"Registration order between modules never matters: items are merged after ",[651,912,913],{},"modules:done",[915,916,917,918,921,922,925,926,928],"note",{},"Nuxt build hooks, such as ",[651,919,920],{},"auth:resolvers"," and ",[651,923,924],{},"widgets:resolved",", run at build time and are not extension points.\n",[606,927,381],{"href":382}," has the build-time helpers for module authors.",[930,931,933],"h3",{"id":932},"registries","Registries",[593,935,936,937,940,941,614],{},"Most registries are built on ",[651,938,939],{},"defineVirtualRegistry"," from ",[651,942,943],{},"@frigear-nu\u002Fmodule-kit\u002Futils",[945,946,947,952,958],"ol",{},[810,948,949,950,614],{},"The module that owns the registry calls it from its ",[651,951,877],{},[810,953,954,955,957],{},"Other modules add entries during their own ",[651,956,877],{},", through the owner's helper.",[810,959,960],{},"Once every module is done, the entries are bundled into one virtual module that the runtime imports.",[593,962,963],{},"There is no scanning at run time, which keeps it working on Cloudflare Workers.",[593,965,966,968,969,971,972,921,975,978],{},[606,967,391],{"href":392}," has the signature and options of\n",[651,970,939],{},", including its ",[651,973,974],{},"app",[651,976,977],{},"server"," targets.",[930,980,982],{"id":981},"hooks-ask-the-host-app","Hooks: ask the host app",[593,984,985],{},"A hook is a question a module asks and the host app answers. Access control has two Nitro hooks, both answered by\nyour host app (or another module), and three app hooks that filter what the interface shows:",[621,987,988,1000],{},[624,989,990],{},[627,991,992,994,997],{},[630,993,832],{},[630,995,996],{},"Fired by",[630,998,999],{},"The listener decides",[640,1001,1002,1026,1046,1066,1085],{},[627,1003,1004,1008,1020],{},[645,1005,1006],{},[651,1007,718],{},[645,1009,1010,1011,670,1014,921,1017],{},"Auth, on every ",[651,1012,1013],{},"authorize",[651,1015,1016],{},"allows",[651,1018,1019],{},"canGrantScope",[645,1021,1022,1023,614],{},"Allow or deny any permission, after the credential checks. Context: ",[651,1024,1025],{},"{ event, principal, user, permission, record, allow(), deny() }",[627,1027,1028,1033,1036],{},[645,1029,1030],{},[651,1031,1032],{},"authorization:roles",[645,1034,1035],{},"Auth, while it resolves a user's roles",[645,1037,1038,1039,1042,1043,614],{},"Roles to add or remove, such as an environment allowlist mapped to ",[651,1040,1041],{},"admin",". Context: ",[651,1044,1045],{},"{ event, user, roles }",[627,1047,1048,1053,1056],{},[645,1049,1050],{},[651,1051,1052],{},"menus:filter",[645,1054,1055],{},"Menu, when it resolves a menu",[645,1057,1058,1059,1062,1063,614],{},"Which items the viewer sees (",[651,1060,1061],{},"item.permission","). Authorization answers it with ",[651,1064,1065],{},"can()",[627,1067,1068,1075,1078],{},[645,1069,1070,670,1072],{},[651,1071,907],{},[651,1073,1074],{},"tabs:filter",[645,1076,1077],{},"The widget components",[645,1079,1080,1081,1084],{},"Which widgets and tabs the viewer sees (",[651,1082,1083],{},"permission"," on the entry).",[627,1086,1087,1092,1094],{},[645,1088,1089],{},[651,1090,1091],{},"oauth:application:access",[645,1093,225],{},[645,1095,1096],{},"Who may sign in to a given OAuth application.",[593,1098,1099,1101,1102,1105,1106,1109,1110,1113,1114,1117,1118,1121],{},[651,1100,718],{}," is the single override. Results are collected across listeners: any ",[651,1103,1104],{},"deny()"," or a listener that\nthrows denies; otherwise any ",[651,1107,1108],{},"allow()"," allows. It runs after the credential checks, so it can never give a token a\npermission it was not issued, nor a token a ",[651,1111,1112],{},"sessionOnly"," permission. A listener identifies the caller from ",[651,1115,1116],{},"ctx.user",",\nnever from the session on ",[651,1119,1120],{},"ctx.event",". The filter hooks only hide things; the server still checks.",[593,1123,1124,1127,1128,1131],{},[606,1125,67],{"href":1126},"\u002Fguide\u002Fconcepts\u002Fauth-and-scopes#the-before-hook"," explains both Nitro hooks. At build time,\n",[651,1129,1130],{},"auth:access-compiled"," hands the compiled permissions, roles and scopes to any module that wants to read them, for\nexample to list them in documentation or an admin page.",[1133,1134,1135,1138,1263],"warning",{},[593,1136,1137],{},"Each module used to have its own authorize hook. Those are removed with no shim: a listener on them never runs. In\ndevelopment a plugin warns when one still has listeners.",[621,1139,1140,1150],{},[624,1141,1142],{},[627,1143,1144,1147],{},[630,1145,1146],{},"Removed hook",[630,1148,1149],{},"Replace it with",[640,1151,1152,1183,1196,1207,1219,1233,1247],{},[627,1153,1154,1174],{},[645,1155,1156,670,1159,670,1162,670,1165,670,1168,670,1171],{},[651,1157,1158],{},"events:authorize",[651,1160,1161],{},"forms:authorize",[651,1163,1164],{},"payments:authorize",[651,1166,1167],{},"oauth:authorize",[651,1169,1170],{},"authorization:authorize",[651,1172,1173],{},"users:admin:authorize",[645,1175,1176,1178,1179,1182],{},[651,1177,718],{},", or a policy (",[651,1180,1181],{},"definePolicy",") on that module's permissions",[627,1184,1185,1190],{},[645,1186,1187],{},[651,1188,1189],{},"users:admin:roles",[645,1191,1192,1193],{},"Roles in ",[651,1194,1195],{},"authorization.roles",[627,1197,1198,1203],{},[645,1199,1200],{},[651,1201,1202],{},"authorization:role",[645,1204,1205],{},[651,1206,1032],{},[627,1208,1209,1214],{},[645,1210,1211],{},[651,1212,1213],{},"auth:user-permissions",[645,1215,1216,1217],{},"Roles, or ",[651,1218,718],{},[627,1220,1221,1226],{},[645,1222,1223],{},[651,1224,1225],{},"auth:scope-available",[645,1227,1228,1230,1231],{},[651,1229,718],{},"; minting goes through ",[651,1232,1019],{},[627,1234,1235,1240],{},[645,1236,1237],{},[651,1238,1239],{},"oauth:scope:staff",[645,1241,1216,1242,1244,1245],{},[651,1243,718],{},"; consent goes through ",[651,1246,1019],{},[627,1248,1249,1254],{},[645,1250,1251],{},[651,1252,1253],{},"oauth:user:roles",[645,1255,1256,1258,1259,1262],{},[651,1257,1032],{},"; an application's ",[651,1260,1261],{},"allowedRoles"," resolves through the role provider",[593,1264,1265,1266,1269],{},"The ",[606,1267,1268],{"href":152},"migration guide"," has a before and after for each.",[930,1271,659],{"id":1272},"menus",[593,1274,1275,1277,1278,670,1281,921,1284,1287,1288,1291,1292,1295,1296,1298,1299,614],{},[606,1276,354],{"href":355}," holds named menus, such as ",[651,1279,1280],{},"main",[651,1282,1283],{},"dashboard",[651,1285,1286],{},"account",". A menu path is split on\nits first dot: ",[651,1289,1290],{},"dashboard.more"," is group ",[651,1293,1294],{},"more"," of menu ",[651,1297,1283],{},". Items from Nuxt Content, from modules and from\nruntime sources are merged and sorted by ",[651,1300,1301],{},"order",[593,1303,1304,1305,1307],{},"An item may name a ",[651,1306,1083],{},". The item is hidden for a viewer who lacks it, and the menu is resolved again on sign-in\nand sign-out. The route behind the item checks the permission itself.",[593,1309,1310],{},"A module adds static items at build time:",[1312,1313,1318],"pre",{"className":1314,"code":1315,"language":1316,"meta":1317,"style":1317},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { extendMenu } from '@frigear-nu\u002Fmenu\u002Futils'\n\nextendMenu(nuxt, 'dashboard.more', [\n  { label: 'Billing', description: 'Manage your subscription', to: '\u002Fbilling' },\n])\n","ts","",[651,1319,1320,1353,1360,1384,1435],{"__ignoreMap":1317},[1321,1322,1325,1329,1333,1337,1340,1343,1346,1350],"span",{"class":1323,"line":1324},"line",1,[1321,1326,1328],{"class":1327},"s7zQu","import",[1321,1330,1332],{"class":1331},"sMK4o"," {",[1321,1334,1336],{"class":1335},"sTEyZ"," extendMenu",[1321,1338,1339],{"class":1331}," }",[1321,1341,1342],{"class":1327}," from",[1321,1344,1345],{"class":1331}," '",[1321,1347,1349],{"class":1348},"sfazB","@frigear-nu\u002Fmenu\u002Futils",[1321,1351,1352],{"class":1331},"'\n",[1321,1354,1356],{"class":1323,"line":1355},2,[1321,1357,1359],{"emptyLinePlaceholder":1358},true,"\n",[1321,1361,1363,1366,1369,1372,1374,1376,1379,1381],{"class":1323,"line":1362},3,[1321,1364,653],{"class":1365},"s2Zo4",[1321,1367,1368],{"class":1335},"(nuxt",[1321,1370,1371],{"class":1331},",",[1321,1373,1345],{"class":1331},[1321,1375,1290],{"class":1348},[1321,1377,1378],{"class":1331},"'",[1321,1380,1371],{"class":1331},[1321,1382,1383],{"class":1335}," [\n",[1321,1385,1387,1390,1394,1397,1399,1402,1404,1406,1409,1411,1413,1416,1418,1420,1423,1425,1427,1430,1432],{"class":1323,"line":1386},4,[1321,1388,1389],{"class":1331},"  {",[1321,1391,1393],{"class":1392},"swJcz"," label",[1321,1395,1396],{"class":1331},":",[1321,1398,1345],{"class":1331},[1321,1400,1401],{"class":1348},"Billing",[1321,1403,1378],{"class":1331},[1321,1405,1371],{"class":1331},[1321,1407,1408],{"class":1392}," description",[1321,1410,1396],{"class":1331},[1321,1412,1345],{"class":1331},[1321,1414,1415],{"class":1348},"Manage your subscription",[1321,1417,1378],{"class":1331},[1321,1419,1371],{"class":1331},[1321,1421,1422],{"class":1392}," to",[1321,1424,1396],{"class":1331},[1321,1426,1345],{"class":1331},[1321,1428,1429],{"class":1348},"\u002Fbilling",[1321,1431,1378],{"class":1331},[1321,1433,1434],{"class":1331}," },\n",[1321,1436,1438],{"class":1323,"line":1437},5,[1321,1439,1440],{"class":1335},"])\n",[593,1442,1443,1444,940,1447,1449],{},"Dynamic items come from a runtime source file. A module that must work without Menu registers its source with\n",[651,1445,1446],{},"addOptionalMenuSource",[651,1448,943],{},". It does nothing when Menu is not installed.",[930,1451,694],{"id":1452},"widget-areas-and-tabs",[593,1454,1455,1457,1458,1460,1461,921,1463,1465],{},[606,1456,358],{"href":359}," offers two registries with the same ordering and gating. A widget or tab may name a\n",[651,1459,1083],{},"; ",[651,1462,907],{},[651,1464,1074],{}," hide it from viewers who lack it.",[807,1467,1468,1480],{},[810,1469,1470,1473,1474,1477,1478,614],{},[601,1471,1472],{},"Widgets"," fill a named widget area. The host app renders ",[651,1475,1476],{},"\u003CWidgetArea name=\"admin.dashboard\">"," wherever it likes.\nModules add components with ",[651,1479,689],{},[810,1481,1482,1485,1486,1489,1490,614],{},[601,1483,1484],{},"Tabs"," fill a detail page. ",[651,1487,1488],{},"\u003CWidgetTabs name=\"admin.users\">"," renders every registered tab, with the active tab in\n",[651,1491,1492],{},"?tab=",[1312,1494,1496],{"className":1314,"code":1495,"language":1316,"meta":1317,"style":1317},"import { extendWidgets, extendTabs } from '@frigear-nu\u002Fwidget\u002Futils'\n\nextendWidgets(nuxt, 'admin.dashboard', { component: resolver.resolve('.\u002Fruntime\u002Fapp\u002Fwidgets\u002FNewMembers.vue'), order: 10 })\nextendTabs(nuxt, 'admin.users', { id: 'payments', component: resolver.resolve('.\u002Fruntime\u002Fapp\u002Ftabs\u002FPayments.vue'), label: 'Payments' })\n",[651,1497,1498,1523,1527,1588],{"__ignoreMap":1317},[1321,1499,1500,1502,1504,1507,1509,1512,1514,1516,1518,1521],{"class":1323,"line":1324},[1321,1501,1328],{"class":1327},[1321,1503,1332],{"class":1331},[1321,1505,1506],{"class":1335}," extendWidgets",[1321,1508,1371],{"class":1331},[1321,1510,1511],{"class":1335}," extendTabs",[1321,1513,1339],{"class":1331},[1321,1515,1342],{"class":1327},[1321,1517,1345],{"class":1331},[1321,1519,1520],{"class":1348},"@frigear-nu\u002Fwidget\u002Futils",[1321,1522,1352],{"class":1331},[1321,1524,1525],{"class":1323,"line":1355},[1321,1526,1359],{"emptyLinePlaceholder":1358},[1321,1528,1529,1531,1533,1535,1537,1540,1542,1544,1546,1549,1551,1554,1556,1559,1562,1564,1567,1569,1572,1574,1577,1579,1583,1585],{"class":1323,"line":1362},[1321,1530,689],{"class":1365},[1321,1532,1368],{"class":1335},[1321,1534,1371],{"class":1331},[1321,1536,1345],{"class":1331},[1321,1538,1539],{"class":1348},"admin.dashboard",[1321,1541,1378],{"class":1331},[1321,1543,1371],{"class":1331},[1321,1545,1332],{"class":1331},[1321,1547,1548],{"class":1392}," component",[1321,1550,1396],{"class":1331},[1321,1552,1553],{"class":1335}," resolver",[1321,1555,614],{"class":1331},[1321,1557,1558],{"class":1365},"resolve",[1321,1560,1561],{"class":1335},"(",[1321,1563,1378],{"class":1331},[1321,1565,1566],{"class":1348},".\u002Fruntime\u002Fapp\u002Fwidgets\u002FNewMembers.vue",[1321,1568,1378],{"class":1331},[1321,1570,1571],{"class":1335},")",[1321,1573,1371],{"class":1331},[1321,1575,1576],{"class":1392}," order",[1321,1578,1396],{"class":1331},[1321,1580,1582],{"class":1581},"sbssI"," 10",[1321,1584,1339],{"class":1331},[1321,1586,1587],{"class":1335},")\n",[1321,1589,1590,1592,1594,1596,1598,1601,1603,1605,1607,1610,1612,1614,1617,1619,1621,1623,1625,1627,1629,1631,1633,1635,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656],{"class":1323,"line":1386},[1321,1591,704],{"class":1365},[1321,1593,1368],{"class":1335},[1321,1595,1371],{"class":1331},[1321,1597,1345],{"class":1331},[1321,1599,1600],{"class":1348},"admin.users",[1321,1602,1378],{"class":1331},[1321,1604,1371],{"class":1331},[1321,1606,1332],{"class":1331},[1321,1608,1609],{"class":1392}," id",[1321,1611,1396],{"class":1331},[1321,1613,1345],{"class":1331},[1321,1615,1616],{"class":1348},"payments",[1321,1618,1378],{"class":1331},[1321,1620,1371],{"class":1331},[1321,1622,1548],{"class":1392},[1321,1624,1396],{"class":1331},[1321,1626,1553],{"class":1335},[1321,1628,614],{"class":1331},[1321,1630,1558],{"class":1365},[1321,1632,1561],{"class":1335},[1321,1634,1378],{"class":1331},[1321,1636,1637],{"class":1348},".\u002Fruntime\u002Fapp\u002Ftabs\u002FPayments.vue",[1321,1639,1378],{"class":1331},[1321,1641,1571],{"class":1335},[1321,1643,1371],{"class":1331},[1321,1645,1393],{"class":1392},[1321,1647,1396],{"class":1331},[1321,1649,1345],{"class":1331},[1321,1651,254],{"class":1348},[1321,1653,1378],{"class":1331},[1321,1655,1339],{"class":1331},[1321,1657,1587],{"class":1335},[593,1659,1660,1661,1664],{},"An entry registered again under the same ",[651,1662,1663],{},"id"," replaces the earlier one. That is how a host app overrides a module's\nwidget.",[1666,1667,1668,1669,1672,1673,1676,1677,1679],"tip",{},"Use ",[651,1670,1671],{},"WidgetTabs"," when other modules should add sections to a page. Use ",[651,1674,1675],{},"PageTabs"," when the page owns them (see\n",[606,1678,101],{"href":102},").",[930,1681,679],{"id":1682},"admin-navigation-and-dashboard",[593,1684,1685,1687],{},[606,1686,217],{"href":351}," draws the admin area entirely from the two registries above. The sidebar is three menu\ngroups:",[621,1689,1690,1703],{},[624,1691,1692],{},[627,1693,1694,1697,1700],{},[630,1695,1696],{},"Group",[630,1698,1699],{},"Menu path",[630,1701,1702],{},"Put here",[640,1704,1705,1717,1730],{},[627,1706,1707,1709,1714],{},[645,1708,173],{},[645,1710,1711],{},[651,1712,1713],{},"admin.nav.overview",[645,1715,1716],{},"The dashboard",[627,1718,1719,1722,1727],{},[645,1720,1721],{},"Manage",[645,1723,1724],{},[651,1725,1726],{},"admin.nav",[645,1728,1729],{},"What staff manage every day",[627,1731,1732,1735,1740],{},[645,1733,1734],{},"Settings",[645,1736,1737],{},[651,1738,1739],{},"admin.nav.settings",[645,1741,26],{},[593,1743,1744],{},"The dashboard is three widget areas:",[621,1746,1747,1756],{},[624,1748,1749],{},[627,1750,1751,1754],{},[630,1752,1753],{},"Dashboard area",[630,1755,1702],{},[640,1757,1758,1772,1781],{},[627,1759,1760,1765],{},[645,1761,1762],{},[651,1763,1764],{},"admin.dashboard.top",[645,1766,1767,1768,1771],{},"One ",[651,1769,1770],{},"StatCard"," per widget",[627,1773,1774,1778],{},[645,1775,1776],{},[651,1777,1539],{},[645,1779,1780],{},"Lists and tables",[627,1782,1783,1788],{},[645,1784,1785],{},[651,1786,1787],{},"admin.dashboard.side",[645,1789,1790,1791,1794],{},"Short ",[651,1792,1793],{},"ListCard","s",[1312,1796,1798],{"className":1314,"code":1797,"language":1316,"meta":1317,"style":1317},"import { extendAdminNav, extendAdminDashboard, adminRoute } from '@frigear-nu\u002Fadmin\u002Futils'\n\nextendAdminNav(nuxt, { label: 'Forms', to: adminRoute(nuxt, 'forms'), icon: 'i-lucide-clipboard-list', order: 20 })\nextendAdminDashboard(nuxt, { component: resolver.resolve('.\u002Fruntime\u002Fapp\u002Fwidgets\u002FSubmissions.vue'), order: 10 })\n",[651,1799,1800,1830,1834,1901],{"__ignoreMap":1317},[1321,1801,1802,1804,1806,1809,1811,1814,1816,1819,1821,1823,1825,1828],{"class":1323,"line":1324},[1321,1803,1328],{"class":1327},[1321,1805,1332],{"class":1331},[1321,1807,1808],{"class":1335}," extendAdminNav",[1321,1810,1371],{"class":1331},[1321,1812,1813],{"class":1335}," extendAdminDashboard",[1321,1815,1371],{"class":1331},[1321,1817,1818],{"class":1335}," adminRoute",[1321,1820,1339],{"class":1331},[1321,1822,1342],{"class":1327},[1321,1824,1345],{"class":1331},[1321,1826,1827],{"class":1348},"@frigear-nu\u002Fadmin\u002Futils",[1321,1829,1352],{"class":1331},[1321,1831,1832],{"class":1323,"line":1355},[1321,1833,1359],{"emptyLinePlaceholder":1358},[1321,1835,1836,1838,1840,1842,1844,1846,1848,1850,1852,1854,1856,1858,1860,1862,1864,1866,1868,1871,1873,1875,1877,1880,1882,1884,1886,1888,1890,1892,1894,1897,1899],{"class":1323,"line":1362},[1321,1837,669],{"class":1365},[1321,1839,1368],{"class":1335},[1321,1841,1371],{"class":1331},[1321,1843,1332],{"class":1331},[1321,1845,1393],{"class":1392},[1321,1847,1396],{"class":1331},[1321,1849,1345],{"class":1331},[1321,1851,294],{"class":1348},[1321,1853,1378],{"class":1331},[1321,1855,1371],{"class":1331},[1321,1857,1422],{"class":1392},[1321,1859,1396],{"class":1331},[1321,1861,1818],{"class":1365},[1321,1863,1368],{"class":1335},[1321,1865,1371],{"class":1331},[1321,1867,1345],{"class":1331},[1321,1869,1870],{"class":1348},"forms",[1321,1872,1378],{"class":1331},[1321,1874,1571],{"class":1335},[1321,1876,1371],{"class":1331},[1321,1878,1879],{"class":1392}," icon",[1321,1881,1396],{"class":1331},[1321,1883,1345],{"class":1331},[1321,1885,498],{"class":1348},[1321,1887,1378],{"class":1331},[1321,1889,1371],{"class":1331},[1321,1891,1576],{"class":1392},[1321,1893,1396],{"class":1331},[1321,1895,1896],{"class":1581}," 20",[1321,1898,1339],{"class":1331},[1321,1900,1587],{"class":1335},[1321,1902,1903,1905,1907,1909,1911,1913,1915,1917,1919,1921,1923,1925,1928,1930,1932,1934,1936,1938,1940,1942],{"class":1323,"line":1386},[1321,1904,673],{"class":1365},[1321,1906,1368],{"class":1335},[1321,1908,1371],{"class":1331},[1321,1910,1332],{"class":1331},[1321,1912,1548],{"class":1392},[1321,1914,1396],{"class":1331},[1321,1916,1553],{"class":1335},[1321,1918,614],{"class":1331},[1321,1920,1558],{"class":1365},[1321,1922,1561],{"class":1335},[1321,1924,1378],{"class":1331},[1321,1926,1927],{"class":1348},".\u002Fruntime\u002Fapp\u002Fwidgets\u002FSubmissions.vue",[1321,1929,1378],{"class":1331},[1321,1931,1571],{"class":1335},[1321,1933,1371],{"class":1331},[1321,1935,1576],{"class":1392},[1321,1937,1396],{"class":1331},[1321,1939,1582],{"class":1581},[1321,1941,1339],{"class":1331},[1321,1943,1587],{"class":1335},[593,1945,1946,1947,921,1949,1951,1952,614],{},"New features go into one of the existing groups. A module can also call ",[651,1948,653],{},[651,1950,689],{}," directly,\nwithout depending on Admin. See ",[606,1953,96],{"href":97},[930,1955,1957],{"id":1956},"account-menu-and-overview","Account menu and overview",[593,1959,1960,1962],{},[606,1961,195],{"href":196}," uses the same registries for the account pages:",[807,1964,1965,1970,1976,1979,1989],{},[810,1966,1265,1967,1969],{},[651,1968,1286],{}," menu is the account navigation and the overview's shortcuts.",[810,1971,1972,1975],{},[651,1973,1974],{},"account.dropdown"," is the account menu in the header.",[810,1977,1978],{},"A module adds an account page by registering the route and a menu item. It never draws its own account navigation.",[810,1980,1265,1981,1984,1985,1988],{},[651,1982,1983],{},"account.overview"," widget area fills the overview page. Its widgets receive the session ",[651,1986,1987],{},"user"," as a prop.",[810,1990,1991,1993],{},[651,1992,1301],{}," places a page between others. Membership sits at 15, Tickets at 40 and Applications at 50.",[930,1995,771],{"id":1996},"mcp-tools",[593,1998,1999,2001,2002,2004,2005,2007,2008,670,2011,610,2014,1396],{},[606,2000,317],{"href":318}," serves one Model Context Protocol endpoint for everybody. A module adds tools from its\n",[651,2003,877],{}," with ",[651,2006,765],{},". It defines them with ",[651,2009,2010],{},"defineScopedMcpTool",[651,2012,2013],{},"defineScopedMcpResource",[651,2015,2016],{},"defineScopedMcpPrompt",[1312,2018,2020],{"className":1314,"code":2019,"language":1316,"meta":1317,"style":1317},"addPermission(nuxt, { name: 'notes:read', label: 'Read your notes', implicit: true })\nif (options.settings?.mcp === true) addMcpDefinitions(nuxt, resolver.resolve('.\u002Fruntime\u002Fserver\u002Fmcp'))\n",[651,2021,2022,2072],{"__ignoreMap":1317},[1321,2023,2024,2026,2028,2030,2032,2035,2037,2039,2042,2044,2046,2048,2050,2052,2055,2057,2059,2062,2064,2068,2070],{"class":1323,"line":1324},[1321,2025,732],{"class":1365},[1321,2027,1368],{"class":1335},[1321,2029,1371],{"class":1331},[1321,2031,1332],{"class":1331},[1321,2033,2034],{"class":1392}," name",[1321,2036,1396],{"class":1331},[1321,2038,1345],{"class":1331},[1321,2040,2041],{"class":1348},"notes:read",[1321,2043,1378],{"class":1331},[1321,2045,1371],{"class":1331},[1321,2047,1393],{"class":1392},[1321,2049,1396],{"class":1331},[1321,2051,1345],{"class":1331},[1321,2053,2054],{"class":1348},"Read your notes",[1321,2056,1378],{"class":1331},[1321,2058,1371],{"class":1331},[1321,2060,2061],{"class":1392}," implicit",[1321,2063,1396],{"class":1331},[1321,2065,2067],{"class":2066},"sfNiH"," true",[1321,2069,1339],{"class":1331},[1321,2071,1587],{"class":1335},[1321,2073,2074,2077,2080,2082,2085,2088,2091,2094,2096,2099,2101,2103,2105,2107,2109,2111,2113,2115,2118,2120],{"class":1323,"line":1355},[1321,2075,2076],{"class":1327},"if",[1321,2078,2079],{"class":1335}," (options",[1321,2081,614],{"class":1331},[1321,2083,2084],{"class":1335},"settings",[1321,2086,2087],{"class":1331},"?.",[1321,2089,2090],{"class":1335},"mcp ",[1321,2092,2093],{"class":1331},"===",[1321,2095,2067],{"class":2066},[1321,2097,2098],{"class":1335},") ",[1321,2100,765],{"class":1365},[1321,2102,1368],{"class":1335},[1321,2104,1371],{"class":1331},[1321,2106,1553],{"class":1335},[1321,2108,614],{"class":1331},[1321,2110,1558],{"class":1365},[1321,2112,1561],{"class":1335},[1321,2114,1378],{"class":1331},[1321,2116,2117],{"class":1348},".\u002Fruntime\u002Fserver\u002Fmcp",[1321,2119,1378],{"class":1331},[1321,2121,2122],{"class":1335},"))\n",[807,2124,2125,2131,2142,2148,2157],{},[810,2126,2127,2128,2130],{},"Every definition declares the ",[651,2129,1083],{}," it needs.",[810,2132,2133,2134,2137,2138,2141],{},"A tool is listed and callable only when ",[651,2135,2136],{},"allows(event, permission)"," is true for the caller's credential, so a token\nthat holds only ",[651,2139,2140],{},"tickets:read"," never sees an admin tool.",[810,2143,2144,2145,2147],{},"A tool takes the user from ",[651,2146,1116],{},", never as an argument.",[810,2149,2150,2153,2154,614],{},[651,2151,2152],{},"ctx.can(permission, record?)"," asks a further permission for the same caller, such as an opt-in ",[651,2155,2156],{},"*:pii",[810,2158,2159,2160,614],{},"Exposing tools is opt-in per module, through ",[651,2161,2162],{},"settings.mcp",[930,2164,741],{"id":2165},"auth-extension-points",[593,2167,2168,2171,2172,1396],{},[651,2169,2170],{},"@frigear-nu\u002Fauth\u002Futils"," offers these build-time registries. Any module may call them from its ",[651,2173,877],{},[621,2175,2176,2186],{},[624,2177,2178],{},[627,2179,2180,2183],{},[630,2181,2182],{},"Helper",[630,2184,2185],{},"Registers",[640,2187,2188,2214,2227,2236,2249,2265,2275],{},[627,2189,2190,2194],{},[645,2191,2192],{},[651,2193,732],{},[645,2195,1265,2196,2199,2200,670,2203,670,2206,670,2208,921,2211,614],{},[651,2197,2198],{},"resource:action"," permissions your module checks, with ",[651,2201,2202],{},"implicit",[651,2204,2205],{},"sensitive",[651,2207,1112],{},[651,2209,2210],{},"aliases",[651,2212,2213],{},"grantTo",[627,2215,2216,2220],{},[645,2217,2218],{},[651,2219,735],{},[645,2221,2222,2223,2226],{},"A default role, such as ",[651,2224,2225],{},"eventmanager",". The host's config merges into it.",[627,2228,2229,2233],{},[645,2230,2231],{},[651,2232,751],{},[645,2234,2235],{},"A named scope that expands to permissions, for API tokens and OAuth.",[627,2237,2238,2243],{},[645,2239,2240],{},[651,2241,2242],{},"addRoleRequirement",[645,2244,2245,2246,614],{},"A requirement a role can name, such as ",[651,2247,2248],{},"subscription",[627,2250,2251,2256],{},[645,2252,2253],{},[651,2254,2255],{},"addPolicies",[645,2257,2258,2259,921,2261,2264],{},"A file of ",[651,2260,1181],{},[651,2262,2263],{},"defineSharedPolicy"," exports.",[627,2266,2267,2272],{},[645,2268,2269],{},[651,2270,2271],{},"addAuthResolver",[645,2273,2274],{},"A custom bearer credential, by prefix or as a JWT.",[627,2276,2277,2282],{},[645,2278,2279],{},[651,2280,2281],{},"setAuthUserStore",[645,2283,2284],{},"Where users are stored. The first registration wins.",[593,2286,2287,2288,2291],{},"Users registers the user store. That is why Auth works without depending on it. Authorization registers the role\nprovider in the same way. ",[606,2289,67],{"href":2290},"\u002Fguide\u002Fconcepts\u002Fauth-and-scopes#how-modules-contribute"," shows each one\nin use, and how the host's config merges with what modules register.",[930,2293,787],{"id":2294},"bootstrap-providers",[593,2296,2297,2298,2300,2301,2304,2305,614],{},"Some pages show several modules' data for the signed-in user, such as the account pages. Without help they make one\nrequest per module. ",[651,2299,781],{}," adds a module's piece to the single\n",[651,2302,2303],{},"GET \u002Fapi\u002Fauth\u002Fbootstrap?scope=\u003Cscope>"," request of that area. See ",[606,2306,381],{"href":382},[1666,2308,2309],{},"Register only data that a page of the area shows on first render.",[616,2311,2313],{"id":2312},"common-mistakes","Common mistakes",[1133,2315,2316,2319],{},[601,2317,2318],{},"Trusting a hidden widget."," Gating a widget only hides it in the interface. Always enforce permissions on the server\nfor the data a widget loads.",[1133,2321,2322,2325,2326,2328,2329,2331],{},[601,2323,2324],{},"Reading the session in a hook listener."," Listeners identify the caller from ",[651,2327,1116],{},", never from the session on\n",[651,2330,1120],{},". The session is empty for bearer requests.",[616,2333,2335],{"id":2334},"next-steps","Next steps",[807,2337,2338,2343,2348],{},[810,2339,2340,2342],{},[606,2341,486],{"href":487}," – put the registries to work in a real page.",[810,2344,2345,2347],{},[606,2346,77],{"href":78}," – use the one extension point that changes the schema.",[810,2349,2350,2352],{},[606,2351,519],{"href":525}," – start a module that adds to others.",[2354,2355,2356],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":1317,"searchDepth":1355,"depth":1355,"links":2358},[2359,2360,2361,2372,2373],{"id":618,"depth":1355,"text":619},{"id":804,"depth":1355,"text":805},{"id":844,"depth":1355,"text":845,"children":2362},[2363,2364,2365,2366,2367,2368,2369,2370,2371],{"id":932,"depth":1362,"text":933},{"id":981,"depth":1362,"text":982},{"id":1272,"depth":1362,"text":659},{"id":1452,"depth":1362,"text":694},{"id":1682,"depth":1362,"text":679},{"id":1956,"depth":1362,"text":1957},{"id":1996,"depth":1362,"text":771},{"id":2165,"depth":1362,"text":741},{"id":2294,"depth":1362,"text":787},{"id":2312,"depth":1355,"text":2313},{"id":2334,"depth":1355,"text":2335},"How a module adds menu items, dashboard widgets, tabs, admin pages, permissions, roles, scopes and MCP tools to other modules without importing them.","md",null,{},{"icon":75},{"title":72,"description":2374},"LOHULtFw_Adqv6_gvscWVaFSSWMmdHhSJQGNI44JSso",[2382,2384],{"title":67,"path":68,"stem":69,"description":2383,"icon":70,"children":-1},"How a server route learns who is calling and decides what they may do, with one vocabulary of permissions, roles, scopes, policies and a before hook, and what fails closed.",{"title":77,"path":78,"stem":79,"description":2385,"icon":44,"children":-1},"How your app or a module adds columns to a table another module owns, why every schema ships once per dialect, and which migrations to commit.",1794503147782]