[{"data":1,"prerenderedAt":676},["ShallowReactive",2],{"navigation_docs":3,"landing":60},[4,25,45],{"title":5,"path":6,"stem":7,"children":8,"page":24},"Getting Started","\u002Fdocs\u002Fgetting-started","docs\u002F1.getting-started",[9,14,19],{"title":10,"path":11,"stem":12,"icon":13},"Introduction","\u002Fdocs\u002Fgetting-started\u002Fintroduction","docs\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":15,"path":16,"stem":17,"icon":18},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","docs\u002F1.getting-started\u002F2.installation","i-lucide-rocket",{"title":20,"path":21,"stem":22,"icon":23},"Configuration","\u002Fdocs\u002Fgetting-started\u002Fconfiguration","docs\u002F1.getting-started\u002F3.configuration","i-lucide-sliders-horizontal",false,{"title":26,"path":27,"stem":28,"children":29,"page":24},"Architecture","\u002Fdocs\u002Farchitecture","docs\u002F2.architecture",[30,35,40],{"title":31,"path":32,"stem":33,"icon":34},"Layers","\u002Fdocs\u002Farchitecture\u002Flayers","docs\u002F2.architecture\u002F1.layers","i-lucide-layers",{"title":36,"path":37,"stem":38,"icon":39},"Multi-tenancy","\u002Fdocs\u002Farchitecture\u002Fmulti-tenancy","docs\u002F2.architecture\u002F2.multi-tenancy","i-lucide-building-2",{"title":41,"path":42,"stem":43,"icon":44},"Testing","\u002Fdocs\u002Farchitecture\u002Ftesting","docs\u002F2.architecture\u002F3.testing","i-lucide-flask-conical",{"title":46,"path":47,"stem":48,"children":49,"page":24},"Reference","\u002Fdocs\u002Freference","docs\u002F3.reference",[50,55],{"title":51,"path":52,"stem":53,"icon":54},"Commands","\u002Fdocs\u002Freference\u002Fcommands","docs\u002F3.reference\u002F1.commands","i-lucide-terminal",{"title":56,"path":57,"stem":58,"icon":59},"Decisions","\u002Fdocs\u002Freference\u002Fdecisions","docs\u002F3.reference\u002F2.decisions","i-lucide-git-branch",{"id":61,"title":62,"body":63,"description":62,"extension":668,"meta":669,"navigation":148,"path":670,"seo":671,"stem":674,"__hash__":675},"landing\u002Findex.md","",{"type":64,"value":65,"toc":666},"minimark",[66,205,384,551,647,653,662],[67,68,72,76,79,82,98],"u-page-hero",{"className":69,"orientation":71},[70],"grid-surface","horizontal",[73,74,75],"template",{"v-slot:headline":62},"Nuxt 4 · Supabase · Postgres RLS",[73,77,78],{"v-slot:title":62},"Ship multi-tenant SaaS, not scaffolding.",[73,80,81],{"v-slot:description":62},"An opinionated Nuxt 4 + Supabase starter where access control lives in the\ndatabase, every subsystem is wired but off by default, and one tenant can never\nread another's rows — proven by a test, not a promise.",[73,83,84,90],{"v-slot:links":62},[85,86,89],"u-button",{"to":16,"trailing-icon":87,"size":88},"i-lucide-arrow-right","xl","Get started",[85,91,97],{"to":92,"variant":93,"size":88,"color":94,"icon":95,"target":96},"https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fgstack","subtle","neutral","i-simple-icons-github","_blank","View on GitHub",[99,100,104],"pre",{"className":101,"code":102,"language":103,"meta":62,"style":62},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pnpm dlx degit TerrorSquad\u002Fgstack my-app   # or: Use this template\ncd my-app\n\npnpm install\npnpm setup          # pick your integrations; writes .env\npnpm supabase start # local Postgres + Auth (needs Docker)\npnpm db:reset       # migrate + seed a demo tenant\npnpm dev            # http:\u002F\u002Flocalhost:3000\n","bash",[105,106,107,133,143,150,158,169,183,194],"code",{"__ignoreMap":62},[108,109,112,116,120,123,126,129],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"sBMFI","pnpm",[108,117,119],{"class":118},"sfazB"," dlx",[108,121,122],{"class":118}," degit",[108,124,125],{"class":118}," TerrorSquad\u002Fgstack",[108,127,128],{"class":118}," my-app",[108,130,132],{"class":131},"sHwdD","   # or: Use this template\n",[108,134,136,140],{"class":110,"line":135},2,[108,137,139],{"class":138},"s2Zo4","cd",[108,141,142],{"class":118}," my-app\n",[108,144,146],{"class":110,"line":145},3,[108,147,149],{"emptyLinePlaceholder":148},true,"\n",[108,151,153,155],{"class":110,"line":152},4,[108,154,115],{"class":114},[108,156,157],{"class":118}," install\n",[108,159,161,163,166],{"class":110,"line":160},5,[108,162,115],{"class":114},[108,164,165],{"class":118}," setup",[108,167,168],{"class":131},"          # pick your integrations; writes .env\n",[108,170,172,174,177,180],{"class":110,"line":171},6,[108,173,115],{"class":114},[108,175,176],{"class":118}," supabase",[108,178,179],{"class":118}," start",[108,181,182],{"class":131}," # local Postgres + Auth (needs Docker)\n",[108,184,186,188,191],{"class":110,"line":185},7,[108,187,115],{"class":114},[108,189,190],{"class":118}," db:reset",[108,192,193],{"class":131},"       # migrate + seed a demo tenant\n",[108,195,197,199,202],{"class":110,"line":196},8,[108,198,115],{"class":114},[108,200,201],{"class":118}," dev",[108,203,204],{"class":131},"            # http:\u002F\u002Flocalhost:3000\n",[206,207,209,212,215,368,380],"u-page-section",{"headline":208},"01 — The guarantee",[73,210,211],{"v-slot:title":62},"The isolation test",[73,213,214],{"v-slot:description":62},"Most starters claim multi-tenancy. This one logs in as a second tenant and\nasserts the first tenant's rows are invisible — through the rendered page, the\nsearch box, and the SSR payload. Widen a policy by accident and this goes red.",[99,216,221],{"className":217,"code":218,"filename":219,"language":220,"meta":62,"style":62},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","test('a Globex admin cannot see Acme notes', async ({ page }) => {\n  await login(page, ADMIN2.email)\n\n  \u002F\u002F The seeded Acme secret title must be absent from Globex's notes list.\n  await page.goto('\u002Fnotes')\n  await expect(page.getByText(ACME_SECRET_NOTE_TITLE)).toHaveCount(0)\n})\n","e2e\u002Ftenant-isolation.spec.ts","ts",[105,222,223,264,293,297,302,324,361],{"__ignoreMap":62},[108,224,225,228,232,236,239,241,244,248,251,255,258,261],{"class":110,"line":111},[108,226,227],{"class":138},"test",[108,229,231],{"class":230},"sTEyZ","(",[108,233,235],{"class":234},"sMK4o","'",[108,237,238],{"class":118},"a Globex admin cannot see Acme notes",[108,240,235],{"class":234},[108,242,243],{"class":234},",",[108,245,247],{"class":246},"spNyl"," async",[108,249,250],{"class":234}," ({",[108,252,254],{"class":253},"sHdIc"," page",[108,256,257],{"class":234}," })",[108,259,260],{"class":246}," =>",[108,262,263],{"class":234}," {\n",[108,265,266,270,273,276,279,281,284,287,290],{"class":110,"line":135},[108,267,269],{"class":268},"s7zQu","  await",[108,271,272],{"class":138}," login",[108,274,231],{"class":275},"swJcz",[108,277,278],{"class":230},"page",[108,280,243],{"class":234},[108,282,283],{"class":230}," ADMIN2",[108,285,286],{"class":234},".",[108,288,289],{"class":230},"email",[108,291,292],{"class":275},")\n",[108,294,295],{"class":110,"line":145},[108,296,149],{"emptyLinePlaceholder":148},[108,298,299],{"class":110,"line":152},[108,300,301],{"class":131},"  \u002F\u002F The seeded Acme secret title must be absent from Globex's notes list.\n",[108,303,304,306,308,310,313,315,317,320,322],{"class":110,"line":160},[108,305,269],{"class":268},[108,307,254],{"class":230},[108,309,286],{"class":234},[108,311,312],{"class":138},"goto",[108,314,231],{"class":275},[108,316,235],{"class":234},[108,318,319],{"class":118},"\u002Fnotes",[108,321,235],{"class":234},[108,323,292],{"class":275},[108,325,326,328,331,333,335,337,340,342,345,348,350,353,355,359],{"class":110,"line":171},[108,327,269],{"class":268},[108,329,330],{"class":138}," expect",[108,332,231],{"class":275},[108,334,278],{"class":230},[108,336,286],{"class":234},[108,338,339],{"class":138},"getByText",[108,341,231],{"class":275},[108,343,344],{"class":230},"ACME_SECRET_NOTE_TITLE",[108,346,347],{"class":275},"))",[108,349,286],{"class":234},[108,351,352],{"class":138},"toHaveCount",[108,354,231],{"class":275},[108,356,358],{"class":357},"sbssI","0",[108,360,292],{"class":275},[108,362,363,366],{"class":110,"line":185},[108,364,365],{"class":234},"}",[108,367,292],{"class":230},[369,370,371,372,375,376,379],"p",{},"Every table carries a ",[105,373,374],{},"tenant_id",", and RLS scopes rows through a\n",[105,377,378],{},"current_tenant_id()"," security-definer helper that reads the caller's own\nprofile. Page-level role gates are UX only — a bug in a component cannot leak\nanother tenant's data, because the component never had the rows.",[85,381],{"to":37,"trailing-icon":87,"variant":382,"label":383},"link","How multi-tenancy works",[206,385,387,390,398,538,548],{"headline":386},"02 — Batteries included",[73,388,389],{"v-slot:title":62},"Every subsystem is one env flag",[73,391,392,393,397],{"v-slot:description":62},"Nothing half-works. Each subsystem is fully wired and switched off until you set\nits flag, so a bare clone runs end-to-end with ",[394,395,396],"strong",{},"zero third-party accounts"," —\nSupabase runs locally in Docker.",[399,400,401,417],"table",{},[402,403,404],"thead",{},[405,406,407,411,414],"tr",{},[408,409,410],"th",{},"Subsystem",[408,412,413],{},"Flag",[408,415,416],{},"Needs an account",[418,419,420,435,448,460,473,486,499,512,525],"tbody",{},[405,421,422,426,432],{},[423,424,425],"td",{},"Supabase — Postgres, Auth, RLS, Storage",[423,427,428],{},[429,430,431],"em",{},"core, always on",[423,433,434],{},"No (local via Docker)",[405,436,437,440,445],{},[423,438,439],{},"Feedback widget → your own DB",[423,441,442],{},[105,443,444],{},"NUXT_PUBLIC_FEEDBACK_ENABLED",[423,446,447],{},"No",[405,449,450,453,458],{},[423,451,452],{},"Onboarding tour (driver.js)",[423,454,455],{},[105,456,457],{},"NUXT_PUBLIC_TOUR_ENABLED",[423,459,447],{},[405,461,462,465,470],{},[423,463,464],{},"Email notifications",[423,466,467],{},[105,468,469],{},"NUXT_PUBLIC_NOTIFICATIONS_ENABLED",[423,471,472],{},"Resend",[405,474,475,478,483],{},[423,476,477],{},"Billing — checkout, portal, webhooks",[423,479,480],{},[105,481,482],{},"NUXT_PUBLIC_BILLING_ENABLED",[423,484,485],{},"Polar",[405,487,488,491,496],{},[423,489,490],{},"Product analytics + feature flags",[423,492,493],{},[105,494,495],{},"NUXT_PUBLIC_POSTHOG_ENABLED",[423,497,498],{},"PostHog",[405,500,501,504,509],{},[423,502,503],{},"GitHub \u002F Google OAuth login",[423,505,506],{},[429,507,508],{},"set the keys",[423,510,511],{},"GitHub \u002F Google",[405,513,514,517,522],{},[423,515,516],{},"Error tracking",[423,518,519],{},[429,520,521],{},"set the DSN",[423,523,524],{},"Sentry",[405,526,527,530,535],{},[423,528,529],{},"Log forwarding",[423,531,532],{},[429,533,534],{},"set the token",[423,536,537],{},"BetterStack",[369,539,540,543,544,547],{},[105,541,542],{},"pnpm setup"," writes the flags, ",[105,545,546],{},"pnpm doctor"," verifies them, and both read the\nsame manifest — so a subsystem can't be half-configured without one of them\nsaying so.",[85,549],{"to":21,"trailing-icon":87,"variant":382,"label":550},"Configuration reference",[206,552,554,557,560,644],{"headline":553},"03 — Proof, not adjectives",[73,555,556],{"v-slot:title":62},"What CI won't let you break",[73,558,559],{"v-slot:description":62},"The interesting part of a starter isn't what it ships on day one, it's what it\nrefuses to let you regress on day ninety.",[399,561,562,572],{},[402,563,564],{},[405,565,566,569],{},[408,567,568],{},"Gate",[408,570,571],{},"What it catches",[418,573,574,584,602,612,622,632],{},[405,575,576,581],{},[423,577,578],{},[105,579,580],{},"pnpm lint",[423,582,583],{},"oxlint + oxfmt, auto-fixing on pre-commit",[405,585,586,591],{},[423,587,588],{},[105,589,590],{},"pnpm lint:i18n",[423,592,593,594,597,598,601],{},"a key added to ",[105,595,596],{},"en"," but not ",[105,599,600],{},"sr",", or a key nothing uses",[405,603,604,609],{},[423,605,606],{},[105,607,608],{},"pnpm test",[423,610,611],{},"logic + the email-shell drift test: auth templates that no longer match their generator",[405,613,614,619],{},[423,615,616],{},[105,617,618],{},"pnpm typecheck",[423,620,621],{},"a query that under-selects, against types generated from the live schema",[405,623,624,629],{},[423,625,626],{},[105,627,628],{},"pnpm test:e2e",[423,630,631],{},"tenant isolation, auth flows, notes CRUD",[405,633,634,637],{},[423,635,636],{},"axe, twice daily",[423,638,639,640,643],{},"contrast and landmark failures, on every page, in ",[394,641,642],{},"both"," light and dark",[369,645,646],{},"Releases are cut by release-please from conventional commits; the user-facing\nchangelog is curated separately, so shipping a refactor doesn't spam your users.",[206,648],{"headline":649,":features":650,"description":651,"title":652},"04 — Architecture","[{\"title\":\"Type-safe end to end\",\"description\":\"Postgres schema → generated types → composables → UI. No ORM. Change a column and the build tells you every call site that cared.\",\"icon\":\"i-lucide-shield-check\"},{\"title\":\"Auth that survives SSR\",\"description\":\"Email\u002Fpassword plus GitHub and Google OAuth, password reset, confirmation, and a role-aware global middleware instead of the Supabase module's redirect.\",\"icon\":\"i-lucide-key-round\"},{\"title\":\"One email shell\",\"description\":\"Transactional mail and the Supabase auth templates render from the same generated shell, so the branding can't drift. A unit test fails if it does.\",\"icon\":\"i-lucide-mail\"},{\"title\":\"Bilingual from the start\",\"description\":\"English and Serbian, with key parity enforced in CI. Dynamic keys are written so the usage checker can still resolve them.\",\"icon\":\"i-lucide-languages\"},{\"title\":\"Portable deploy\",\"description\":\"Nitro output, so Vercel is the default and not a lock-in. Supabase runs locally in Docker for development and CI alike.\",\"icon\":\"i-lucide-cloud\"},{\"title\":\"Decisions on the record\",\"description\":\"The reversible-but-significant choices — Polar over Stripe, no ORM yet, layers over a monorepo — are written down as ADRs with their trade-offs.\",\"icon\":\"i-lucide-file-text\"}]","Marketing, notes, admin, account, billing, email, feedback, tour and analytics are ten independent Nuxt layers over a shared design-system layer. Adding a feature is one scaffold command plus one line in extends — and deleting one is deleting a directory.","Features are layers, not folders",[206,654,656,659],{":links":655},"[{\"label\":\"Read the docs\",\"to\":\"\u002Fdocs\u002Fgetting-started\u002Fintroduction\",\"size\":\"xl\",\"trailingIcon\":\"i-lucide-arrow-right\"},{\"label\":\"Browse the decisions\",\"to\":\"\u002Fdocs\u002Freference\u002Fdecisions\",\"size\":\"xl\",\"color\":\"neutral\",\"variant\":\"subtle\"}]",[73,657,658],{"v-slot:title":62},"Free and MIT licensed",[73,660,661],{"v-slot:description":62},"Clone it, rename it with one command, and start building your product instead of\nits scaffolding.",[663,664,665],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 .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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":62,"searchDepth":135,"depth":135,"links":667},[],"md",{},"\u002F",{"title":672,"description":673},"GStack — RLS-first Nuxt + Supabase SaaS starter","An opinionated, type-safe, batteries-included stack for shipping multi-tenant SaaS with Nuxt. Auth, RLS multi-tenancy, billing, email, i18n, a11y and release automation, wired and flag-gated.","index","b2jy2ciyRtxsVpG6kSnpIwqfGA5pS2pRMqMSWnoqn5I",1785947750863]