[{"data":1,"prerenderedAt":3563},["ShallowReactive",2],{"navigation-landing-en":3,"navigation-nuxt-auto-en":180,"navigation-nuxt-protokit-en":365,"navigation-nuxt-cookie-consent-en":507,"\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fpermissions-business-logic-en":549,"\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fpermissions-business-logic-surround-en":3558},[4,8,13,28,41,51,64,77,94,110,134,150,157,172],{"title":5,"path":6,"stem":7},"Overview","\u002Fdocs\u002Flanding","0.docs\u002F1.landing\u002F001.index",{"title":9,"path":10,"stem":11,"badge":12},"Built-in Features","\u002Fdocs\u002Flanding\u002Fbuilt-in-features","0.docs\u002F1.landing\u002F002.built-in-features","New",{"title":14,"path":15,"stem":16,"children":17,"icon":27},"Content Foundation","\u002Fdocs\u002Flanding\u002Fcontent","0.docs\u002F1.landing\u002F02.content\u002F1.index",[18,19,23],{"title":5,"path":15,"stem":16},{"title":20,"path":21,"stem":22},"Details","\u002Fdocs\u002Flanding\u002Fcontent\u002Fdetails","0.docs\u002F1.landing\u002F02.content\u002F2.details",{"title":24,"path":25,"stem":26},"Technical","\u002Fdocs\u002Flanding\u002Fcontent\u002Ftechnical","0.docs\u002F1.landing\u002F02.content\u002F4.technical","i-heroicons-document-text",{"title":29,"path":30,"stem":31,"children":32,"icon":40},"Regional Content","\u002Fdocs\u002Flanding\u002Fregional","0.docs\u002F1.landing\u002F03.regional\u002F1.index",[33,34,37],{"title":5,"path":30,"stem":31},{"title":20,"path":35,"stem":36},"\u002Fdocs\u002Flanding\u002Fregional\u002Fdetails","0.docs\u002F1.landing\u002F03.regional\u002F2.details",{"title":24,"path":38,"stem":39},"\u002Fdocs\u002Flanding\u002Fregional\u002Ftechnical","0.docs\u002F1.landing\u002F03.regional\u002F4.technical","i-heroicons-globe-alt",{"title":42,"path":43,"stem":44,"children":45,"icon":50},"Multi-language","\u002Fdocs\u002Flanding\u002Fmultilang","0.docs\u002F1.landing\u002F04.multilang\u002F1.index",[46,47],{"title":5,"path":43,"stem":44},{"title":20,"path":48,"stem":49},"\u002Fdocs\u002Flanding\u002Fmultilang\u002Fdetails","0.docs\u002F1.landing\u002F04.multilang\u002F2.details","i-heroicons-language",{"title":52,"path":53,"stem":54,"children":55,"icon":63},"Blog","\u002Fdocs\u002Flanding\u002Fblog","0.docs\u002F1.landing\u002F05.blog\u002F1.index",[56,57,60],{"title":5,"path":53,"stem":54},{"title":20,"path":58,"stem":59},"\u002Fdocs\u002Flanding\u002Fblog\u002Fdetails","0.docs\u002F1.landing\u002F05.blog\u002F2.details",{"title":24,"path":61,"stem":62},"\u002Fdocs\u002Flanding\u002Fblog\u002Ftechnical","0.docs\u002F1.landing\u002F05.blog\u002F4.technical","i-heroicons-pencil-square",{"title":65,"path":66,"stem":67,"children":68,"icon":76},"Documentation","\u002Fdocs\u002Flanding\u002Fdocs","0.docs\u002F1.landing\u002F06.docs\u002F1.index",[69,70,73],{"title":5,"path":66,"stem":67},{"title":20,"path":71,"stem":72},"\u002Fdocs\u002Flanding\u002Fdocs\u002Fdetails","0.docs\u002F1.landing\u002F06.docs\u002F2.details",{"title":24,"path":74,"stem":75},"\u002Fdocs\u002Flanding\u002Fdocs\u002Ftechnical","0.docs\u002F1.landing\u002F06.docs\u002F4.technical","i-heroicons-book-open",{"title":78,"path":79,"stem":80,"children":81,"icon":93},"Forms","\u002Fdocs\u002Flanding\u002Fforms","0.docs\u002F1.landing\u002F07.forms\u002F1.index",[82,83,86,90],{"title":5,"path":79,"stem":80},{"title":20,"path":84,"stem":85},"\u002Fdocs\u002Flanding\u002Fforms\u002Fdetails","0.docs\u002F1.landing\u002F07.forms\u002F2.details",{"title":87,"path":88,"stem":89},"Admin","\u002Fdocs\u002Flanding\u002Fforms\u002Fadmin","0.docs\u002F1.landing\u002F07.forms\u002F3.admin",{"title":24,"path":91,"stem":92},"\u002Fdocs\u002Flanding\u002Fforms\u002Ftechnical","0.docs\u002F1.landing\u002F07.forms\u002F4.technical","i-heroicons-clipboard-document-list",{"title":95,"path":96,"stem":97,"children":98,"icon":109},"Email","\u002Fdocs\u002Flanding\u002Femail","0.docs\u002F1.landing\u002F08.email\u002F1.index",[99,100,103,106],{"title":5,"path":96,"stem":97},{"title":20,"path":101,"stem":102},"\u002Fdocs\u002Flanding\u002Femail\u002Fdetails","0.docs\u002F1.landing\u002F08.email\u002F2.details",{"title":87,"path":104,"stem":105},"\u002Fdocs\u002Flanding\u002Femail\u002Fadmin","0.docs\u002F1.landing\u002F08.email\u002F3.admin",{"title":24,"path":107,"stem":108},"\u002Fdocs\u002Flanding\u002Femail\u002Ftechnical","0.docs\u002F1.landing\u002F08.email\u002F4.technical","i-heroicons-envelope",{"title":111,"path":112,"stem":113,"children":114,"icon":133},"Feedback Platform","\u002Fdocs\u002Flanding\u002Ffeedback","0.docs\u002F1.landing\u002F09.feedback\u002F1.index",[115,116,119,122,125,129],{"title":5,"path":112,"stem":113},{"title":20,"path":117,"stem":118},"\u002Fdocs\u002Flanding\u002Ffeedback\u002Fdetails","0.docs\u002F1.landing\u002F09.feedback\u002F2.details",{"title":87,"path":120,"stem":121},"\u002Fdocs\u002Flanding\u002Ffeedback\u002Fadmin","0.docs\u002F1.landing\u002F09.feedback\u002F3.admin",{"title":24,"path":123,"stem":124},"\u002Fdocs\u002Flanding\u002Ffeedback\u002Ftechnical","0.docs\u002F1.landing\u002F09.feedback\u002F4.technical",{"title":126,"path":127,"stem":128},"Compare vs SaaS","\u002Fdocs\u002Flanding\u002Ffeedback\u002Fcompare","0.docs\u002F1.landing\u002F09.feedback\u002F5.compare",{"title":130,"path":131,"stem":132},"FAQ","\u002Fdocs\u002Flanding\u002Ffeedback\u002Ffaq","0.docs\u002F1.landing\u002F09.feedback\u002F6.faq","i-heroicons-chat-bubble-left-right",{"title":135,"path":136,"stem":137,"children":138,"icon":149},"Storage","\u002Fdocs\u002Flanding\u002Fstorage","0.docs\u002F1.landing\u002F10.storage\u002F1.index",[139,140,143,146],{"title":5,"path":136,"stem":137},{"title":20,"path":141,"stem":142},"\u002Fdocs\u002Flanding\u002Fstorage\u002Fdetails","0.docs\u002F1.landing\u002F10.storage\u002F2.details",{"title":87,"path":144,"stem":145},"\u002Fdocs\u002Flanding\u002Fstorage\u002Fadmin","0.docs\u002F1.landing\u002F10.storage\u002F3.admin",{"title":24,"path":147,"stem":148},"\u002Fdocs\u002Flanding\u002Fstorage\u002Ftechnical","0.docs\u002F1.landing\u002F10.storage\u002F4.technical","i-heroicons-circle-stack",{"title":151,"path":152,"stem":153,"children":154,"icon":156},"Offline First","\u002Fdocs\u002Flanding\u002Foffline-first","0.docs\u002F1.landing\u002F11.offline-first\u002F1.index",[155],{"title":151,"path":152,"stem":153},"i-heroicons-users",{"title":158,"path":159,"stem":160,"children":161,"icon":156},"Yjs Sync","\u002Fdocs\u002Flanding\u002Fyjs-sync","0.docs\u002F1.landing\u002F12.yjs-sync\u002F1.index",[162,163,166,169],{"title":5,"path":159,"stem":160},{"title":20,"path":164,"stem":165},"\u002Fdocs\u002Flanding\u002Fyjs-sync\u002Fdetails","0.docs\u002F1.landing\u002F12.yjs-sync\u002F2.details",{"title":87,"path":167,"stem":168},"\u002Fdocs\u002Flanding\u002Fyjs-sync\u002Fadmin","0.docs\u002F1.landing\u002F12.yjs-sync\u002F3.admin",{"title":24,"path":170,"stem":171},"\u002Fdocs\u002Flanding\u002Fyjs-sync\u002Ftechnical","0.docs\u002F1.landing\u002F12.yjs-sync\u002F4.technical",{"title":173,"path":174,"stem":175,"children":176,"badge":178,"icon":179},"Newsletter","\u002Fdocs\u002Flanding\u002Fnewsletter","0.docs\u002F1.landing\u002F13.newsletter\u002Findex",[177],{"title":173,"path":174,"stem":175,"badge":178},"Coming Soon","i-lucide-send",[181,184,202,318,358],{"title":5,"path":182,"stem":183},"\u002Fdocs\u002Fnuxt-auto","0.docs\u002F3.nuxt-auto\u002Findex",{"title":185,"path":186,"stem":187,"children":188,"page":201},"Getting Started","\u002Fdocs\u002Fnuxt-auto\u002Fgetting-started","0.docs\u002F3.nuxt-auto\u002F1.getting-started",[189,193,197],{"title":190,"path":191,"stem":192},"Introduction","\u002Fdocs\u002Fnuxt-auto\u002Fgetting-started\u002Fintroduction","0.docs\u002F3.nuxt-auto\u002F1.getting-started\u002F1.introduction",{"title":194,"path":195,"stem":196},"Installation","\u002Fdocs\u002Fnuxt-auto\u002Fgetting-started\u002Finstallation","0.docs\u002F3.nuxt-auto\u002F1.getting-started\u002F2.installation",{"title":198,"path":199,"stem":200},"Quick Start","\u002Fdocs\u002Fnuxt-auto\u002Fgetting-started\u002Fquick-start","0.docs\u002F3.nuxt-auto\u002F1.getting-started\u002F3.quick-start",false,{"title":203,"path":204,"stem":205,"children":206,"page":201},"Auto API","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api","0.docs\u002F3.nuxt-auto\u002F2.auto-api",[207,210,214,218,222,226,230,234,238,242,246,250,254,258,262,266,270,274,278,282,286,290,294,298,302,306,310,314],{"title":185,"path":208,"stem":209},"\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fgetting-started","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F1.getting-started",{"title":211,"path":212,"stem":213},"Aggregations","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Faggregations","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F10.aggregations",{"title":215,"path":216,"stem":217},"Lifecycle Hooks","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Flifecycle-hooks","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F11.lifecycle-hooks",{"title":219,"path":220,"stem":221},"Many-to-Many (M2M) Relationships","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fm2m-relationships","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F12.m2m-relationships",{"title":223,"path":224,"stem":225},"Plugin System","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fplugin-system","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F13.plugin-system",{"title":227,"path":228,"stem":229},"Database Adapters","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fdatabase-adapters","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F14.database-adapters",{"title":231,"path":232,"stem":233},"Custom Endpoints","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fcustom-endpoints","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F15.custom-endpoints",{"title":235,"path":236,"stem":237},"Multi-Tenancy","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fmulti-tenancy","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F16.multi-tenancy",{"title":239,"path":240,"stem":241},"Schema Presets (Cross-engine)","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fschema-presets","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F17.schema-presets",{"title":243,"path":244,"stem":245},"Permissions Cookbook","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fpermissions-cookbook","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F18.permissions-cookbook",{"title":247,"path":248,"stem":249},"Billing & Plan Gating","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fbilling","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F19.billing",{"title":251,"path":252,"stem":253},"Validation","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fvalidation","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F2.validation",{"title":255,"path":256,"stem":257},"Rate Limiting","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Frate-limiting","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F20.rate-limiting",{"title":259,"path":260,"stem":261},"Request Metadata Plugin","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Frequest-metadata","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F21.request-metadata",{"title":263,"path":264,"stem":265},"Plugin Catalog","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fplugin-catalog","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F22.plugin-catalog",{"title":267,"path":268,"stem":269},"Configuration Reference","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fconfiguration","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F23.configuration",{"title":271,"path":272,"stem":273},"Handler Overrides","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fhandler-overrides","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F3.handler-overrides",{"title":275,"path":276,"stem":277},"Cloudflare D1","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fcloudflare-d1","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F30.cloudflare-d1",{"title":279,"path":280,"stem":281},"SQLite to D1 Migration","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fmigration-sqlite-d1","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F31.migration-sqlite-d1",{"title":283,"path":284,"stem":285},"Frontend Composables","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Ffrontend-composables","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F32.frontend-composables",{"title":287,"path":288,"stem":289},"Testing","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Ftesting","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F33.testing",{"title":291,"path":292,"stem":293},"Pagination","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fpagination","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F4.pagination",{"title":295,"path":296,"stem":297},"Upgrading","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fupgrading","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F40.upgrading",{"title":299,"path":300,"stem":301},"Soft Deletes","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fsoft-deletes","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F5.soft-deletes",{"title":303,"path":304,"stem":305},"Authentication & Authorization","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fauthentication-authorization","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F6.authentication-authorization",{"title":307,"path":308,"stem":309},"Better-Auth Integration","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fbetter-auth","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F7.better-auth",{"title":311,"path":312,"stem":313},"Nested Relations","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fnested-relationships","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F8.nested-relationships",{"title":315,"path":316,"stem":317},"Bulk Operations","\u002Fdocs\u002Fnuxt-auto\u002Fauto-api\u002Fbulk-operations","0.docs\u002F3.nuxt-auto\u002F2.auto-api\u002F9.bulk-operations",{"title":319,"path":320,"stem":321,"children":322,"page":201},"Auto Admin","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin","0.docs\u002F3.nuxt-auto\u002F3.auto-admin",[323,326,330,334,338,342,346,350,354],{"title":185,"path":324,"stem":325},"\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fgetting-started","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F1.getting-started",{"title":327,"path":328,"stem":329},"Configuration & Theming","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fconfiguration-theming","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F2.configuration-theming",{"title":331,"path":332,"stem":333},"Resource Configuration","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fresource-configuration","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F3.resource-configuration",{"title":335,"path":336,"stem":337},"Form Fields & Widgets","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fform-fields-widgets","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F4.form-fields-widgets",{"title":339,"path":340,"stem":341},"Permissions","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fpermissions","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F5.permissions",{"title":343,"path":344,"stem":345},"Custom Pages","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fcustom-pages","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F6.custom-pages",{"title":347,"path":348,"stem":349},"M2M Relationships","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fm2m-relationships","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F7.m2m-relationships",{"title":351,"path":352,"stem":353},"Custom Actions","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fcustom-actions","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F8.custom-actions",{"title":355,"path":356,"stem":357},"Composables","\u002Fdocs\u002Fnuxt-auto\u002Fauto-admin\u002Fcomposables","0.docs\u002F3.nuxt-auto\u002F3.auto-admin\u002F9.composables",{"title":359,"path":360,"stem":361,"children":362,"icon":364},"Claude Code Skills","\u002Fdocs\u002Fnuxt-auto\u002Fclaude-code-skills","0.docs\u002F3.nuxt-auto\u002F4.claude-code-skills\u002F1.index",[363],{"title":5,"path":360,"stem":361},"i-lucide-bot",[366,369,382,409,427,450,472,502],{"title":5,"path":367,"stem":368},"\u002Fdocs\u002Fnuxt-protokit","0.docs\u002F4.nuxt-protokit\u002Findex",{"title":185,"path":370,"stem":371,"children":372,"icon":381},"\u002Fdocs\u002Fnuxt-protokit\u002Fgetting-started","0.docs\u002F4.nuxt-protokit\u002F1.getting-started\u002F1.index",[373,374,378],{"title":190,"path":370,"stem":371},{"title":375,"path":376,"stem":377},"Core Concepts","\u002Fdocs\u002Fnuxt-protokit\u002Fgetting-started\u002Fconcepts","0.docs\u002F4.nuxt-protokit\u002F1.getting-started\u002F2.concepts",{"title":198,"path":379,"stem":380},"\u002Fdocs\u002Fnuxt-protokit\u002Fgetting-started\u002Fquick-start","0.docs\u002F4.nuxt-protokit\u002F1.getting-started\u002F3.quick-start","i-lucide-rocket",{"title":383,"path":384,"stem":385,"children":386,"icon":408},"Schemas","\u002Fdocs\u002Fnuxt-protokit\u002Fschemas","0.docs\u002F4.nuxt-protokit\u002F2.schemas\u002F1.index",[387,388,392,396,400,404],{"title":5,"path":384,"stem":385},{"title":389,"path":390,"stem":391},"Field Types","\u002Fdocs\u002Fnuxt-protokit\u002Fschemas\u002Ffields","0.docs\u002F4.nuxt-protokit\u002F2.schemas\u002F2.fields",{"title":393,"path":394,"stem":395},"Collections","\u002Fdocs\u002Fnuxt-protokit\u002Fschemas\u002Fcollections","0.docs\u002F4.nuxt-protokit\u002F2.schemas\u002F3.collections",{"title":397,"path":398,"stem":399},"Derived & Computed","\u002Fdocs\u002Fnuxt-protokit\u002Fschemas\u002Fderived-computed","0.docs\u002F4.nuxt-protokit\u002F2.schemas\u002F4.derived-computed",{"title":401,"path":402,"stem":403},"Connections","\u002Fdocs\u002Fnuxt-protokit\u002Fschemas\u002Fconnections","0.docs\u002F4.nuxt-protokit\u002F2.schemas\u002F5.connections",{"title":405,"path":406,"stem":407},"Visualizations & Layouts","\u002Fdocs\u002Fnuxt-protokit\u002Fschemas\u002Fvisualizations","0.docs\u002F4.nuxt-protokit\u002F2.schemas\u002F6.visualizations","i-lucide-file-code",{"title":355,"path":410,"stem":411,"children":412,"icon":426},"\u002Fdocs\u002Fnuxt-protokit\u002Fcomposables","0.docs\u002F4.nuxt-protokit\u002F3.composables\u002F1.index",[413,414,418,422],{"title":5,"path":410,"stem":411},{"title":415,"path":416,"stem":417},"usePrototype","\u002Fdocs\u002Fnuxt-protokit\u002Fcomposables\u002Fuse-prototype","0.docs\u002F4.nuxt-protokit\u002F3.composables\u002F2.use-prototype",{"title":419,"path":420,"stem":421},"useProtoDoc","\u002Fdocs\u002Fnuxt-protokit\u002Fcomposables\u002Fuse-proto-doc","0.docs\u002F4.nuxt-protokit\u002F3.composables\u002F3.use-proto-doc",{"title":423,"path":424,"stem":425},"useProtoCollection","\u002Fdocs\u002Fnuxt-protokit\u002Fcomposables\u002Fuse-proto-collection","0.docs\u002F4.nuxt-protokit\u002F3.composables\u002F4.use-proto-collection","i-lucide-layers",{"title":428,"path":429,"stem":430,"children":431,"icon":449},"Components","\u002Fdocs\u002Fnuxt-protokit\u002Fcomponents","0.docs\u002F4.nuxt-protokit\u002F4.components\u002F1.index",[432,433,437,441,445],{"title":5,"path":429,"stem":430},{"title":434,"path":435,"stem":436},"ProtoTool","\u002Fdocs\u002Fnuxt-protokit\u002Fcomponents\u002Fproto-tool","0.docs\u002F4.nuxt-protokit\u002F4.components\u002F2.proto-tool",{"title":438,"path":439,"stem":440},"ProtoCrudModal","\u002Fdocs\u002Fnuxt-protokit\u002Fcomponents\u002Fproto-crud-modal","0.docs\u002F4.nuxt-protokit\u002F4.components\u002F3.proto-crud-modal",{"title":442,"path":443,"stem":444},"ProtoCalendar","\u002Fdocs\u002Fnuxt-protokit\u002Fcomponents\u002Fproto-calendar","0.docs\u002F4.nuxt-protokit\u002F4.components\u002F4.proto-calendar",{"title":446,"path":447,"stem":448},"ProtoItemPanel variants","\u002Fdocs\u002Fnuxt-protokit\u002Fcomponents\u002Fproto-item-panels","0.docs\u002F4.nuxt-protokit\u002F4.components\u002F5.proto-item-panels","i-lucide-puzzle",{"title":151,"path":451,"stem":452,"children":453,"icon":471},"\u002Fdocs\u002Fnuxt-protokit\u002Foffline-first","0.docs\u002F4.nuxt-protokit\u002F5.offline-first\u002F1.index",[454,455,459,463,467],{"title":151,"path":451,"stem":452},{"title":456,"path":457,"stem":458},"Corruption Recovery","\u002Fdocs\u002Fnuxt-protokit\u002Foffline-first\u002Fcorruption-recovery","0.docs\u002F4.nuxt-protokit\u002F5.offline-first\u002F2.corruption-recovery",{"title":460,"path":461,"stem":462},"Size & GC","\u002Fdocs\u002Fnuxt-protokit\u002Foffline-first\u002Fdocument-size-and-gc","0.docs\u002F4.nuxt-protokit\u002F5.offline-first\u002F3.document-size-and-gc",{"title":464,"path":465,"stem":466},"Multi-tenant Isolation","\u002Fdocs\u002Fnuxt-protokit\u002Foffline-first\u002Fmulti-tenant-isolation","0.docs\u002F4.nuxt-protokit\u002F5.offline-first\u002F4.multi-tenant-isolation",{"title":468,"path":469,"stem":470},"Encryption at Rest","\u002Fdocs\u002Fnuxt-protokit\u002Foffline-first\u002Fencryption","0.docs\u002F4.nuxt-protokit\u002F5.offline-first\u002F5.encryption","i-lucide-wifi-off",{"title":473,"icon":474,"path":475,"stem":476,"children":477},"Advanced","i-lucide-graduation-cap","\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F1.index",[478,479,483,487,491,494,498],{"title":5,"path":475,"stem":476},{"title":480,"path":481,"stem":482},"Multi-Tool Apps","\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fbuilding-a-toolkit","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F1.building-a-toolkit",{"title":484,"path":485,"stem":486},"Schema Patterns","\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fcustom-schema-patterns","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F2.custom-schema-patterns",{"title":488,"path":489,"stem":490},"Custom Fields & Viz","\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fextensibility","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F3.extensibility",{"title":339,"path":492,"stem":493},"\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fpermissions","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F4.permissions",{"title":495,"path":496,"stem":497},"Business Logic Guards","\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fpermissions-business-logic","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F5.permissions-business-logic",{"title":499,"path":500,"stem":501},"Headless Mode","\u002Fdocs\u002Fnuxt-protokit\u002Fadvanced\u002Fheadless-mode","0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F6.headless-mode",{"title":359,"path":503,"stem":504,"children":505,"icon":364},"\u002Fdocs\u002Fnuxt-protokit\u002Fclaude-code-skills","0.docs\u002F4.nuxt-protokit\u002F7.claude-code-skills\u002F1.index",[506],{"title":5,"path":503,"stem":504},[508,511],{"title":5,"path":509,"stem":510},"\u002Fdocs\u002Fnuxt-cookie-consent","0.docs\u002F5.nuxt-cookie-consent\u002Findex",{"title":512,"icon":513,"path":514,"stem":515,"children":516,"page":201},"Guide","i-lucide-book-open","\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide",[517,521,525,530,535,539,544],{"title":190,"path":518,"stem":519,"icon":520},"\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Fintroduction","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F1.introduction","i-lucide-info",{"title":194,"path":522,"stem":523,"icon":524},"\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Finstallation","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F2.installation","i-lucide-download",{"title":526,"path":527,"stem":528,"icon":529},"Configuration","\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Fconfiguration","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F3.configuration","i-lucide-settings-2",{"title":531,"path":532,"stem":533,"icon":534},"Usage","\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Fusage","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F4.usage","i-lucide-play",{"title":355,"path":536,"stem":537,"icon":538},"\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Fcomposables","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F5.composables","i-lucide-function-square",{"title":540,"path":541,"stem":542,"icon":543},"Components & overriding","\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Fcomponents","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F6.components","i-lucide-component",{"title":545,"path":546,"stem":547,"icon":548},"Internationalization","\u002Fdocs\u002Fnuxt-cookie-consent\u002Fguide\u002Fi18n","0.docs\u002F5.nuxt-cookie-consent\u002F1.guide\u002F7.i18n","i-lucide-languages",{"page":550,"fallbackPage":3553},{"id":551,"title":552,"body":553,"description":3551,"extension":3552,"links":3553,"meta":3554,"navigation":3555,"ogImage":3553,"path":496,"seo":3556,"stem":497,"__hash__":3557},"nuxt_protokit\u002F0.docs\u002F4.nuxt-protokit\u002F6.advanced\u002F5.permissions-business-logic.md","Business Logic in Permission Guards",{"type":554,"value":555,"toc":3534},"minimark",[556,560,578,585,601,606,619,803,810,813,817,830,1133,1144,1261,1265,1272,1411,1510,1514,1519,1522,1676,1687,1694,1698,1701,1914,1922,1926,1933,2003,2061,2065,2071,2255,2271,2278,2281,2284,2288,2295,2301,2308,2312,2315,2328,2335,2346,2350,2364,2372,2382,2387,2402,2406,2417,3195,3198,3209,3213,3216,3361,3364,3448,3451,3455,3524,3530],[557,558,552],"h1",{"id":559},"business-logic-in-permission-guards",[561,562,563,564,568,569,572,573,577],"p",{},"Role guards answer the question ",[565,566,567],"em",{},"who"," can do something. Business logic answers ",[565,570,571],{},"when"," something can be done. The two concerns are distinct, and nuxt-protokit keeps them that way deliberately — schema permissions handle roles, while Vue's ",[574,575,576],"code",{},"computed()"," handles everything else.",[561,579,580],{},[581,582],"img",{"alt":583,"src":584},"Role guards and business logic for the viewer role: read-only fields, hidden budget and notes, the effective permission table","\u002Fscreenshots\u002Fnuxt-protokit\u002Fpermissions-1440.png",[586,587,588],"warning",{},[561,589,590,594,595,600],{},[591,592,593],"strong",{},"Frontend guard only."," Everything on this page — role checks, status checks, quota checks, owner comparisons — runs entirely in the browser. A user with devtools access can modify any Y.js value (project status, item owner, array contents) directly, bypassing every computed flag described here. Business logic guards control the UI. They do not enforce data integrity. Read the ",[596,597,599],"a",{"href":598},"#limitations","Limitations"," section before relying on these patterns for sensitive operations.",[602,603,605],"h2",{"id":604},"the-two-layers","The two layers",[561,607,608,609,611,612,614,615,618],{},"When ",[574,610,415],{}," or ",[574,613,423],{}," returns ",[574,616,617],{},"collectionPermissions",", each flag reflects only the role guard defined in the schema. That is intentional. Role guards belong in the schema because they are static rules that do not depend on the current document state.",[620,621,627],"pre",{"className":622,"code":623,"filename":624,"language":625,"meta":626,"style":626},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","collections: {\n  tasks: {\n    permissions: {\n      read:   ['admin', 'editor', 'viewer'],\n      create: ['admin', 'editor'],\n      update: ['admin', 'editor'],\n      delete: 'admin',\n    },\n  },\n}\n","schemas\u002Fproject.ts","typescript","",[574,628,629,645,655,665,712,741,769,785,791,797],{"__ignoreMap":626},[630,631,634,638,642],"span",{"class":632,"line":633},"line",1,[630,635,637],{"class":636},"sBMFI","collections",[630,639,641],{"class":640},"sMK4o",":",[630,643,644],{"class":640}," {\n",[630,646,648,651,653],{"class":632,"line":647},2,[630,649,650],{"class":636},"  tasks",[630,652,641],{"class":640},[630,654,644],{"class":640},[630,656,658,661,663],{"class":632,"line":657},3,[630,659,660],{"class":636},"    permissions",[630,662,641],{"class":640},[630,664,644],{"class":640},[630,666,668,671,673,677,680,684,686,689,692,695,697,699,701,704,706,709],{"class":632,"line":667},4,[630,669,670],{"class":636},"      read",[630,672,641],{"class":640},[630,674,676],{"class":675},"swJcz","   [",[630,678,679],{"class":640},"'",[630,681,683],{"class":682},"sfazB","admin",[630,685,679],{"class":640},[630,687,688],{"class":640},",",[630,690,691],{"class":640}," '",[630,693,694],{"class":682},"editor",[630,696,679],{"class":640},[630,698,688],{"class":640},[630,700,691],{"class":640},[630,702,703],{"class":682},"viewer",[630,705,679],{"class":640},[630,707,708],{"class":675},"]",[630,710,711],{"class":640},",\n",[630,713,715,718,720,723,725,727,729,731,733,735,737,739],{"class":632,"line":714},5,[630,716,717],{"class":636},"      create",[630,719,641],{"class":640},[630,721,722],{"class":675}," [",[630,724,679],{"class":640},[630,726,683],{"class":682},[630,728,679],{"class":640},[630,730,688],{"class":640},[630,732,691],{"class":640},[630,734,694],{"class":682},[630,736,679],{"class":640},[630,738,708],{"class":675},[630,740,711],{"class":640},[630,742,744,747,749,751,753,755,757,759,761,763,765,767],{"class":632,"line":743},6,[630,745,746],{"class":636},"      update",[630,748,641],{"class":640},[630,750,722],{"class":675},[630,752,679],{"class":640},[630,754,683],{"class":682},[630,756,679],{"class":640},[630,758,688],{"class":640},[630,760,691],{"class":640},[630,762,694],{"class":682},[630,764,679],{"class":640},[630,766,708],{"class":675},[630,768,711],{"class":640},[630,770,772,775,777,779,781,783],{"class":632,"line":771},7,[630,773,774],{"class":636},"      delete",[630,776,641],{"class":640},[630,778,691],{"class":640},[630,780,683],{"class":682},[630,782,679],{"class":640},[630,784,711],{"class":640},[630,786,788],{"class":632,"line":787},8,[630,789,790],{"class":640},"    },\n",[630,792,794],{"class":632,"line":793},9,[630,795,796],{"class":640},"  },\n",[630,798,800],{"class":632,"line":799},10,[630,801,802],{"class":640},"}\n",[561,804,805,806,809],{},"Business logic — \"tasks cannot be modified once the project is completed\" — is a separate concern. It depends on a runtime value (",[574,807,808],{},"state.status","), which changes while the user interacts with the app. Putting runtime state into a static schema definition couples two things that should stay apart.",[561,811,812],{},"The solution is to compose a new set of reactive flags on top of the schema-derived ones.",[602,814,816],{"id":815},"the-computed-composition-pattern","The computed composition pattern",[561,818,819,820,822,823,825,826,829],{},"After calling ",[574,821,415],{},", wrap the schema permissions with ",[574,824,576],{}," calls that add your business rules. Both layers are Vue ",[574,827,828],{},"ComputedRef\u003Cboolean>"," values, so they compose naturally.",[620,831,834],{"className":622,"code":832,"filename":833,"language":625,"meta":626,"style":626},"const { state, collections, collectionPermissions } = usePrototype(schema)\n\n\u002F\u002F Schema-derived (role only)\nconst roleTaskPerms = collectionPermissions.tasks\n\n\u002F\u002F Effective permissions — role AND status must both allow the action\nconst taskPerms = {\n  canRead: roleTaskPerms.canRead,\n  canCreate: computed(() =>\n    roleTaskPerms.canCreate.value && state.status.value !== 'completed',\n  ),\n  canUpdate: computed(() =>\n    roleTaskPerms.canUpdate.value && state.status.value === 'active',\n  ),\n  canDelete: computed(() =>\n    roleTaskPerms.canDelete.value && state.status.value === 'active',\n  ),\n}\n","pages\u002Fproject.vue",[574,835,836,872,878,884,903,907,912,923,940,959,1000,1008,1024,1062,1069,1085,1121,1128],{"__ignoreMap":626},[630,837,838,842,845,849,851,854,856,859,862,865,869],{"class":632,"line":633},[630,839,841],{"class":840},"spNyl","const",[630,843,844],{"class":640}," {",[630,846,848],{"class":847},"sTEyZ"," state",[630,850,688],{"class":640},[630,852,853],{"class":847}," collections",[630,855,688],{"class":640},[630,857,858],{"class":847}," collectionPermissions ",[630,860,861],{"class":640},"}",[630,863,864],{"class":640}," =",[630,866,868],{"class":867},"s2Zo4"," usePrototype",[630,870,871],{"class":847},"(schema)\n",[630,873,874],{"class":632,"line":647},[630,875,877],{"emptyLinePlaceholder":876},true,"\n",[630,879,880],{"class":632,"line":657},[630,881,883],{"class":882},"sHwdD","\u002F\u002F Schema-derived (role only)\n",[630,885,886,888,891,894,897,900],{"class":632,"line":667},[630,887,841],{"class":840},[630,889,890],{"class":847}," roleTaskPerms ",[630,892,893],{"class":640},"=",[630,895,896],{"class":847}," collectionPermissions",[630,898,899],{"class":640},".",[630,901,902],{"class":847},"tasks\n",[630,904,905],{"class":632,"line":714},[630,906,877],{"emptyLinePlaceholder":876},[630,908,909],{"class":632,"line":743},[630,910,911],{"class":882},"\u002F\u002F Effective permissions — role AND status must both allow the action\n",[630,913,914,916,919,921],{"class":632,"line":771},[630,915,841],{"class":840},[630,917,918],{"class":847}," taskPerms ",[630,920,893],{"class":640},[630,922,644],{"class":640},[630,924,925,928,930,933,935,938],{"class":632,"line":787},[630,926,927],{"class":675},"  canRead",[630,929,641],{"class":640},[630,931,932],{"class":847}," roleTaskPerms",[630,934,899],{"class":640},[630,936,937],{"class":847},"canRead",[630,939,711],{"class":640},[630,941,942,945,947,950,953,956],{"class":632,"line":793},[630,943,944],{"class":675},"  canCreate",[630,946,641],{"class":640},[630,948,949],{"class":867}," computed",[630,951,952],{"class":847},"(",[630,954,955],{"class":640},"()",[630,957,958],{"class":840}," =>\n",[630,960,961,964,966,969,971,974,977,979,981,984,986,988,991,993,996,998],{"class":632,"line":799},[630,962,963],{"class":847},"    roleTaskPerms",[630,965,899],{"class":640},[630,967,968],{"class":847},"canCreate",[630,970,899],{"class":640},[630,972,973],{"class":847},"value ",[630,975,976],{"class":640},"&&",[630,978,848],{"class":847},[630,980,899],{"class":640},[630,982,983],{"class":847},"status",[630,985,899],{"class":640},[630,987,973],{"class":847},[630,989,990],{"class":640},"!==",[630,992,691],{"class":640},[630,994,995],{"class":682},"completed",[630,997,679],{"class":640},[630,999,711],{"class":640},[630,1001,1003,1006],{"class":632,"line":1002},11,[630,1004,1005],{"class":847},"  )",[630,1007,711],{"class":640},[630,1009,1011,1014,1016,1018,1020,1022],{"class":632,"line":1010},12,[630,1012,1013],{"class":675},"  canUpdate",[630,1015,641],{"class":640},[630,1017,949],{"class":867},[630,1019,952],{"class":847},[630,1021,955],{"class":640},[630,1023,958],{"class":840},[630,1025,1027,1029,1031,1034,1036,1038,1040,1042,1044,1046,1048,1050,1053,1055,1058,1060],{"class":632,"line":1026},13,[630,1028,963],{"class":847},[630,1030,899],{"class":640},[630,1032,1033],{"class":847},"canUpdate",[630,1035,899],{"class":640},[630,1037,973],{"class":847},[630,1039,976],{"class":640},[630,1041,848],{"class":847},[630,1043,899],{"class":640},[630,1045,983],{"class":847},[630,1047,899],{"class":640},[630,1049,973],{"class":847},[630,1051,1052],{"class":640},"===",[630,1054,691],{"class":640},[630,1056,1057],{"class":682},"active",[630,1059,679],{"class":640},[630,1061,711],{"class":640},[630,1063,1065,1067],{"class":632,"line":1064},14,[630,1066,1005],{"class":847},[630,1068,711],{"class":640},[630,1070,1072,1075,1077,1079,1081,1083],{"class":632,"line":1071},15,[630,1073,1074],{"class":675},"  canDelete",[630,1076,641],{"class":640},[630,1078,949],{"class":867},[630,1080,952],{"class":847},[630,1082,955],{"class":640},[630,1084,958],{"class":840},[630,1086,1088,1090,1092,1095,1097,1099,1101,1103,1105,1107,1109,1111,1113,1115,1117,1119],{"class":632,"line":1087},16,[630,1089,963],{"class":847},[630,1091,899],{"class":640},[630,1093,1094],{"class":847},"canDelete",[630,1096,899],{"class":640},[630,1098,973],{"class":847},[630,1100,976],{"class":640},[630,1102,848],{"class":847},[630,1104,899],{"class":640},[630,1106,983],{"class":847},[630,1108,899],{"class":640},[630,1110,973],{"class":847},[630,1112,1052],{"class":640},[630,1114,691],{"class":640},[630,1116,1057],{"class":682},[630,1118,679],{"class":640},[630,1120,711],{"class":640},[630,1122,1124,1126],{"class":632,"line":1123},17,[630,1125,1005],{"class":847},[630,1127,711],{"class":640},[630,1129,1131],{"class":632,"line":1130},18,[630,1132,802],{"class":640},[561,1134,1135,1136,1139,1140,1143],{},"You can now use ",[574,1137,1138],{},"taskPerms"," exactly like ",[574,1141,1142],{},"collectionPermissions.tasks"," in your template. When the project status changes, every dependent binding updates automatically — no watchers, no event buses, no manual synchronisation.",[620,1145,1150],{"className":1146,"code":1147,"filename":1148,"language":1149,"meta":626,"style":626},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003CUCheckbox\n  :model-value=\"task.done\"\n  :disabled=\"!taskPerms.canUpdate.value\"\n  @update:model-value=\"tasks.update(i, { done: $event })\"\n\u002F>\n","template","vue",[574,1151,1152,1160,1184,1211,1256],{"__ignoreMap":626},[630,1153,1154,1157],{"class":632,"line":633},[630,1155,1156],{"class":640},"\u003C",[630,1158,1159],{"class":675},"UCheckbox\n",[630,1161,1162,1165,1168,1170,1173,1176,1178,1181],{"class":632,"line":647},[630,1163,1164],{"class":640},"  :",[630,1166,1167],{"class":840},"model-value",[630,1169,893],{"class":640},[630,1171,1172],{"class":640},"\"",[630,1174,1175],{"class":847},"task",[630,1177,899],{"class":640},[630,1179,1180],{"class":847},"done",[630,1182,1183],{"class":640},"\"\n",[630,1185,1186,1188,1191,1193,1195,1198,1200,1202,1204,1206,1209],{"class":632,"line":657},[630,1187,1164],{"class":640},[630,1189,1190],{"class":840},"disabled",[630,1192,893],{"class":640},[630,1194,1172],{"class":640},[630,1196,1197],{"class":640},"!",[630,1199,1138],{"class":847},[630,1201,899],{"class":640},[630,1203,1033],{"class":847},[630,1205,899],{"class":640},[630,1207,1208],{"class":847},"value",[630,1210,1183],{"class":640},[630,1212,1213,1216,1219,1221,1223,1225,1227,1230,1232,1234,1237,1239,1241,1244,1246,1249,1251,1254],{"class":632,"line":667},[630,1214,1215],{"class":640},"  @",[630,1217,1218],{"class":840},"update",[630,1220,641],{"class":640},[630,1222,1167],{"class":840},[630,1224,893],{"class":640},[630,1226,1172],{"class":640},[630,1228,1229],{"class":847},"tasks",[630,1231,899],{"class":640},[630,1233,1218],{"class":867},[630,1235,1236],{"class":847},"(i",[630,1238,688],{"class":640},[630,1240,844],{"class":640},[630,1242,1243],{"class":675}," done",[630,1245,641],{"class":640},[630,1247,1248],{"class":847}," $event ",[630,1250,861],{"class":640},[630,1252,1253],{"class":847},")",[630,1255,1183],{"class":640},[630,1257,1258],{"class":632,"line":714},[630,1259,1260],{"class":847},"\u002F>\n",[602,1262,1264],{"id":1263},"communicating-why-access-is-denied","Communicating why access is denied",[561,1266,1267,1268,1271],{},"Distinguishing ",[565,1269,1270],{},"why"," an action is blocked helps users understand what to do next. A user who lacks the right role needs to contact an administrator. A user who lacks the right project status needs to change the status first. Keep the two reasons separate in your UI.",[620,1273,1275],{"className":622,"code":1274,"language":625,"meta":626,"style":626},"\u002F\u002F Tells the template the status-level reason, regardless of role\nconst statusLockReason = computed\u003Cstring | null>(() => {\n  if (state.status.value === 'completed') return 'Project completed — tasks are frozen'\n  if (state.status.value === 'paused')    return 'Project paused — tasks are read-only'\n  return null\n})\n",[574,1276,1277,1282,1316,1359,1396,1404],{"__ignoreMap":626},[630,1278,1279],{"class":632,"line":633},[630,1280,1281],{"class":882},"\u002F\u002F Tells the template the status-level reason, regardless of role\n",[630,1283,1284,1286,1289,1291,1293,1295,1298,1301,1304,1307,1309,1311,1314],{"class":632,"line":647},[630,1285,841],{"class":840},[630,1287,1288],{"class":847}," statusLockReason ",[630,1290,893],{"class":640},[630,1292,949],{"class":867},[630,1294,1156],{"class":640},[630,1296,1297],{"class":636},"string",[630,1299,1300],{"class":640}," |",[630,1302,1303],{"class":636}," null",[630,1305,1306],{"class":640},">",[630,1308,952],{"class":847},[630,1310,955],{"class":640},[630,1312,1313],{"class":840}," =>",[630,1315,644],{"class":640},[630,1317,1318,1322,1325,1328,1330,1332,1334,1336,1339,1341,1343,1345,1348,1351,1353,1356],{"class":632,"line":657},[630,1319,1321],{"class":1320},"s7zQu","  if",[630,1323,1324],{"class":675}," (",[630,1326,1327],{"class":847},"state",[630,1329,899],{"class":640},[630,1331,983],{"class":847},[630,1333,899],{"class":640},[630,1335,1208],{"class":847},[630,1337,1338],{"class":640}," ===",[630,1340,691],{"class":640},[630,1342,995],{"class":682},[630,1344,679],{"class":640},[630,1346,1347],{"class":675},") ",[630,1349,1350],{"class":1320},"return",[630,1352,691],{"class":640},[630,1354,1355],{"class":682},"Project completed — tasks are frozen",[630,1357,1358],{"class":640},"'\n",[630,1360,1361,1363,1365,1367,1369,1371,1373,1375,1377,1379,1382,1384,1387,1389,1391,1394],{"class":632,"line":667},[630,1362,1321],{"class":1320},[630,1364,1324],{"class":675},[630,1366,1327],{"class":847},[630,1368,899],{"class":640},[630,1370,983],{"class":847},[630,1372,899],{"class":640},[630,1374,1208],{"class":847},[630,1376,1338],{"class":640},[630,1378,691],{"class":640},[630,1380,1381],{"class":682},"paused",[630,1383,679],{"class":640},[630,1385,1386],{"class":675},")    ",[630,1388,1350],{"class":1320},[630,1390,691],{"class":640},[630,1392,1393],{"class":682},"Project paused — tasks are read-only",[630,1395,1358],{"class":640},[630,1397,1398,1401],{"class":632,"line":714},[630,1399,1400],{"class":1320},"  return",[630,1402,1403],{"class":640}," null\n",[630,1405,1406,1408],{"class":632,"line":743},[630,1407,861],{"class":640},[630,1409,1410],{"class":847},")\n",[620,1412,1414],{"className":1146,"code":1413,"filename":1148,"language":1149,"meta":626,"style":626},"\u003Cdiv v-if=\"statusLockReason\" class=\"warning-banner\">\n  {{ statusLockReason }}\n\u003C\u002Fdiv>\n\n\u003Cp v-else-if=\"!taskPerms.canCreate.value\">\n  Your role does not allow adding tasks.\n\u003C\u002Fp>\n",[574,1415,1416,1450,1455,1464,1468,1497,1502],{"__ignoreMap":626},[630,1417,1418,1420,1423,1426,1428,1430,1433,1435,1438,1440,1442,1445,1447],{"class":632,"line":633},[630,1419,1156],{"class":640},[630,1421,1422],{"class":675},"div",[630,1424,1425],{"class":1320}," v-if",[630,1427,893],{"class":640},[630,1429,1172],{"class":640},[630,1431,1432],{"class":847},"statusLockReason",[630,1434,1172],{"class":640},[630,1436,1437],{"class":840}," class",[630,1439,893],{"class":640},[630,1441,1172],{"class":640},[630,1443,1444],{"class":682},"warning-banner",[630,1446,1172],{"class":640},[630,1448,1449],{"class":640},">\n",[630,1451,1452],{"class":632,"line":647},[630,1453,1454],{"class":847},"  {{ statusLockReason }}\n",[630,1456,1457,1460,1462],{"class":632,"line":657},[630,1458,1459],{"class":640},"\u003C\u002F",[630,1461,1422],{"class":675},[630,1463,1449],{"class":640},[630,1465,1466],{"class":632,"line":667},[630,1467,877],{"emptyLinePlaceholder":876},[630,1469,1470,1472,1474,1477,1479,1481,1483,1485,1487,1489,1491,1493,1495],{"class":632,"line":714},[630,1471,1156],{"class":640},[630,1473,561],{"class":675},[630,1475,1476],{"class":1320}," v-else-if",[630,1478,893],{"class":640},[630,1480,1172],{"class":640},[630,1482,1197],{"class":640},[630,1484,1138],{"class":847},[630,1486,899],{"class":640},[630,1488,968],{"class":847},[630,1490,899],{"class":640},[630,1492,1208],{"class":847},[630,1494,1172],{"class":640},[630,1496,1449],{"class":640},[630,1498,1499],{"class":632,"line":743},[630,1500,1501],{"class":847},"  Your role does not allow adding tasks.\n",[630,1503,1504,1506,1508],{"class":632,"line":771},[630,1505,1459],{"class":640},[630,1507,561],{"class":675},[630,1509,1449],{"class":640},[602,1511,1513],{"id":1512},"more-patterns","More patterns",[1515,1516,1518],"h3",{"id":1517},"owner-only-editing","Owner-only editing",[561,1520,1521],{},"A user can always edit their own items, but only admins can edit items belonging to others.",[620,1523,1525],{"className":622,"code":1524,"language":625,"meta":626,"style":626},"const { session } = useAuth()\n\nfunction canEditItem(item: { ownerId: string }) {\n  return computed(() =>\n    collectionPermissions.entries.canUpdate.value\n    && (item.ownerId === session.value?.userId || session.value?.role === 'admin'),\n  )\n}\n",[574,1526,1527,1546,1550,1581,1593,1612,1667,1672],{"__ignoreMap":626},[630,1528,1529,1531,1533,1536,1538,1540,1543],{"class":632,"line":633},[630,1530,841],{"class":840},[630,1532,844],{"class":640},[630,1534,1535],{"class":847}," session ",[630,1537,861],{"class":640},[630,1539,864],{"class":640},[630,1541,1542],{"class":867}," useAuth",[630,1544,1545],{"class":847},"()\n",[630,1547,1548],{"class":632,"line":647},[630,1549,877],{"emptyLinePlaceholder":876},[630,1551,1552,1555,1558,1560,1564,1566,1568,1571,1573,1576,1579],{"class":632,"line":657},[630,1553,1554],{"class":840},"function",[630,1556,1557],{"class":867}," canEditItem",[630,1559,952],{"class":640},[630,1561,1563],{"class":1562},"sHdIc","item",[630,1565,641],{"class":640},[630,1567,844],{"class":640},[630,1569,1570],{"class":675}," ownerId",[630,1572,641],{"class":640},[630,1574,1575],{"class":636}," string",[630,1577,1578],{"class":640}," })",[630,1580,644],{"class":640},[630,1582,1583,1585,1587,1589,1591],{"class":632,"line":667},[630,1584,1400],{"class":1320},[630,1586,949],{"class":867},[630,1588,952],{"class":675},[630,1590,955],{"class":640},[630,1592,958],{"class":840},[630,1594,1595,1598,1600,1603,1605,1607,1609],{"class":632,"line":714},[630,1596,1597],{"class":847},"    collectionPermissions",[630,1599,899],{"class":640},[630,1601,1602],{"class":847},"entries",[630,1604,899],{"class":640},[630,1606,1033],{"class":847},[630,1608,899],{"class":640},[630,1610,1611],{"class":847},"value\n",[630,1613,1614,1617,1619,1621,1623,1626,1628,1631,1633,1635,1638,1641,1644,1646,1648,1650,1652,1655,1657,1659,1661,1663,1665],{"class":632,"line":743},[630,1615,1616],{"class":640},"    &&",[630,1618,1324],{"class":675},[630,1620,1563],{"class":847},[630,1622,899],{"class":640},[630,1624,1625],{"class":847},"ownerId",[630,1627,1338],{"class":640},[630,1629,1630],{"class":847}," session",[630,1632,899],{"class":640},[630,1634,1208],{"class":847},[630,1636,1637],{"class":640},"?.",[630,1639,1640],{"class":847},"userId",[630,1642,1643],{"class":640}," ||",[630,1645,1630],{"class":847},[630,1647,899],{"class":640},[630,1649,1208],{"class":847},[630,1651,1637],{"class":640},[630,1653,1654],{"class":847},"role",[630,1656,1338],{"class":640},[630,1658,691],{"class":640},[630,1660,683],{"class":682},[630,1662,679],{"class":640},[630,1664,1253],{"class":675},[630,1666,711],{"class":640},[630,1668,1669],{"class":632,"line":771},[630,1670,1671],{"class":675},"  )\n",[630,1673,1674],{"class":632,"line":787},[630,1675,802],{"class":640},[561,1677,1678,1679,1682,1683,1686],{},"Call ",[574,1680,1681],{},"canEditItem(item)"," per row in a ",[574,1684,1685],{},"v-for"," loop to get a per-item computed flag.",[586,1688,1689],{},[561,1690,1691,1693],{},[574,1692,1625],{}," lives in the Y.js document. A user can overwrite it via devtools before triggering an edit, making the check trivially bypassable. If ownership matters for data integrity, validate it server-side on every write.",[1515,1695,1697],{"id":1696},"quota-based-creation","Quota-based creation",[561,1699,1700],{},"Prevent new items from being added once a limit is reached, regardless of role.",[620,1702,1704],{"className":622,"code":1703,"language":625,"meta":626,"style":626},"const MAX_ENTRIES = 10\nconst entries = collections.entries\n\nconst canAddEntry = computed(() =>\n  collectionPermissions.entries.canCreate.value\n  && entries.count.value \u003C MAX_ENTRIES,\n)\n\nconst addEntryReason = computed(() => {\n  if (!collectionPermissions.entries.canCreate.value) return 'Insufficient role'\n  if (entries.count.value >= MAX_ENTRIES) return `Limit of ${MAX_ENTRIES} reached`\n  return null\n})\n",[574,1705,1706,1719,1735,1739,1756,1773,1797,1801,1805,1824,1857,1902,1908],{"__ignoreMap":626},[630,1707,1708,1710,1713,1715],{"class":632,"line":633},[630,1709,841],{"class":840},[630,1711,1712],{"class":847}," MAX_ENTRIES ",[630,1714,893],{"class":640},[630,1716,1718],{"class":1717},"sbssI"," 10\n",[630,1720,1721,1723,1726,1728,1730,1732],{"class":632,"line":647},[630,1722,841],{"class":840},[630,1724,1725],{"class":847}," entries ",[630,1727,893],{"class":640},[630,1729,853],{"class":847},[630,1731,899],{"class":640},[630,1733,1734],{"class":847},"entries\n",[630,1736,1737],{"class":632,"line":657},[630,1738,877],{"emptyLinePlaceholder":876},[630,1740,1741,1743,1746,1748,1750,1752,1754],{"class":632,"line":667},[630,1742,841],{"class":840},[630,1744,1745],{"class":847}," canAddEntry ",[630,1747,893],{"class":640},[630,1749,949],{"class":867},[630,1751,952],{"class":847},[630,1753,955],{"class":640},[630,1755,958],{"class":840},[630,1757,1758,1761,1763,1765,1767,1769,1771],{"class":632,"line":714},[630,1759,1760],{"class":847},"  collectionPermissions",[630,1762,899],{"class":640},[630,1764,1602],{"class":847},[630,1766,899],{"class":640},[630,1768,968],{"class":847},[630,1770,899],{"class":640},[630,1772,1611],{"class":847},[630,1774,1775,1778,1781,1783,1786,1788,1790,1792,1795],{"class":632,"line":743},[630,1776,1777],{"class":640},"  &&",[630,1779,1780],{"class":847}," entries",[630,1782,899],{"class":640},[630,1784,1785],{"class":847},"count",[630,1787,899],{"class":640},[630,1789,973],{"class":847},[630,1791,1156],{"class":640},[630,1793,1794],{"class":847}," MAX_ENTRIES",[630,1796,711],{"class":640},[630,1798,1799],{"class":632,"line":771},[630,1800,1410],{"class":847},[630,1802,1803],{"class":632,"line":787},[630,1804,877],{"emptyLinePlaceholder":876},[630,1806,1807,1809,1812,1814,1816,1818,1820,1822],{"class":632,"line":793},[630,1808,841],{"class":840},[630,1810,1811],{"class":847}," addEntryReason ",[630,1813,893],{"class":640},[630,1815,949],{"class":867},[630,1817,952],{"class":847},[630,1819,955],{"class":640},[630,1821,1313],{"class":840},[630,1823,644],{"class":640},[630,1825,1826,1828,1830,1832,1834,1836,1838,1840,1842,1844,1846,1848,1850,1852,1855],{"class":632,"line":799},[630,1827,1321],{"class":1320},[630,1829,1324],{"class":675},[630,1831,1197],{"class":640},[630,1833,617],{"class":847},[630,1835,899],{"class":640},[630,1837,1602],{"class":847},[630,1839,899],{"class":640},[630,1841,968],{"class":847},[630,1843,899],{"class":640},[630,1845,1208],{"class":847},[630,1847,1347],{"class":675},[630,1849,1350],{"class":1320},[630,1851,691],{"class":640},[630,1853,1854],{"class":682},"Insufficient role",[630,1856,1358],{"class":640},[630,1858,1859,1861,1863,1865,1867,1869,1871,1873,1876,1878,1880,1882,1885,1888,1891,1894,1896,1899],{"class":632,"line":1002},[630,1860,1321],{"class":1320},[630,1862,1324],{"class":675},[630,1864,1602],{"class":847},[630,1866,899],{"class":640},[630,1868,1785],{"class":847},[630,1870,899],{"class":640},[630,1872,1208],{"class":847},[630,1874,1875],{"class":640}," >=",[630,1877,1794],{"class":847},[630,1879,1347],{"class":675},[630,1881,1350],{"class":1320},[630,1883,1884],{"class":640}," `",[630,1886,1887],{"class":682},"Limit of ",[630,1889,1890],{"class":640},"${",[630,1892,1893],{"class":847},"MAX_ENTRIES",[630,1895,861],{"class":640},[630,1897,1898],{"class":682}," reached",[630,1900,1901],{"class":640},"`\n",[630,1903,1904,1906],{"class":632,"line":1010},[630,1905,1400],{"class":1320},[630,1907,1403],{"class":640},[630,1909,1910,1912],{"class":632,"line":1026},[630,1911,861],{"class":640},[630,1913,1410],{"class":847},[586,1915,1916],{},[561,1917,1918,1921],{},[574,1919,1920],{},"entries.count"," is derived from the Y.Array in the browser. A user can insert items directly into the array, bypassing the quota check entirely. Enforce quotas server-side if they carry real business weight.",[1515,1923,1925],{"id":1924},"multi-field-condition","Multi-field condition",[561,1927,1928,1929,1932],{},"Lock a field once a dependent field reaches a terminal state. Here, the ",[574,1930,1931],{},"invoiceNumber"," field becomes read-only after the invoice is marked as paid.",[620,1934,1936],{"className":622,"code":1935,"language":625,"meta":626,"style":626},"const canEditInvoiceNumber = computed(() =>\n  fieldPermissions.invoiceNumber.canWrite.value\n  && state.paymentStatus.value !== 'paid',\n)\n",[574,1937,1938,1955,1973,1999],{"__ignoreMap":626},[630,1939,1940,1942,1945,1947,1949,1951,1953],{"class":632,"line":633},[630,1941,841],{"class":840},[630,1943,1944],{"class":847}," canEditInvoiceNumber ",[630,1946,893],{"class":640},[630,1948,949],{"class":867},[630,1950,952],{"class":847},[630,1952,955],{"class":640},[630,1954,958],{"class":840},[630,1956,1957,1960,1962,1964,1966,1969,1971],{"class":632,"line":647},[630,1958,1959],{"class":847},"  fieldPermissions",[630,1961,899],{"class":640},[630,1963,1931],{"class":847},[630,1965,899],{"class":640},[630,1967,1968],{"class":847},"canWrite",[630,1970,899],{"class":640},[630,1972,1611],{"class":847},[630,1974,1975,1977,1979,1981,1984,1986,1988,1990,1992,1995,1997],{"class":632,"line":657},[630,1976,1777],{"class":640},[630,1978,848],{"class":847},[630,1980,899],{"class":640},[630,1982,1983],{"class":847},"paymentStatus",[630,1985,899],{"class":640},[630,1987,973],{"class":847},[630,1989,990],{"class":640},[630,1991,691],{"class":640},[630,1993,1994],{"class":682},"paid",[630,1996,679],{"class":640},[630,1998,711],{"class":640},[630,2000,2001],{"class":632,"line":667},[630,2002,1410],{"class":847},[620,2004,2006],{"className":1146,"code":2005,"filename":1148,"language":1149,"meta":626,"style":626},"\u003CUInput\n  v-model=\"state.invoiceNumber.value\"\n  :disabled=\"!canEditInvoiceNumber.value\"\n\u002F>\n",[574,2007,2008,2015,2036,2057],{"__ignoreMap":626},[630,2009,2010,2012],{"class":632,"line":633},[630,2011,1156],{"class":640},[630,2013,2014],{"class":675},"UInput\n",[630,2016,2017,2020,2022,2024,2026,2028,2030,2032,2034],{"class":632,"line":647},[630,2018,2019],{"class":840},"  v-model",[630,2021,893],{"class":640},[630,2023,1172],{"class":640},[630,2025,1327],{"class":847},[630,2027,899],{"class":640},[630,2029,1931],{"class":847},[630,2031,899],{"class":640},[630,2033,1208],{"class":847},[630,2035,1183],{"class":640},[630,2037,2038,2040,2042,2044,2046,2048,2051,2053,2055],{"class":632,"line":657},[630,2039,1164],{"class":640},[630,2041,1190],{"class":840},[630,2043,893],{"class":640},[630,2045,1172],{"class":640},[630,2047,1197],{"class":640},[630,2049,2050],{"class":847},"canEditInvoiceNumber",[630,2052,899],{"class":640},[630,2054,1208],{"class":847},[630,2056,1183],{"class":640},[630,2058,2059],{"class":632,"line":667},[630,2060,1260],{"class":847},[602,2062,2064],{"id":2063},"using-function-guards-in-the-schema","Using function guards in the schema",[561,2066,2067,2068,2070],{},"For simpler cases where the business condition does not depend on document state but on a value available at schema-definition time, a function guard works well. The guard runs inside a Vue ",[574,2069,576],{},", so any reactive value it closes over will trigger re-evaluation.",[620,2072,2074],{"className":622,"code":2073,"filename":833,"language":625,"meta":626,"style":626},"\u002F\u002F A ref that exists before the schema is defined\nconst featureEnabled = useFeatureFlag('advanced-editing')\n\nconst schema = definePrototype({\n  collections: {\n    tasks: {\n      permissions: {\n        \u002F\u002F Combines role check and a feature-flag check in one guard\n        update: (ctx) =>\n          ['admin', 'editor'].some(r => ctx.roles.includes(r))\n          && featureEnabled.value,\n      },\n    },\n  },\n})\n",[574,2075,2076,2081,2104,2108,2125,2134,2143,2152,2157,2173,2222,2236,2241,2245,2249],{"__ignoreMap":626},[630,2077,2078],{"class":632,"line":633},[630,2079,2080],{"class":882},"\u002F\u002F A ref that exists before the schema is defined\n",[630,2082,2083,2085,2088,2090,2093,2095,2097,2100,2102],{"class":632,"line":647},[630,2084,841],{"class":840},[630,2086,2087],{"class":847}," featureEnabled ",[630,2089,893],{"class":640},[630,2091,2092],{"class":867}," useFeatureFlag",[630,2094,952],{"class":847},[630,2096,679],{"class":640},[630,2098,2099],{"class":682},"advanced-editing",[630,2101,679],{"class":640},[630,2103,1410],{"class":847},[630,2105,2106],{"class":632,"line":657},[630,2107,877],{"emptyLinePlaceholder":876},[630,2109,2110,2112,2115,2117,2120,2122],{"class":632,"line":667},[630,2111,841],{"class":840},[630,2113,2114],{"class":847}," schema ",[630,2116,893],{"class":640},[630,2118,2119],{"class":867}," definePrototype",[630,2121,952],{"class":847},[630,2123,2124],{"class":640},"{\n",[630,2126,2127,2130,2132],{"class":632,"line":714},[630,2128,2129],{"class":675},"  collections",[630,2131,641],{"class":640},[630,2133,644],{"class":640},[630,2135,2136,2139,2141],{"class":632,"line":743},[630,2137,2138],{"class":675},"    tasks",[630,2140,641],{"class":640},[630,2142,644],{"class":640},[630,2144,2145,2148,2150],{"class":632,"line":771},[630,2146,2147],{"class":675},"      permissions",[630,2149,641],{"class":640},[630,2151,644],{"class":640},[630,2153,2154],{"class":632,"line":787},[630,2155,2156],{"class":882},"        \u002F\u002F Combines role check and a feature-flag check in one guard\n",[630,2158,2159,2162,2164,2166,2169,2171],{"class":632,"line":793},[630,2160,2161],{"class":867},"        update",[630,2163,641],{"class":640},[630,2165,1324],{"class":640},[630,2167,2168],{"class":1562},"ctx",[630,2170,1253],{"class":640},[630,2172,958],{"class":840},[630,2174,2175,2178,2180,2182,2184,2186,2188,2190,2192,2194,2196,2199,2201,2204,2206,2209,2211,2214,2216,2219],{"class":632,"line":799},[630,2176,2177],{"class":847},"          [",[630,2179,679],{"class":640},[630,2181,683],{"class":682},[630,2183,679],{"class":640},[630,2185,688],{"class":640},[630,2187,691],{"class":640},[630,2189,694],{"class":682},[630,2191,679],{"class":640},[630,2193,708],{"class":847},[630,2195,899],{"class":640},[630,2197,2198],{"class":867},"some",[630,2200,952],{"class":847},[630,2202,2203],{"class":1562},"r",[630,2205,1313],{"class":840},[630,2207,2208],{"class":847}," ctx",[630,2210,899],{"class":640},[630,2212,2213],{"class":847},"roles",[630,2215,899],{"class":640},[630,2217,2218],{"class":867},"includes",[630,2220,2221],{"class":847},"(r))\n",[630,2223,2224,2227,2230,2232,2234],{"class":632,"line":1002},[630,2225,2226],{"class":640},"          &&",[630,2228,2229],{"class":847}," featureEnabled",[630,2231,899],{"class":640},[630,2233,1208],{"class":847},[630,2235,711],{"class":640},[630,2237,2238],{"class":632,"line":1010},[630,2239,2240],{"class":640},"      },\n",[630,2242,2243],{"class":632,"line":1026},[630,2244,790],{"class":640},[630,2246,2247],{"class":632,"line":1064},[630,2248,796],{"class":640},[630,2250,2251,2253],{"class":632,"line":1071},[630,2252,861],{"class":640},[630,2254,1410],{"class":847},[586,2256,2257],{},[561,2258,2259,2260,2263,2264,2267,2268,2270],{},"This pattern requires the reactive value (",[574,2261,2262],{},"featureEnabled",") to be defined ",[591,2265,2266],{},"before"," the schema object. It does not work for values that come from ",[574,2269,415],{}," itself, since the schema must exist before the composable is called.",[561,2272,2273,2274,2277],{},"When you need to combine roles with document state (values from ",[574,2275,2276],{},"state.*","), always use the computed composition pattern described above. Function guards cannot reference values that do not yet exist at the time the schema object is constructed.",[602,2279,599],{"id":2280},"limitations",[561,2282,2283],{},"All of the patterns described in this document are UI guards. None of them prevent a determined user from writing directly to the Y.js document.",[1515,2285,2287],{"id":2286},"the-condition-itself-can-be-modified","The condition itself can be modified",[561,2289,2290,2291,2294],{},"This is the critical difference between business logic guards and role guards. A role guard checks a value that comes from outside the document (a session token, a JWT claim). A business logic guard typically checks a value that lives ",[565,2292,2293],{},"inside"," the document — the project status, an item's owner field, the array length.",[561,2296,2297,2298,2300],{},"A user who wants to bypass a status check does not need to understand Vue or the permission system at all. They only need to modify the document value that the guard reads — ",[574,2299,808],{}," in this case — before performing the operation. The computed flag recalculates immediately, and the UI allows the action.",[561,2302,2303,2304,2307],{},"In production, Vue DevTools are disabled and bundled module internals are not directly accessible from the console. The document is however persisted in IndexedDB under the key ",[574,2305,2306],{},"proto:\u003CdocKey>",", which any user can inspect via the browser's Application tab. The stored data is Y.js binary — not human-readable and not editable through the DevTools UI — but a developer who loads the Y.js library from a CDN, reads the IndexedDB updates, constructs a valid CRDT update, and writes it back can modify any field. That is meaningful technical effort, but it is feasible for someone with the motivation.",[1515,2309,2311],{"id":2310},"what-this-means-in-practice","What this means in practice",[561,2313,2314],{},"Business logic guards are appropriate for:",[2316,2317,2318,2322,2325],"ul",{},[2319,2320,2321],"li",{},"Preventing accidental edits — a \"completed\" badge that greys out the task list stops most users from making unintended changes.",[2319,2323,2324],{},"Improving UX clarity — hiding controls that do not apply in the current state reduces noise and confusion.",[2319,2326,2327],{},"Encoding workflow expectations — making the intended flow obvious through the interface.",[561,2329,2330,2331,2334],{},"They are ",[591,2332,2333],{},"not"," appropriate for:",[2316,2336,2337,2340,2343],{},[2319,2338,2339],{},"Preventing deliberate modifications by a motivated user.",[2319,2341,2342],{},"Enforcing data integrity rules that carry legal, financial, or security consequences.",[2319,2344,2345],{},"Any check where the consequence of bypass is data corruption or unauthorised access.",[1515,2347,2349],{"id":2348},"enforcing-business-rules-server-side","Enforcing business rules server-side",[561,2351,2352,2353,2356,2357,2359,2360,2363],{},"nuxt-protokit ships client-side only. The server sync endpoint (",[574,2354,2355],{},"\u002Fapi\u002Fyjs\u002Fsync"," by default) is implemented by you. After every local change, ",[574,2358,419],{}," debounces and POSTs the ",[591,2361,2362],{},"full document state"," to that endpoint:",[620,2365,2370],{"className":2366,"code":2368,"language":2369},[2367],"language-text","POST \u002Fapi\u002Fyjs\u002Fsync\n{\n  docKey: \"proto:my-project\",\n  docType: \"custom\",\n  updates: [{ update: \"\u003Cbase64-encoded Y.js state>\", timestamp: 1234567890 }],\n  clientId: \"proto-abc123\"\n}\n","text",[574,2371,2368],{"__ignoreMap":626},[561,2373,2374,2375,2377,2378,2381],{},"The ",[574,2376,1218],{}," field is ",[574,2379,2380],{},"Y.encodeStateAsUpdate(doc)"," — the entire document encoded as base64, not a per-field diff.",[2383,2384,2386],"h4",{"id":2385},"why-rejecting-the-push-does-not-work","Why rejecting the push does not work",[561,2388,2389,2390,2393,2394,2397,2398,2401],{},"The natural instinct is to return a ",[574,2391,2392],{},"403"," when a rule is violated. This does not work with the current sync design. ",[574,2395,2396],{},"pushDocToServer"," wraps every request in a ",[574,2399,2400],{},"catch {}"," that swallows all errors silently (designed for offline resilience — the push should never crash the app). A rejected push is invisible to the client. The local IndexedDB and in-memory Y.Doc remain in the invalid state, and the next debounced push 30 seconds later retries with the same content.",[2383,2403,2405],{"id":2404},"pull-based-reconciliation","Pull-based reconciliation",[561,2407,2408,2409,2412,2413,2416],{},"The correct approach for this architecture is to ",[591,2410,2411],{},"accept the push, then correct it",". The server applies a compensating Y.js update that enforces the rule, stores the corrected state, and returns it on the next pull. The client reconciles automatically when it calls ",[574,2414,2415],{},"pullFromServer"," — which happens on every fresh page load.",[620,2418,2421],{"className":622,"code":2419,"filename":2420,"language":625,"meta":626,"style":626},"import * as Y from 'yjs'\n\nexport default defineEventHandler(async (event) => {\n  const { docKey, updates } = await readBody(event)\n\n  \u002F\u002F Load previously stored state (your database implementation)\n  const stored = await db.yjsDocuments.findUnique({ where: { key: docKey } })\n\n  \u002F\u002F Build a doc from stored state, then apply the incoming update\n  const doc = new Y.Doc()\n  if (stored?.data) Y.applyUpdate(doc, Buffer.from(stored.data))\n\n  const incoming = Buffer.from(updates[0].update, 'base64')\n  Y.applyUpdate(doc, incoming)\n\n  \u002F\u002F Check and enforce business rules against the merged state\n  const bare = docKey.replace(\u002F^proto:\u002F, '')\n  const map = doc.getMap(bare)\n\n  if (stored?.data) {\n    \u002F\u002F Revert any changes if the previously-stored project was already completed.\n    \u002F\u002F Re-apply only the last known-good state so the stored record stays correct.\n    const previousDoc = new Y.Doc()\n    Y.applyUpdate(previousDoc, Buffer.from(stored.data))\n    const previousStatus = previousDoc.getMap(bare).get('status')\n    previousDoc.destroy()\n\n    if (previousStatus === 'completed') {\n      \u002F\u002F Discard the incoming changes — persist the stored state unchanged\n      doc.destroy()\n      return { success: true, corrected: true }\n    }\n  }\n\n  \u002F\u002F Persist the accepted state\n  const finalState = Y.encodeStateAsUpdate(doc)\n  await db.yjsDocuments.upsert({\n    where: { key: docKey },\n    update: { data: Buffer.from(finalState) },\n    create: { key: docKey, data: Buffer.from(finalState) },\n  })\n  doc.destroy()\n\n  return { success: true }\n})\n","server\u002Fapi\u002Fyjs\u002Fsync.post.ts",[574,2422,2423,2447,2451,2478,2510,2514,2519,2568,2572,2577,2599,2648,2652,2695,2714,2718,2723,2759,2782,2787,2804,2810,2816,2837,2870,2907,2920,2925,2948,2954,2966,2995,3001,3007,3012,3018,3041,3062,3081,3112,3148,3156,3168,3173,3188],{"__ignoreMap":626},[630,2424,2425,2428,2431,2434,2437,2440,2442,2445],{"class":632,"line":633},[630,2426,2427],{"class":1320},"import",[630,2429,2430],{"class":640}," *",[630,2432,2433],{"class":1320}," as",[630,2435,2436],{"class":847}," Y ",[630,2438,2439],{"class":1320},"from",[630,2441,691],{"class":640},[630,2443,2444],{"class":682},"yjs",[630,2446,1358],{"class":640},[630,2448,2449],{"class":632,"line":647},[630,2450,877],{"emptyLinePlaceholder":876},[630,2452,2453,2456,2459,2462,2464,2467,2469,2472,2474,2476],{"class":632,"line":657},[630,2454,2455],{"class":1320},"export",[630,2457,2458],{"class":1320}," default",[630,2460,2461],{"class":867}," defineEventHandler",[630,2463,952],{"class":847},[630,2465,2466],{"class":840},"async",[630,2468,1324],{"class":640},[630,2470,2471],{"class":1562},"event",[630,2473,1253],{"class":640},[630,2475,1313],{"class":840},[630,2477,644],{"class":640},[630,2479,2480,2483,2485,2488,2490,2493,2496,2498,2501,2504,2506,2508],{"class":632,"line":667},[630,2481,2482],{"class":840},"  const",[630,2484,844],{"class":640},[630,2486,2487],{"class":847}," docKey",[630,2489,688],{"class":640},[630,2491,2492],{"class":847}," updates",[630,2494,2495],{"class":640}," }",[630,2497,864],{"class":640},[630,2499,2500],{"class":1320}," await",[630,2502,2503],{"class":867}," readBody",[630,2505,952],{"class":675},[630,2507,2471],{"class":847},[630,2509,1410],{"class":675},[630,2511,2512],{"class":632,"line":714},[630,2513,877],{"emptyLinePlaceholder":876},[630,2515,2516],{"class":632,"line":743},[630,2517,2518],{"class":882},"  \u002F\u002F Load previously stored state (your database implementation)\n",[630,2520,2521,2523,2526,2528,2530,2533,2535,2538,2540,2543,2545,2548,2551,2553,2555,2558,2560,2562,2564,2566],{"class":632,"line":771},[630,2522,2482],{"class":840},[630,2524,2525],{"class":847}," stored",[630,2527,864],{"class":640},[630,2529,2500],{"class":1320},[630,2531,2532],{"class":847}," db",[630,2534,899],{"class":640},[630,2536,2537],{"class":847},"yjsDocuments",[630,2539,899],{"class":640},[630,2541,2542],{"class":867},"findUnique",[630,2544,952],{"class":675},[630,2546,2547],{"class":640},"{",[630,2549,2550],{"class":675}," where",[630,2552,641],{"class":640},[630,2554,844],{"class":640},[630,2556,2557],{"class":675}," key",[630,2559,641],{"class":640},[630,2561,2487],{"class":847},[630,2563,2495],{"class":640},[630,2565,2495],{"class":640},[630,2567,1410],{"class":675},[630,2569,2570],{"class":632,"line":787},[630,2571,877],{"emptyLinePlaceholder":876},[630,2573,2574],{"class":632,"line":793},[630,2575,2576],{"class":882},"  \u002F\u002F Build a doc from stored state, then apply the incoming update\n",[630,2578,2579,2581,2584,2586,2589,2592,2594,2597],{"class":632,"line":799},[630,2580,2482],{"class":840},[630,2582,2583],{"class":847}," doc",[630,2585,864],{"class":640},[630,2587,2588],{"class":640}," new",[630,2590,2591],{"class":847}," Y",[630,2593,899],{"class":640},[630,2595,2596],{"class":867},"Doc",[630,2598,1545],{"class":675},[630,2600,2601,2603,2605,2608,2610,2613,2615,2618,2620,2623,2625,2628,2630,2633,2635,2637,2639,2641,2643,2645],{"class":632,"line":1002},[630,2602,1321],{"class":1320},[630,2604,1324],{"class":675},[630,2606,2607],{"class":847},"stored",[630,2609,1637],{"class":640},[630,2611,2612],{"class":847},"data",[630,2614,1347],{"class":675},[630,2616,2617],{"class":847},"Y",[630,2619,899],{"class":640},[630,2621,2622],{"class":867},"applyUpdate",[630,2624,952],{"class":675},[630,2626,2627],{"class":847},"doc",[630,2629,688],{"class":640},[630,2631,2632],{"class":847}," Buffer",[630,2634,899],{"class":640},[630,2636,2439],{"class":867},[630,2638,952],{"class":675},[630,2640,2607],{"class":847},[630,2642,899],{"class":640},[630,2644,2612],{"class":847},[630,2646,2647],{"class":675},"))\n",[630,2649,2650],{"class":632,"line":1010},[630,2651,877],{"emptyLinePlaceholder":876},[630,2653,2654,2656,2659,2661,2663,2665,2667,2669,2672,2675,2678,2680,2682,2684,2686,2688,2691,2693],{"class":632,"line":1026},[630,2655,2482],{"class":840},[630,2657,2658],{"class":847}," incoming",[630,2660,864],{"class":640},[630,2662,2632],{"class":847},[630,2664,899],{"class":640},[630,2666,2439],{"class":867},[630,2668,952],{"class":675},[630,2670,2671],{"class":847},"updates",[630,2673,2674],{"class":675},"[",[630,2676,2677],{"class":1717},"0",[630,2679,708],{"class":675},[630,2681,899],{"class":640},[630,2683,1218],{"class":847},[630,2685,688],{"class":640},[630,2687,691],{"class":640},[630,2689,2690],{"class":682},"base64",[630,2692,679],{"class":640},[630,2694,1410],{"class":675},[630,2696,2697,2700,2702,2704,2706,2708,2710,2712],{"class":632,"line":1064},[630,2698,2699],{"class":847},"  Y",[630,2701,899],{"class":640},[630,2703,2622],{"class":867},[630,2705,952],{"class":675},[630,2707,2627],{"class":847},[630,2709,688],{"class":640},[630,2711,2658],{"class":847},[630,2713,1410],{"class":675},[630,2715,2716],{"class":632,"line":1071},[630,2717,877],{"emptyLinePlaceholder":876},[630,2719,2720],{"class":632,"line":1087},[630,2721,2722],{"class":882},"  \u002F\u002F Check and enforce business rules against the merged state\n",[630,2724,2725,2727,2730,2732,2734,2736,2739,2741,2744,2747,2750,2752,2754,2757],{"class":632,"line":1123},[630,2726,2482],{"class":840},[630,2728,2729],{"class":847}," bare",[630,2731,864],{"class":640},[630,2733,2487],{"class":847},[630,2735,899],{"class":640},[630,2737,2738],{"class":867},"replace",[630,2740,952],{"class":675},[630,2742,2743],{"class":640},"\u002F",[630,2745,2746],{"class":1320},"^",[630,2748,2749],{"class":682},"proto:",[630,2751,2743],{"class":640},[630,2753,688],{"class":640},[630,2755,2756],{"class":640}," ''",[630,2758,1410],{"class":675},[630,2760,2761,2763,2766,2768,2770,2772,2775,2777,2780],{"class":632,"line":1130},[630,2762,2482],{"class":840},[630,2764,2765],{"class":847}," map",[630,2767,864],{"class":640},[630,2769,2583],{"class":847},[630,2771,899],{"class":640},[630,2773,2774],{"class":867},"getMap",[630,2776,952],{"class":675},[630,2778,2779],{"class":847},"bare",[630,2781,1410],{"class":675},[630,2783,2785],{"class":632,"line":2784},19,[630,2786,877],{"emptyLinePlaceholder":876},[630,2788,2790,2792,2794,2796,2798,2800,2802],{"class":632,"line":2789},20,[630,2791,1321],{"class":1320},[630,2793,1324],{"class":675},[630,2795,2607],{"class":847},[630,2797,1637],{"class":640},[630,2799,2612],{"class":847},[630,2801,1347],{"class":675},[630,2803,2124],{"class":640},[630,2805,2807],{"class":632,"line":2806},21,[630,2808,2809],{"class":882},"    \u002F\u002F Revert any changes if the previously-stored project was already completed.\n",[630,2811,2813],{"class":632,"line":2812},22,[630,2814,2815],{"class":882},"    \u002F\u002F Re-apply only the last known-good state so the stored record stays correct.\n",[630,2817,2819,2822,2825,2827,2829,2831,2833,2835],{"class":632,"line":2818},23,[630,2820,2821],{"class":840},"    const",[630,2823,2824],{"class":847}," previousDoc",[630,2826,864],{"class":640},[630,2828,2588],{"class":640},[630,2830,2591],{"class":847},[630,2832,899],{"class":640},[630,2834,2596],{"class":867},[630,2836,1545],{"class":675},[630,2838,2840,2843,2845,2847,2849,2852,2854,2856,2858,2860,2862,2864,2866,2868],{"class":632,"line":2839},24,[630,2841,2842],{"class":847},"    Y",[630,2844,899],{"class":640},[630,2846,2622],{"class":867},[630,2848,952],{"class":675},[630,2850,2851],{"class":847},"previousDoc",[630,2853,688],{"class":640},[630,2855,2632],{"class":847},[630,2857,899],{"class":640},[630,2859,2439],{"class":867},[630,2861,952],{"class":675},[630,2863,2607],{"class":847},[630,2865,899],{"class":640},[630,2867,2612],{"class":847},[630,2869,2647],{"class":675},[630,2871,2873,2875,2878,2880,2882,2884,2886,2888,2890,2892,2894,2897,2899,2901,2903,2905],{"class":632,"line":2872},25,[630,2874,2821],{"class":840},[630,2876,2877],{"class":847}," previousStatus",[630,2879,864],{"class":640},[630,2881,2824],{"class":847},[630,2883,899],{"class":640},[630,2885,2774],{"class":867},[630,2887,952],{"class":675},[630,2889,2779],{"class":847},[630,2891,1253],{"class":675},[630,2893,899],{"class":640},[630,2895,2896],{"class":867},"get",[630,2898,952],{"class":675},[630,2900,679],{"class":640},[630,2902,983],{"class":682},[630,2904,679],{"class":640},[630,2906,1410],{"class":675},[630,2908,2910,2913,2915,2918],{"class":632,"line":2909},26,[630,2911,2912],{"class":847},"    previousDoc",[630,2914,899],{"class":640},[630,2916,2917],{"class":867},"destroy",[630,2919,1545],{"class":675},[630,2921,2923],{"class":632,"line":2922},27,[630,2924,877],{"emptyLinePlaceholder":876},[630,2926,2928,2931,2933,2936,2938,2940,2942,2944,2946],{"class":632,"line":2927},28,[630,2929,2930],{"class":1320},"    if",[630,2932,1324],{"class":675},[630,2934,2935],{"class":847},"previousStatus",[630,2937,1338],{"class":640},[630,2939,691],{"class":640},[630,2941,995],{"class":682},[630,2943,679],{"class":640},[630,2945,1347],{"class":675},[630,2947,2124],{"class":640},[630,2949,2951],{"class":632,"line":2950},29,[630,2952,2953],{"class":882},"      \u002F\u002F Discard the incoming changes — persist the stored state unchanged\n",[630,2955,2957,2960,2962,2964],{"class":632,"line":2956},30,[630,2958,2959],{"class":847},"      doc",[630,2961,899],{"class":640},[630,2963,2917],{"class":867},[630,2965,1545],{"class":675},[630,2967,2969,2972,2974,2977,2979,2983,2985,2988,2990,2992],{"class":632,"line":2968},31,[630,2970,2971],{"class":1320},"      return",[630,2973,844],{"class":640},[630,2975,2976],{"class":675}," success",[630,2978,641],{"class":640},[630,2980,2982],{"class":2981},"sfNiH"," true",[630,2984,688],{"class":640},[630,2986,2987],{"class":675}," corrected",[630,2989,641],{"class":640},[630,2991,2982],{"class":2981},[630,2993,2994],{"class":640}," }\n",[630,2996,2998],{"class":632,"line":2997},32,[630,2999,3000],{"class":640},"    }\n",[630,3002,3004],{"class":632,"line":3003},33,[630,3005,3006],{"class":640},"  }\n",[630,3008,3010],{"class":632,"line":3009},34,[630,3011,877],{"emptyLinePlaceholder":876},[630,3013,3015],{"class":632,"line":3014},35,[630,3016,3017],{"class":882},"  \u002F\u002F Persist the accepted state\n",[630,3019,3021,3023,3026,3028,3030,3032,3035,3037,3039],{"class":632,"line":3020},36,[630,3022,2482],{"class":840},[630,3024,3025],{"class":847}," finalState",[630,3027,864],{"class":640},[630,3029,2591],{"class":847},[630,3031,899],{"class":640},[630,3033,3034],{"class":867},"encodeStateAsUpdate",[630,3036,952],{"class":675},[630,3038,2627],{"class":847},[630,3040,1410],{"class":675},[630,3042,3044,3047,3049,3051,3053,3055,3058,3060],{"class":632,"line":3043},37,[630,3045,3046],{"class":1320},"  await",[630,3048,2532],{"class":847},[630,3050,899],{"class":640},[630,3052,2537],{"class":847},[630,3054,899],{"class":640},[630,3056,3057],{"class":867},"upsert",[630,3059,952],{"class":675},[630,3061,2124],{"class":640},[630,3063,3065,3068,3070,3072,3074,3076,3078],{"class":632,"line":3064},38,[630,3066,3067],{"class":675},"    where",[630,3069,641],{"class":640},[630,3071,844],{"class":640},[630,3073,2557],{"class":675},[630,3075,641],{"class":640},[630,3077,2487],{"class":847},[630,3079,3080],{"class":640}," },\n",[630,3082,3084,3087,3089,3091,3094,3096,3098,3100,3102,3104,3107,3109],{"class":632,"line":3083},39,[630,3085,3086],{"class":675},"    update",[630,3088,641],{"class":640},[630,3090,844],{"class":640},[630,3092,3093],{"class":675}," data",[630,3095,641],{"class":640},[630,3097,2632],{"class":847},[630,3099,899],{"class":640},[630,3101,2439],{"class":867},[630,3103,952],{"class":675},[630,3105,3106],{"class":847},"finalState",[630,3108,1347],{"class":675},[630,3110,3111],{"class":640},"},\n",[630,3113,3115,3118,3120,3122,3124,3126,3128,3130,3132,3134,3136,3138,3140,3142,3144,3146],{"class":632,"line":3114},40,[630,3116,3117],{"class":675},"    create",[630,3119,641],{"class":640},[630,3121,844],{"class":640},[630,3123,2557],{"class":675},[630,3125,641],{"class":640},[630,3127,2487],{"class":847},[630,3129,688],{"class":640},[630,3131,3093],{"class":675},[630,3133,641],{"class":640},[630,3135,2632],{"class":847},[630,3137,899],{"class":640},[630,3139,2439],{"class":867},[630,3141,952],{"class":675},[630,3143,3106],{"class":847},[630,3145,1347],{"class":675},[630,3147,3111],{"class":640},[630,3149,3151,3154],{"class":632,"line":3150},41,[630,3152,3153],{"class":640},"  }",[630,3155,1410],{"class":675},[630,3157,3159,3162,3164,3166],{"class":632,"line":3158},42,[630,3160,3161],{"class":847},"  doc",[630,3163,899],{"class":640},[630,3165,2917],{"class":867},[630,3167,1545],{"class":675},[630,3169,3171],{"class":632,"line":3170},43,[630,3172,877],{"emptyLinePlaceholder":876},[630,3174,3176,3178,3180,3182,3184,3186],{"class":632,"line":3175},44,[630,3177,1400],{"class":1320},[630,3179,844],{"class":640},[630,3181,2976],{"class":675},[630,3183,641],{"class":640},[630,3185,2982],{"class":2981},[630,3187,2994],{"class":640},[630,3189,3191,3193],{"class":632,"line":3190},45,[630,3192,861],{"class":640},[630,3194,1410],{"class":847},[561,3196,3197],{},"The client will reconcile on the next pull (page reload or tab focus). Until then there is a window where the local state is technically invalid — this is the inherent trade-off of local-first architecture.",[586,3199,3200,3206],{},[561,3201,3202,3205],{},[591,3203,3204],{},"This approach scales poorly with data complexity."," The example above works because the rule is all-or-nothing: if the project was completed, discard the entire incoming state. Real schemas are rarely this simple. If you need to allow some changes while blocking others — for example, letting a user update their own profile fields on a locked project, or allowing comments to be added but not tasks modified — the server must diff the previous and incoming Y.js states, identify exactly which Y.Map keys or Y.Array items changed, decide which changes are permitted, and reconstruct a corrected document that preserves the valid changes without accepting the invalid ones. Y.js does not expose a human-readable diff API; this logic must be written against the raw CRDT structure. The result is brittle, hard to test, and easy to get wrong in ways that cause silent data loss.",[561,3207,3208],{},"Only implement pull-based reconciliation where the business genuinely requires server-enforced rules — for example, financial records that must be immutable once approved, or regulatory compliance requirements. For most prototyping and internal tooling use cases, the combination of client-side guards and the separate lock endpoint described below is sufficient.",[2383,3210,3212],{"id":3211},"immediate-feedback-via-a-separate-lock-endpoint","Immediate feedback via a separate lock endpoint",[561,3214,3215],{},"If you need the UI to reflect the locked state promptly rather than waiting for a pull cycle, add a lightweight REST endpoint alongside the Y.js sync:",[620,3217,3220],{"className":622,"code":3218,"filename":3219,"language":625,"meta":626,"style":626},"export default defineEventHandler(async (event) => {\n  const project = await db.projects.findUnique({\n    where: { id: getRouterParam(event, 'id') },\n    select: { status: true },\n  })\n  return { editable: project?.status !== 'completed' }\n})\n","server\u002Fapi\u002Fprojects\u002F[id]\u002Feditable.get.ts",[574,3221,3222,3244,3270,3303,3321,3327,3355],{"__ignoreMap":626},[630,3223,3224,3226,3228,3230,3232,3234,3236,3238,3240,3242],{"class":632,"line":633},[630,3225,2455],{"class":1320},[630,3227,2458],{"class":1320},[630,3229,2461],{"class":867},[630,3231,952],{"class":847},[630,3233,2466],{"class":840},[630,3235,1324],{"class":640},[630,3237,2471],{"class":1562},[630,3239,1253],{"class":640},[630,3241,1313],{"class":840},[630,3243,644],{"class":640},[630,3245,3246,3248,3251,3253,3255,3257,3259,3262,3264,3266,3268],{"class":632,"line":647},[630,3247,2482],{"class":840},[630,3249,3250],{"class":847}," project",[630,3252,864],{"class":640},[630,3254,2500],{"class":1320},[630,3256,2532],{"class":847},[630,3258,899],{"class":640},[630,3260,3261],{"class":847},"projects",[630,3263,899],{"class":640},[630,3265,2542],{"class":867},[630,3267,952],{"class":675},[630,3269,2124],{"class":640},[630,3271,3272,3274,3276,3278,3281,3283,3286,3288,3290,3292,3294,3297,3299,3301],{"class":632,"line":657},[630,3273,3067],{"class":675},[630,3275,641],{"class":640},[630,3277,844],{"class":640},[630,3279,3280],{"class":675}," id",[630,3282,641],{"class":640},[630,3284,3285],{"class":867}," getRouterParam",[630,3287,952],{"class":675},[630,3289,2471],{"class":847},[630,3291,688],{"class":640},[630,3293,691],{"class":640},[630,3295,3296],{"class":682},"id",[630,3298,679],{"class":640},[630,3300,1347],{"class":675},[630,3302,3111],{"class":640},[630,3304,3305,3308,3310,3312,3315,3317,3319],{"class":632,"line":667},[630,3306,3307],{"class":675},"    select",[630,3309,641],{"class":640},[630,3311,844],{"class":640},[630,3313,3314],{"class":675}," status",[630,3316,641],{"class":640},[630,3318,2982],{"class":2981},[630,3320,3080],{"class":640},[630,3322,3323,3325],{"class":632,"line":714},[630,3324,3153],{"class":640},[630,3326,1410],{"class":675},[630,3328,3329,3331,3333,3336,3338,3340,3342,3344,3347,3349,3351,3353],{"class":632,"line":743},[630,3330,1400],{"class":1320},[630,3332,844],{"class":640},[630,3334,3335],{"class":675}," editable",[630,3337,641],{"class":640},[630,3339,3250],{"class":847},[630,3341,1637],{"class":640},[630,3343,983],{"class":847},[630,3345,3346],{"class":640}," !==",[630,3348,691],{"class":640},[630,3350,995],{"class":682},[630,3352,679],{"class":640},[630,3354,2994],{"class":640},[630,3356,3357,3359],{"class":632,"line":771},[630,3358,861],{"class":640},[630,3360,1410],{"class":847},[561,3362,3363],{},"Poll this from the client on mount and whenever a relevant event occurs (e.g. another user completes the project). Use the result to lock the entire UI independently of the Y.js permission layer:",[620,3365,3367],{"className":622,"code":3366,"filename":833,"language":625,"meta":626,"style":626},"const { data: lock } = await useFetch(`\u002Fapi\u002Fprojects\u002F${id}\u002Feditable`)\nconst isEditable = computed(() => lock.value?.editable ?? true)\n",[574,3368,3369,3412],{"__ignoreMap":626},[630,3370,3371,3373,3375,3377,3379,3382,3384,3386,3388,3391,3393,3396,3399,3401,3403,3405,3408,3410],{"class":632,"line":633},[630,3372,841],{"class":840},[630,3374,844],{"class":640},[630,3376,3093],{"class":675},[630,3378,641],{"class":640},[630,3380,3381],{"class":847}," lock ",[630,3383,861],{"class":640},[630,3385,864],{"class":640},[630,3387,2500],{"class":1320},[630,3389,3390],{"class":867}," useFetch",[630,3392,952],{"class":847},[630,3394,3395],{"class":640},"`",[630,3397,3398],{"class":682},"\u002Fapi\u002Fprojects\u002F",[630,3400,1890],{"class":640},[630,3402,3296],{"class":847},[630,3404,861],{"class":640},[630,3406,3407],{"class":682},"\u002Feditable",[630,3409,3395],{"class":640},[630,3411,1410],{"class":847},[630,3413,3414,3416,3419,3421,3423,3425,3427,3429,3432,3434,3436,3438,3441,3444,3446],{"class":632,"line":647},[630,3415,841],{"class":840},[630,3417,3418],{"class":847}," isEditable ",[630,3420,893],{"class":640},[630,3422,949],{"class":867},[630,3424,952],{"class":847},[630,3426,955],{"class":640},[630,3428,1313],{"class":840},[630,3430,3431],{"class":847}," lock",[630,3433,899],{"class":640},[630,3435,1208],{"class":847},[630,3437,1637],{"class":640},[630,3439,3440],{"class":847},"editable ",[630,3442,3443],{"class":640},"??",[630,3445,2982],{"class":2981},[630,3447,1410],{"class":847},[561,3449,3450],{},"This gives you a server-authoritative lock signal that the UI respects immediately, while the Y.js layer continues to handle persistence and sync. The two concerns stay separate: Y.js owns the data, the REST endpoint owns the access decision.",[602,3452,3454],{"id":3453},"choosing-the-right-approach","Choosing the right approach",[3456,3457,3458,3471],"table",{},[3459,3460,3461],"thead",{},[3462,3463,3464,3468],"tr",{},[3465,3466,3467],"th",{},"Condition source",[3465,3469,3470],{},"Approach",[3472,3473,3474,3489,3497,3513],"tbody",{},[3462,3475,3476,3480],{},[3477,3478,3479],"td",{},"User role only",[3477,3481,3482,3483,3486,3487,1253],{},"Schema ",[574,3484,3485],{},"permissions"," guard (string or string",[630,3488],{},[3462,3490,3491,3494],{},[3477,3492,3493],{},"Role + external flag (feature toggle, config)",[3477,3495,3496],{},"Function guard as closure in schema",[3462,3498,3499,3508],{},[3477,3500,3501,3502,3504,3505,1253],{},"Role + document state (",[574,3503,2276],{},", ",[574,3506,3507],{},"collections.*",[3477,3509,3510,3511],{},"Computed composition after ",[574,3512,415],{},[3462,3514,3515,3521],{},[3477,3516,3517,3518,1253],{},"Per-item conditions (",[574,3519,3520],{},"item.ownerId === currentUser",[3477,3522,3523],{},"Per-item computed in template logic",[561,3525,3526,3527,3529],{},"The schema handles static identity rules. ",[574,3528,576],{}," handles everything dynamic.",[3531,3532,3533],"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 .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);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}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 .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 .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}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":626,"searchDepth":647,"depth":647,"links":3535},[3536,3537,3538,3539,3544,3545,3550],{"id":604,"depth":647,"text":605},{"id":815,"depth":647,"text":816},{"id":1263,"depth":647,"text":1264},{"id":1512,"depth":647,"text":1513,"children":3540},[3541,3542,3543],{"id":1517,"depth":657,"text":1518},{"id":1696,"depth":657,"text":1697},{"id":1924,"depth":657,"text":1925},{"id":2063,"depth":647,"text":2064},{"id":2280,"depth":647,"text":599,"children":3546},[3547,3548,3549],{"id":2286,"depth":657,"text":2287},{"id":2310,"depth":657,"text":2311},{"id":2348,"depth":657,"text":2349},{"id":3453,"depth":647,"text":3454},"Extend role-based permission guards with reactive application state — project status, ownership, quotas, and other runtime conditions — using Vue's computed().","md",null,{},{"title":495},{"title":552,"description":3551},"RTX86NFoXe9zVblJpaizr47TH8QhxJSQSBSNgY1EXlo",[3559,3561],{"title":339,"path":492,"stem":493,"description":3560,"children":-1},"Control collection and field visibility with role-based permission guards. Understand what is enforced in the UI, what requires server-side protection, and how to wire up your own role resolver.",{"title":499,"path":500,"stem":501,"description":3562,"children":-1},"Use protokit as a pure storage layer — Y.js IndexedDB persistence, BroadcastChannel tab sync, schema migrations, and optional server sync — without rendering any built-in components.",1792254332707]