[{"data":1,"prerenderedAt":683},["ShallowReactive",2],{"navigation":3,"\u002Freference\u002Farchitecture":189,"\u002Freference\u002Farchitecture-surround":678},[4,23,40,51,74,116,157,177],{"title":5,"path":6,"stem":7,"children":8,"icon":22},"Get Started","\u002Fget-started","1.get-started\u002F1.index",[9,12,17],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Prerequisites","\u002Fget-started\u002Fprerequisites","1.get-started\u002F2.prerequisites","i-lucide-list-checks",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fget-started\u002Finstallation","1.get-started\u002F3.installation","i-lucide-settings","i-lucide-rocket",{"title":24,"icon":25,"path":26,"stem":27,"children":28,"page":39},"Develop","i-lucide-code","\u002Fdevelop","2.develop",[29,34],{"title":30,"path":31,"stem":32,"icon":33},"Version Control","\u002Fdevelop\u002Fversion-control","2.develop\u002F1.version-control","i-lucide-git-branch",{"title":35,"path":36,"stem":37,"icon":38},"Claude Code","\u002Fdevelop\u002Fclaude-code","2.develop\u002F2.claude-code","i-lucide-sparkles",false,{"title":41,"icon":42,"path":43,"stem":44,"children":45,"page":39},"Launch","i-lucide-globe","\u002Flaunch","3.launch",[46],{"title":47,"path":48,"stem":49,"icon":50},"Cloudflare","\u002Flaunch\u002Fcloudflare","3.launch\u002F1.cloudflare","i-lucide-cloud-upload",{"title":52,"path":53,"stem":54,"children":55,"icon":73},"Plugins","\u002Fplugins","4.plugins\u002F1.index",[56,58,63,68],{"title":52,"path":53,"stem":54,"icon":57},"i-lucide-list",{"title":59,"path":60,"stem":61,"icon":62},"Public API","\u002Fplugins\u002Fapi-keys","4.plugins\u002F2.api-keys","i-lucide-key",{"title":64,"path":65,"stem":66,"icon":67},"Cron Jobs","\u002Fplugins\u002Fcron-jobs","4.plugins\u002F4.cron-jobs","i-lucide-clock",{"title":69,"path":70,"stem":71,"icon":72},"Rate Limiting","\u002Fplugins\u002Frate-limiting","4.plugins\u002F5.rate-limiting","i-lucide-gauge","i-lucide-puzzle",{"title":75,"path":76,"stem":77,"children":78,"icon":115},"Examples","\u002Fexamples","5.examples\u002F1.index",[79,80,85,90,95,100,105,110],{"title":75,"path":76,"stem":77,"icon":57},{"title":81,"path":82,"stem":83,"icon":84},"Job Management","\u002Fexamples\u002Fjob-management","5.examples\u002F2.job-management","i-lucide-briefcase",{"title":86,"path":87,"stem":88,"icon":89},"Kanban \u002F To-Do List","\u002Fexamples\u002Fkanban-todo","5.examples\u002F3.kanban-todo","i-lucide-kanban",{"title":91,"path":92,"stem":93,"icon":94},"Inventory Management","\u002Fexamples\u002Finventory-management","5.examples\u002F4.inventory-management","i-lucide-package",{"title":96,"path":97,"stem":98,"icon":99},"Mini CRM","\u002Fexamples\u002Fmini-crm","5.examples\u002F5.mini-crm","i-lucide-users",{"title":101,"path":102,"stem":103,"icon":104},"Sales Orders & Invoices","\u002Fexamples\u002Fsales-invoices","5.examples\u002F6.sales-invoices","i-lucide-receipt",{"title":106,"path":107,"stem":108,"icon":109},"Calendar & Booking","\u002Fexamples\u002Fcalendar-booking","5.examples\u002F7.calendar-booking","i-lucide-calendar",{"title":111,"path":112,"stem":113,"icon":114},"Support Tickets","\u002Fexamples\u002Fsupport-tickets","5.examples\u002F8.support-tickets","i-lucide-life-buoy","i-lucide-book-open",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":39},"Reference","i-lucide-file-text","\u002Freference","6.reference",[122,127,132,137,142,147,152],{"title":123,"path":124,"stem":125,"icon":126},"Architecture","\u002Freference\u002Farchitecture","6.reference\u002F1.architecture","i-lucide-layers",{"title":128,"path":129,"stem":130,"icon":131},"Permissions","\u002Freference\u002Fpermissions","6.reference\u002F2.permissions","i-lucide-shield",{"title":133,"path":134,"stem":135,"icon":136},"Invitations","\u002Freference\u002Finvitations","6.reference\u002F3.invitations","i-lucide-mail",{"title":138,"path":139,"stem":140,"icon":141},"Webhooks","\u002Freference\u002Fwebhooks","6.reference\u002F4.webhooks","i-lucide-webhook",{"title":143,"path":144,"stem":145,"icon":146},"AI Chat","\u002Freference\u002Fai-chat","6.reference\u002F5.ai-chat","i-lucide-message-square",{"title":148,"path":149,"stem":150,"icon":151},"Activity Log","\u002Freference\u002Factivity-log","6.reference\u002F6.activity-log","i-lucide-scroll",{"title":153,"path":154,"stem":155,"icon":156},"Manual Setup","\u002Freference\u002Fmanual-setup","6.reference\u002F7.manual-setup","i-lucide-wrench",{"title":158,"icon":159,"path":160,"stem":161,"children":162,"page":39},"Legal","i-lucide-scale","\u002Flegal","7.legal",[163,168,172],{"title":164,"path":165,"stem":166,"icon":167},"License","\u002Flegal\u002Flicense","7.legal\u002F1.license","i-lucide-file-check",{"title":169,"path":170,"stem":171,"icon":118},"Terms and Conditions","\u002Flegal\u002Fterms","7.legal\u002F2.terms",{"title":173,"path":174,"stem":175,"icon":176},"Privacy Policy","\u002Flegal\u002Fprivacy","7.legal\u002F3.privacy","i-lucide-lock",{"title":178,"path":179,"stem":180,"children":181,"icon":183},"Upgrades","\u002Fupgrades","8.upgrades\u002F1.index",[182,184],{"title":178,"path":179,"stem":180,"icon":183},"i-lucide-arrow-up-circle",{"title":185,"path":186,"stem":187,"icon":188},"\u002Fapp\u002F* gated subtree routing","\u002Fupgrades\u002Fapp-subtree-routing","8.upgrades\u002F2.app-subtree-routing","i-lucide-route",{"id":190,"title":123,"body":191,"description":671,"extension":672,"links":673,"meta":674,"navigation":675,"path":124,"seo":676,"stem":125,"__hash__":677},"docs\u002F6.reference\u002F1.architecture.md",{"type":192,"value":193,"toc":657},"minimark",[194,199,207,213,244,250,256,262,273,296,302,326,332,338,344,348,382,386,389,394,408,414,456,460,467,472,482,490,501,509,520,531,545,550,577,581,609,613,624],[195,196,198],"h2",{"id":197},"what-the-template-includes","What the template includes",[200,201,202,206],"p",{},[203,204,205],"strong",{},"Authentication"," — Email\u002Fpassword login and signup with Supabase Auth.\nGlobal route middleware protects every page. Onboarding flow for first-time\nusers. Invite-based account creation for team members.",[200,208,209,212],{},[203,210,211],{},"Multi-tenancy"," — Teams are the data isolation boundary. Every row is scoped\nto a team via RLS policies. Users can belong to multiple teams and switch\nbetween them from the sidebar. A team-id cookie persists the selection.",[200,214,215,218,219,223,224,227,228,231,232,235,236,239,240,243],{},[203,216,217],{},"Role-based access"," — Owner, admin, and member roles with a centralised\npermissions system. One file (",[220,221,222],"code",{},"shared\u002Fpermissions.ts",") defines every role and\nwhat it can do. The ",[220,225,226],{},"authUser"," server utility and ",[220,229,230],{},"useUserRole"," composable\nboth read from this map. The ",[220,233,234],{},"\u003CCanAccess>"," component gates UI elements\ndeclaratively and the ",[220,237,238],{},"requirePermission"," route middleware redirects before\nnavigation. See ",[241,242,128],"a",{"href":129}," for full details.",[200,245,246,249],{},[203,247,248],{},"Team management"," — Create teams, update name and avatar, configure webhook\nURLs, delete teams with type-to-confirm safety. All behind a clean settings\nUI built with Nuxt UI dashboard components.",[200,251,252,255],{},[203,253,254],{},"Member management"," — Invite people by email, assign roles, change roles\nlive, remove members with confirmation modals. Pending invitations section\nwith copy-link and revoke actions.",[200,257,258,261],{},[203,259,260],{},"Realtime"," — Supabase Realtime keeps everything in sync across all connected\nclients. Member list, invitations, team settings, announcements, chats, the\nactivity log, and the sidebar all update instantly when another user (or\nanother tab) makes a change. When a user is removed from their last team,\nthey are redirected out immediately.",[200,263,264,266,267,269,270,272],{},[203,265,138],{}," — Every team mutation (invite created, member removed, role\nchanged, team updated\u002Fdeleted) fires a webhook to a configurable URL.\nPayloads include full context. See ",[241,268,133],{"href":134}," and ",[241,271,138],{"href":139}," for details.",[200,274,275,277,278,281,282,285,286,289,290,293,294,243],{},[203,276,143],{}," — Baked-in AI assistant that queries the database in natural\nlanguage and performs writes through typed tool calls. Scoped to the current\nteam, gated per-table via the ",[220,279,280],{},"tablePermissions"," map, and run through a\nsandboxed ",[220,283,284],{},"chat_reader"," Postgres role so RLS (not the LLM) decides visibility.\nWrites are tagged ",[220,287,288],{},"actor_source='chat'"," in the activity log. Enabled when\n",[220,291,292],{},"OPENROUTER_API_KEY"," is set; the UI is hidden otherwise. See\n",[241,295,143],{"href":144},[200,297,298,301],{},[203,299,300],{},"Announcements"," — Owners and admins can publish banner announcements\nvisible to everyone on the team. Configurable title, description, icon,\ncolor, link target, and CTA actions. Managed in Settings → Announcements.",[200,303,304,307,308,311,312,311,315,318,319,322,323,325],{},[203,305,306],{},"Activity log"," — Every mutation on opted-in tables is captured by a\nPostgres AFTER trigger — actor, source (",[220,309,310],{},"api","\u002F",[220,313,314],{},"chat",[220,316,317],{},"system","), before\u002Fafter\nsnapshot, and per-column diff. Visible at ",[220,320,321],{},"\u002Fapp\u002Factivity"," to team owners only.\nSee ",[241,324,148],{"href":149}," for how to opt new tables in.",[200,327,328,331],{},[203,329,330],{},"Avatar uploads"," — Reusable upload component for both user profiles and team\navatars. Handles upload, delete, and preview. Backed by Supabase Storage.",[200,333,334,337],{},[203,335,336],{},"Confirmation modals"," — Two-tier destructive action protection. Standard\nactions get a confirm dialog. High-impact actions (like deleting a team)\nrequire typing a confirmation string.",[200,339,340,343],{},[203,341,342],{},"Dashboard UI"," — Built entirely with Nuxt UI's dashboard components:\ncollapsible\u002Fresizable sidebar, team switcher dropdown, navigation menu,\ncommand palette search, user profile dropdown with theme toggle. Responsive\nand polished out of the box.",[195,345,347],{"id":346},"stack","Stack",[349,350,351,358,364,370,376],"ul",{},[352,353,354,357],"li",{},[203,355,356],{},"Nuxt 3"," — Vue 3 with SSR, file-based routing, auto-imports",[352,359,360,363],{},[203,361,362],{},"Supabase"," — Auth, Postgres with RLS, Realtime, Storage",[352,365,366,369],{},[203,367,368],{},"Nuxt UI"," — Component library with dashboard layout primitives",[352,371,372,375],{},[203,373,374],{},"Vercel AI SDK"," — model-agnostic streaming, tool calls, typed inputs\n(used by the baked-in AI chat)",[352,377,378,381],{},[203,379,380],{},"TypeScript"," — End-to-end type safety with generated database types",[195,383,385],{"id":384},"server-architecture","Server architecture",[200,387,388],{},"All database writes go through Nitro server routes using the Supabase service\nrole key. Vue components never call Supabase directly for mutations.",[200,390,391],{},[203,392,393],{},"Why:",[349,395,396,399,402,405],{},[352,397,398],{},"RLS is your first layer of defence, but not your only one. Server routes\ngive you a second layer you fully control.",[352,400,401],{},"The anon key is public and exposed in the browser. With server routes, all\nsensitive operations require a valid session verified on your server first.",[352,403,404],{},"Business logic (validation, permission checks, side effects) lives in one\nclear place instead of being scattered across components.",[352,406,407],{},"The service role key bypasses RLS and must never be exposed to the browser.\nKeeping it exclusively in Nitro server routes makes it invisible to users.",[200,409,410,413],{},[203,411,412],{},"The pattern:"," Vue components call your own API routes → your server verifies\nidentity and permission → then performs the database operation using the\nservice role key.",[200,415,416,419,420,423,424,427,428,431,432,435,436,439,440,443,444,447,448,451,452,455],{},[203,417,418],{},"Audited client"," — ",[220,421,422],{},"server\u002Futils\u002FcreateAuditedClient.ts"," wraps the service\nrole key and attaches actor + team headers (",[220,425,426],{},"x-actor-id",", ",[220,429,430],{},"x-team-id",",\n",[220,433,434],{},"x-actor-source",") to every PostgREST request. The ",[220,437,438],{},"log_activity()"," trigger\nreads those headers to attribute every mutation. ",[220,441,442],{},"authUser()"," \u002F ",[220,445,446],{},"authUserOnly()","\nreturn this client by default. Chat-driven writes override ",[220,449,450],{},"source: 'chat'","\nand pass ",[220,453,454],{},"sourceRef: \u003CchatId>"," so activity log rows link back to the chat\nthat caused them.",[195,457,459],{"id":458},"deployment-model-internal-app-vs-saas","Deployment model: internal app vs SaaS",[200,461,462,463,466],{},"This template ships configured as an ",[203,464,465],{},"internal app",": you build it for a\nspecific business, you control who signs up, and kicked members cannot create\ntheir own parallel teams inside your app. The multi-tenancy (teams, roles,\nRLS) is still fully in place — it just behaves like \"one organization, many\nmembers\" rather than \"many organizations, self-serve\".",[468,469,471],"h3",{"id":470},"flipping-to-saas","Flipping to SaaS",[200,473,474,475,478,479,481],{},"Flipping the template into a ",[203,476,477],{},"SaaS model"," (self-serve signup, anyone can\ncreate a team, one codebase serving many unrelated businesses) is four small\nedits. None of the architecture changes — teams remain the tenant boundary,\nRLS policies remain team-scoped, ",[220,480,226],{}," still enforces roles. You are only\nrelaxing the guardrails that make sense for an internal deployment.",[200,483,484,419,487],{},[203,485,486],{},"1. Allow deleting your only team",[220,488,489],{},"server\u002Fapi\u002Fteams\u002F[teamId]\u002Findex.delete.ts",[200,491,492,493,496,497,500],{},"The internal app refuses to delete an owner's last team so they are not\nstranded on ",[220,494,495],{},"\u002Fno-team",". In SaaS that is fine because they can just create a new\none. Remove the ",[220,498,499],{},"otherTeamCount"," guard block.",[200,502,503,419,506],{},[203,504,505],{},"2. Ungate \"Create team\" in the sidebar",[220,507,508],{},"app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FHeader.vue",[200,510,511,512,515,516,519],{},"The Create team menu item is gated behind the ",[220,513,514],{},"settings.team"," permission\n(owner of the current team), because in an internal app a kicked member\nspinning up their own shadow team would defeat the point of kicking them. In\nSaaS, team creation is a user-level action — any authenticated user should be\nable to create their own workspace. Remove the ",[220,517,518],{},"can(\"settings.team\")","\nconditional so the menu item always renders.",[200,521,522,419,528],{},[203,523,524,525,527],{},"3. Give ",[220,526,495],{}," a recovery path",[220,529,530],{},"app\u002Fpages\u002Fno-team.vue",[200,532,533,534,536,537,540,541,544],{},"The internal app treats ",[220,535,495],{}," as an unreachable-in-normal-flow dead end\nand tells the user to contact their admin. In SaaS it is a legitimate state\n(new signup who has not created a workspace yet, or someone who just left\ntheir last team) and needs a \"Create a team\" button. Replace the sign-out\nfallback with a primary button opening ",[220,538,539],{},"LayoutCreateTeamModal"," — the modal\nalready exists and the create flow already refreshes the JWT's ",[220,542,543],{},"has_team","\nclaim, so no additional wiring is needed.",[200,546,547],{},[203,548,549],{},"4. Decide on public signup",[200,551,552,553,556,557,561,562,564,565,568,569,572,573,576],{},"This is the one that is already SaaS-shaped: ",[220,554,555],{},"pages\u002Fauth\u002Fsignup.vue"," is\npublicly reachable and anyone with a valid email can create an account. For\nSaaS, keep it. For a ",[558,559,560],"em",{},"strict"," internal app where accounts only come into\nexistence via invitation, delete ",[220,563,555],{}," and\n",[220,566,567],{},"components\u002Fauth\u002FSignupForm.vue",", remove the \"Sign up\" link from\n",[220,570,571],{},"components\u002Fauth\u002FLoginForm.vue",", and disable signups in the Supabase\ndashboard (Authentication → Providers → Email → Disable signups). The\n",[220,574,575],{},"\u002Finvite\u002F[token]"," flow handles invite-only account creation independently\nand will keep working.",[468,578,580],{"id":579},"what-you-do-not-need-to-change","What you do not need to change",[349,582,583,589,597,603],{},[352,584,585,588],{},[203,586,587],{},"RLS policies"," — already team-scoped, correct for both models.",[352,590,591,596],{},[203,592,593,594],{},"Permissions and ",[220,595,226],{}," — owner\u002Fadmin\u002Fmember roles work the same\nway in SaaS. The difference is just how many teams a user can belong to.",[352,598,599,602],{},[203,600,601],{},"Onboarding auto-creating a first team"," — fine for both. SaaS users\nexpect to land in a workspace; they can rename it afterward in settings.",[352,604,605,608],{},[203,606,607],{},"Invite flow, webhooks, avatar storage, team context resolution"," — all\nmodel-agnostic.",[468,610,612],{"id":611},"what-saas-additionally-needs-not-provided","What SaaS additionally needs (not provided)",[200,614,615,616,619,620,623],{},"Out of scope for this template, but worth naming so you know what is missing:\nbilling\u002Fsubscriptions (Stripe + a ",[220,617,618],{},"subscriptions"," table keyed to ",[220,621,622],{},"team_id","),\nseat\u002Fplan enforcement in middleware, per-team usage quotas, and email\nverification enforcement (toggle in Supabase).",[200,625,626,627,629,630,633,634,564,637,639,640,427,643,431,646,427,649,652,653,656],{},"Public marketing pages: the template ships with a minimal public homepage at\n",[220,628,311],{},", and the global middleware gates only ",[220,631,632],{},"\u002Fapp\u002F*"," (plus ",[220,635,636],{},"\u002Fonboarding",[220,638,495],{},"). Anything else at the top level — ",[220,641,642],{},"\u002Fpricing",[220,644,645],{},"\u002Fabout",[220,647,648],{},"\u002Fblog\u002F*",[220,650,651],{},"\u002Flegal\u002F*"," — is public by default. Add files under\n",[220,654,655],{},"app\u002Fpages\u002F"," and they render unauthenticated; the marketing surface is\nalready wired up.",{"title":658,"searchDepth":659,"depth":660,"links":661},"",1,2,[662,663,664,665],{"id":197,"depth":660,"text":198},{"id":346,"depth":660,"text":347},{"id":384,"depth":660,"text":385},{"id":458,"depth":660,"text":459,"children":666},[667,669,670],{"id":470,"depth":668,"text":471},3,{"id":579,"depth":668,"text":580},{"id":611,"depth":668,"text":612},"What ships in the template and how the pieces fit together.","md",null,{},{"icon":126},{"title":123,"description":671},"6amL9Dmkaj9LyuHUWkwegF6vWLCcS05mAXnlL-oG2d4",[679,681],{"title":111,"path":112,"stem":113,"description":680,"icon":114,"children":-1},"Build a helpdesk system with tickets, priorities, assignments, SLA tracking, and canned responses",{"title":128,"path":129,"stem":130,"description":682,"icon":131,"children":-1},"How roles and permissions work, and how to add your own.",1777092171073]