[{"data":1,"prerenderedAt":1385},["ShallowReactive",2],{"navigation_docs":3,"-contribute-decisions-oauth-token-lifetimes":587,"-contribute-decisions-oauth-token-lifetimes-surround":1380},[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":579,"body":589,"description":1373,"extension":1374,"links":1375,"meta":1376,"navigation":1377,"path":580,"seo":1378,"stem":581,"__hash__":1379},"docs\u002F5.contribute\u002F8.decisions\u002F1.oauth-token-lifetimes.md",{"type":590,"value":591,"toc":1361},"minimark",[592,630,635,666,669,742,748,767,771,776,779,803,817,820,871,1063,1067,1078,1098,1102,1113,1185,1192,1196,1207,1211,1270,1274,1311,1315,1357],[593,594,595,599,600,604,605,608,609,611,612,615,616,619,620,623,624,629],"p",{},[596,597,598],"strong",{},"Status:"," accepted (2026-11-05), shipped in ",[601,602,603],"code",{},"@frigear-nu\u002Foauth-server"," 0.5. ",[596,606,607],{},"Scope:"," ",[601,610,603],{},",\n",[601,613,614],{},"@frigear-nu\u002Fauth"," (one hook), ",[601,617,618],{},"@frigear-nu\u002Fusers"," (fires it), ",[601,621,622],{},"@frigear-nu\u002Faccount"," (a page section), their docs.\nDrafted from personal-finance, whose iOS app is the first consumer. How it works today:\n",[625,626,628],"a",{"href":627},"\u002Fmodules\u002Fidentity\u002Foauth-server\u002Froutes-and-clients#session-lifetimes","Session lifetimes",".",[631,632,634],"h2",{"id":633},"context","Context",[593,636,637,638,641,642,645,646,649,650,653,654,657,658,661,662,665],{},"OAuth Server issues refresh tokens with one lifetime for every application: ",[601,639,640],{},"refreshTokenTtl",", default 30 days\n(",[601,643,644],{},"packages\u002Foauth-server\u002Fsrc\u002Fmodule.ts",", ",[601,647,648],{},"oauthServer.refreshTokenTtl","). ",[601,651,652],{},"createRefreshToken"," stamps\n",[601,655,656],{},"expiresAt = now + refreshTokenTtl"," (",[601,659,660],{},"runtime\u002Fserver\u002Futils\u002Foauth-grants.ts","), and every refresh rotates the token into a\nnew row with a fresh 30 days. So today the window is a sliding ",[596,663,664],{},"inactivity"," timeout: a user who opens the app once a\nmonth never signs in again, and one who is away for 31 days always does.",[593,667,668],{},"That one number cannot fit every kind of client the server has:",[670,671,672,687],"table",{},[673,674,675],"thead",{},[676,677,678,681,684],"tr",{},[679,680,213],"th",{},[679,682,683],{},"Where the refresh token lives",[679,685,686],{},"What users expect",[688,689,690,706,717,731],"tbody",{},[676,691,692,700,703],{},[693,694,695,696,699],"td",{},"First-party native app (Capacitor, ",[601,697,698],{},"com.example.app:\u002Fauth\u002Fcallback",")",[693,701,702],{},"Keychain \u002F Keystore",[693,704,705],{},"Signed in until they sign out, like Reddit, Instagram or Gmail",[676,707,708,711,714],{},[693,709,710],{},"Third-party web app (confidential, server-side)",[693,712,713],{},"The app's server",[693,715,716],{},"Weeks to months",[676,718,719,722,728],{},[693,720,721],{},"Browser SPA (public, https redirect only)",[693,723,724,727],{},[601,725,726],{},"localStorage",", readable by any XSS",[693,729,730],{},"Hours to a day (Microsoft caps SPAs at 24 h)",[676,732,733,736,739],{},[693,734,735],{},"MCP \u002F CLI \u002F integrations",[693,737,738],{},"A config file or a vault",[693,740,741],{},"Months, revoked by hand",[593,743,744,745,747],{},"Raising the global setting to six months gives every SPA a six-month credential in ",[601,746,726],{},". Keeping it at 30 days\nsigns mobile users out after a holiday. What large providers do instead is a lifetime per client plus an inactivity and\nan absolute limit (Auth0 and Okta expose both knobs; Google expires an unused refresh token after about six months), and\nthey lean on revocation, not expiry, to end long sessions.",[593,749,750,751,754,755,758,759,762,763,766],{},"What we already have, and keep: access tokens stay short (",[601,752,753],{},"accessTokenTtl",", 15 minutes); refresh tokens rotate, and\nreusing a rotated one revokes its whole family (",[601,756,757],{},"rotateRefreshToken","); an admin can revoke every grant of an\napplication (",[601,760,761],{},"POST \u002Fapi\u002Fadmin\u002Fapplications\u002F:id\u002Frevoke","). What is missing: a lifetime per application, a hard cap, and a\nway for a ",[596,764,765],{},"user"," to end their own sessions or have them ended when their password changes.",[631,768,770],{"id":769},"decision","Decision",[772,773,775],"h3",{"id":774},"_1-a-refresh-token-policy-per-application","1. A refresh-token policy per application",[593,777,778],{},"Two limits, both in seconds:",[780,781,782,789],"ul",{},[783,784,785,788],"li",{},[601,786,787],{},"idleTtl",": how long a refresh token lives unused. Every rotation starts a new window (today's behaviour).",[783,790,791,794,795,798,799,802],{},[601,792,793],{},"maxAge",": how long a sign-in lives in total, counted from the original ",[601,796,797],{},"authTime",", which the refresh token row\nalready carries across rotations. ",[601,800,801],{},"null"," = no cap.",[593,804,805,806,809,810,812,813,816],{},"A new row gets ",[601,807,808],{},"expiresAt = min(now + idleTtl, authTime + maxAge)",". ",[601,811,757],{}," already refuses an expired row,\nso nothing else in the token endpoint changes. Because the policy is read when a token is ",[596,814,815],{},"created",", a changed\npolicy reaches existing sessions on their next refresh; no backfill.",[593,818,819],{},"Where the policy comes from, first match wins:",[821,822,823,847,858],"ol",{},[783,824,825,826,645,829,832,833,657,836,838,839,842,843,846],{},"The application: two nullable columns on ",[601,827,828],{},"oauth_applications",[601,830,831],{},"refreshIdleTtl"," and ",[601,834,835],{},"refreshMaxAge",[601,837,801],{}," =\ninherit). Editable in the admin slideover as ",[596,840,841],{},"Session length",": Default, 1 day, 7 days, 30 days, 90 days,\n6 months, 1 year, plus an optional ",[596,844,845],{},"Sign in again after"," cap.",[783,848,849,850,853,854,857],{},"A configured client: ",[601,851,852],{},"oauthServer.clients[].refreshToken: { idleTtl?, maxAge? }",", managed by config like its other\nfields (409 ",[601,855,856],{},"managed_by_config"," on edit).",[783,859,860,861,809,864,866,867,870],{},"The global defaults: ",[601,862,863],{},"oauthServer.refreshToken: { idleTtl: 30 days, maxAge: null }",[601,865,640],{}," stays as a\ndeprecated alias for ",[601,868,869],{},"refreshToken.idleTtl",", so existing apps behave exactly as today.",[872,873,879],"pre",{"className":874,"code":875,"filename":876,"language":877,"meta":878,"style":878},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","oauthServer: {\n  refreshToken: { idleTtl: 60 * 60 * 24 * 30 }, \u002F\u002F everyone else: 30 days idle\n  clients: [{\n    id: 'eu.hareland.bills',\n    name: 'Household bills',\n    redirectUris: ['eu.hareland.bills:\u002Fauth\u002Fcallback'],\n    refreshToken: { idleTtl: 60 * 60 * 24 * 180 }, \u002F\u002F the iOS app: 6 months idle, no hard cap\n  }],\n}\n","nuxt.config.ts","ts","",[601,880,881,897,939,954,974,991,1013,1047,1057],{"__ignoreMap":878},[882,883,886,890,894],"span",{"class":884,"line":885},"line",1,[882,887,889],{"class":888},"sBMFI","oauthServer",[882,891,893],{"class":892},"sMK4o",":",[882,895,896],{"class":892}," {\n",[882,898,900,903,905,908,911,913,917,920,922,924,927,929,932,935],{"class":884,"line":899},2,[882,901,902],{"class":888},"  refreshToken",[882,904,893],{"class":892},[882,906,907],{"class":892}," {",[882,909,910],{"class":888}," idleTtl",[882,912,893],{"class":892},[882,914,916],{"class":915},"sbssI"," 60",[882,918,919],{"class":892}," *",[882,921,916],{"class":915},[882,923,919],{"class":892},[882,925,926],{"class":915}," 24",[882,928,919],{"class":892},[882,930,931],{"class":915}," 30",[882,933,934],{"class":892}," },",[882,936,938],{"class":937},"sHwdD"," \u002F\u002F everyone else: 30 days idle\n",[882,940,942,945,947,951],{"class":884,"line":941},3,[882,943,944],{"class":888},"  clients",[882,946,893],{"class":892},[882,948,950],{"class":949},"swJcz"," [",[882,952,953],{"class":892},"{\n",[882,955,957,960,962,965,969,972],{"class":884,"line":956},4,[882,958,959],{"class":949},"    id",[882,961,893],{"class":892},[882,963,964],{"class":892}," '",[882,966,968],{"class":967},"sfazB","eu.hareland.bills",[882,970,971],{"class":892},"'",[882,973,611],{"class":892},[882,975,977,980,982,984,987,989],{"class":884,"line":976},5,[882,978,979],{"class":949},"    name",[882,981,893],{"class":892},[882,983,964],{"class":892},[882,985,986],{"class":967},"Household bills",[882,988,971],{"class":892},[882,990,611],{"class":892},[882,992,994,997,999,1001,1003,1006,1008,1011],{"class":884,"line":993},6,[882,995,996],{"class":949},"    redirectUris",[882,998,893],{"class":892},[882,1000,950],{"class":949},[882,1002,971],{"class":892},[882,1004,1005],{"class":967},"eu.hareland.bills:\u002Fauth\u002Fcallback",[882,1007,971],{"class":892},[882,1009,1010],{"class":949},"]",[882,1012,611],{"class":892},[882,1014,1016,1019,1021,1023,1025,1027,1029,1031,1033,1035,1037,1039,1042,1044],{"class":884,"line":1015},7,[882,1017,1018],{"class":949},"    refreshToken",[882,1020,893],{"class":892},[882,1022,907],{"class":892},[882,1024,910],{"class":949},[882,1026,893],{"class":892},[882,1028,916],{"class":915},[882,1030,919],{"class":892},[882,1032,916],{"class":915},[882,1034,919],{"class":892},[882,1036,926],{"class":915},[882,1038,919],{"class":892},[882,1040,1041],{"class":915}," 180",[882,1043,934],{"class":892},[882,1045,1046],{"class":937}," \u002F\u002F the iOS app: 6 months idle, no hard cap\n",[882,1048,1050,1053,1055],{"class":884,"line":1049},8,[882,1051,1052],{"class":892},"  }",[882,1054,1010],{"class":949},[882,1056,611],{"class":892},[882,1058,1060],{"class":884,"line":1059},9,[882,1061,1062],{"class":892},"}\n",[772,1064,1066],{"id":1065},"_2-guardrails-by-client-kind","2. Guardrails by client kind",[593,1068,1069,1070,1073,1074,1077],{},"A long lifetime is only safe where the token is stored safely. The server infers the kind from what it already knows:\na ",[596,1071,1072],{},"native"," application is a public application whose redirect URIs are all custom-scheme or loopback; a ",[596,1075,1076],{},"browser","\napplication is a public application with an https redirect URI.",[780,1079,1080,1088,1095],{},[783,1081,1082,1083,832,1085,1087],{},"Upper bound for everyone: ",[601,1084,787],{},[601,1086,793],{}," at most 2 years. No \"never expires\": a forgotten device should\neventually stop working by itself.",[783,1089,1090,1091,1094],{},"Browser clients: above 7 days idle, the admin form shows a warning and the config validator logs one, naming XSS. A\ndeliberate ",[601,1092,1093],{},"allowLongBrowserSessions: true"," on the client silences it.",[783,1096,1097],{},"Native and confidential clients: no warning.",[772,1099,1101],{"id":1100},"_3-ending-long-sessions","3. Ending long sessions",[593,1103,1104,1105,1108,1109,1112],{},"Long sessions need ways to end them before they expire. All three use the existing ",[601,1106,1107],{},"revokeFamily"," and fire the existing\n",[601,1110,1111],{},"oauth:token:revoked"," hook.",[780,1114,1115,1150,1175],{},[783,1116,1117,608,1120,1123,1124,1126,1127,1130,1131,1134,1135,1138,1139,1142,1143,1145,1146,1149],{},[596,1118,1119],{},"Users see and end their own sessions.",[601,1121,1122],{},"GET \u002Fapi\u002Foauth-server\u002Fme\u002Fsessions"," lists the caller's live refresh-token\nfamilies grouped per application, as the large identity providers do: application name and icon, first signed in\n(",[601,1125,797],{},"), last active (the newest row's ",[601,1128,1129],{},"createdAt",", since each refresh writes a row) and the number of live\nfamilies. (It first listed one entry per family, which showed an application that re-authorized on the same device\nseveral times, with nothing to tell the entries apart.) ",[601,1132,1133],{},"DELETE \u002Fapi\u002Foauth-server\u002Fme\u002Fsessions\u002F:id"," (the application\nid) revokes that application's families, and ",[601,1136,1137],{},"DELETE \u002Fapi\u002Foauth-server\u002Fme\u002Fsessions"," revokes them all (the caller is a browser session, never one of the families). The\nroutes follow the module's URL convention (",[601,1140,1141],{},"\u002Fapi\u002F\u003Cmodule>\u002Fme\u002F...",") and are session only, like everything that\nchanges how a user signs in. ",[601,1144,622],{}," shows them as ",[596,1147,1148],{},"Signed-in apps"," on the Security page, when OAuth\nServer is installed.",[783,1151,1152,1155,1156,657,1159,1162,1163,1166,1167,1170,1171,1174],{},[596,1153,1154],{},"Credential changes end every session."," Auth defines a new Nitro hook, ",[601,1157,1158],{},"auth:credentials:changed",[601,1160,1161],{},"{ userId, reason: 'password' | 'password-reset' | 'email' }","), fired through ",[601,1164,1165],{},"notifyAuthCredentialsChanged"," after a password reset\n(Auth) and a password change, an email change and an email revert (Users, which owns those routes). OAuth Server\nlistens and revokes all of that user's families. An option ",[601,1168,1169],{},"revokeOnCredentialChange"," (default ",[601,1172,1173],{},"true",") turns it off.",[783,1176,1177,1180,1181,1184],{},[596,1178,1179],{},"Admins"," keep revoke-all per application. A per-user revoke on the admin user page is left for a later change;\n",[601,1182,1183],{},"revokeUserGrants(userId)"," already does it from code.",[593,1186,1187,1188,1191],{},"A device label (\"iPhone, Safari\") is left out of this change. The token request carries no reliable device name, and a\nUser-Agent string from a webview says little. A later change can add an optional ",[601,1189,1190],{},"device_name"," parameter that native\nclients send at the code exchange.",[772,1193,1195],{"id":1194},"_4-clients","4. Clients",[593,1197,1198,1199,1202,1203,1206],{},"Auth in client mode needs nothing: it already refreshes on demand and signs out when a refresh fails. The token\nresponse gains the non-standard but common ",[601,1200,1201],{},"refresh_token_expires_in"," (seconds until the new row's ",[601,1204,1205],{},"expiresAt","), which\nclients may use to warn before a forced sign-in; Auth ignores it for now.",[631,1208,1210],{"id":1209},"consequences","Consequences",[780,1212,1213,1229,1246,1255,1261],{},[783,1214,1215,1218,1219,1221,1222,1225,1226,1228],{},[596,1216,1217],{},"Migration:"," two nullable integer columns on ",[601,1220,828],{}," (sqlite and postgresql schemas). Every host\ngenerates and commits a migration, as with ",[601,1223,1224],{},"isListed"," in 0.4.1. No data changes: ",[601,1227,801],{}," inherits the global policy,\nwhich defaults to today's 30 days idle.",[783,1230,1231,608,1234,1236,1237,1239,1240,1242,1243,1245],{},[596,1232,1233],{},"Release:",[601,1235,603],{}," 0.5.0 (schema and options), ",[601,1238,614],{}," minor (the hook),\n",[601,1241,618],{}," (fires it) and ",[601,1244,622],{}," minor (Signed-in apps). Each works without the others:\nwithout the hook nothing revokes on a password change, without Account the API is still there.",[783,1247,1248,1251,1252,1254],{},[596,1249,1250],{},"Security:"," a stolen refresh token from a native app now lives up to ",[601,1253,787],{}," if the thief keeps refreshing it.\nRotation still detects a second user of the same token and kills the family, and the user can end it from Security.\nAccess tokens stay at 15 minutes, so resource servers that cannot revoke are unaffected.",[783,1256,1257,1260],{},[596,1258,1259],{},"Load:"," none. The policy is a read on the application row, which the token endpoint already loads.",[783,1262,1263,1266,1267,1269],{},[596,1264,1265],{},"A policy that shrinks:"," a sign-in already past a new, shorter ",[601,1268,793],{}," is refused at its next refresh, even if its\ncurrent token was issued a minute ago, rather than handed a token that is born expired.",[631,1271,1273],{"id":1272},"alternatives-considered","Alternatives considered",[780,1275,1276,1282,1295,1301],{},[783,1277,1278,1281],{},[596,1279,1280],{},"Raise the global lifetime."," One line, but it gives browser SPAs the same six months. Rejected.",[783,1283,1284,1291,1292,1294],{},[596,1285,1286,1287,1290],{},"Lifetime by ",[601,1288,1289],{},"clientType"," only (public \u002F confidential)."," Public covers both native apps (safe storage) and SPAs\n(",[601,1293,726],{},"), the two extremes. Rejected in favour of a per-application value with guardrails by kind.",[783,1296,1297,1300],{},[596,1298,1299],{},"Non-expiring tokens for first-party apps."," What large consumer apps effectively do, but they have device\nmanagement, anomaly detection and support staff behind it. A 2-year ceiling with sliding renewal gives the same\nexperience for anyone who opens the app at all.",[783,1302,1303,1306,1307,1310],{},[596,1304,1305],{},"Re-authentication with biometrics instead."," Complementary, not a replacement. It belongs in ",[601,1308,1309],{},"@frigear-nu\u002Fmobile","\nas an app lock and does not change server lifetimes.",[631,1312,1314],{"id":1313},"tests","Tests",[780,1316,1317,1325,1328,1335,1340,1347,1352],{},[783,1318,1319,1321,1322,1324],{},[601,1320,652],{}," uses application, then configured client, then global policy; ",[601,1323,793],{}," caps a sliding window.",[783,1326,1327],{},"Rotation after a policy change gives the new row the new lifetime.",[783,1329,1330,1331,1334],{},"A token past ",[601,1332,1333],{},"authTime + maxAge"," is refused even if it was rotated a minute ago.",[783,1336,1337,1339],{},[601,1338,640],{}," alone still produces today's 30-day behaviour.",[783,1341,1342,1343,1346],{},"Browser-kind client above 7 days warns; ",[601,1344,1345],{},"allowLongBrowserSessions"," silences it; values above 2 years are refused.",[783,1348,1349,1350,629],{},"Sessions API lists only the caller's families, revokes one \u002F all others, and fires ",[601,1351,1111],{},[783,1353,1354,1356],{},[601,1355,1158],{}," revokes every family of that user and none of anyone else's.",[1358,1359,1360],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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);}",{"title":878,"searchDepth":899,"depth":899,"links":1362},[1363,1364,1370,1371,1372],{"id":633,"depth":899,"text":634},{"id":769,"depth":899,"text":770,"children":1365},[1366,1367,1368,1369],{"id":774,"depth":941,"text":775},{"id":1065,"depth":941,"text":1066},{"id":1100,"depth":941,"text":1101},{"id":1194,"depth":941,"text":1195},{"id":1209,"depth":899,"text":1210},{"id":1272,"depth":899,"text":1273},{"id":1313,"depth":899,"text":1314},"Why refresh-token lifetimes move from one global setting to a policy per application, how a first-party native app stays signed in for months, and what users and admins get to end those sessions.","md",null,{},{"icon":582},{"title":579,"description":1373},"XhYgdne6dFfnMtaueOxwjPQBxCPYwYSNniz0jU7PL0A",[1381,1383],{"title":572,"path":573,"stem":574,"description":1382,"icon":577,"children":-1},"Decision records - why a part of the workspace works the way it does, what else was considered, and what it costs.",{"title":584,"path":585,"stem":586,"description":1384,"icon":70,"children":-1},"Why abilities, role-name checks, RBAC scopes and per-module authorize hooks become one model of permissions, roles, scopes and policies, how a check is evaluated, and what fails closed.",1794503153702]