Ship multi-tenant SaaS, not scaffolding.

GStack is an opinionated Nuxt 4 + Supabase starter where access control lives in the database, every subsystem is wired but off by default, and one tenant can never read another's rows — proven by a test, not a promise.
pnpm dlx degit TerrorSquad/gstack my-app   # or: Use this template
cd my-app

pnpm install
pnpm setup          # pick your integrations; writes .env
pnpm supabase start # local Postgres + Auth (needs Docker)
pnpm db:reset       # migrate + seed a demo tenant
pnpm dev            # http://localhost:3000

Batteries included, every one flag-gated

Nothing half-works. Each subsystem is fully wired and switched off until you set its env flag, so a bare clone runs with no third-party account.
  • RLS-first multi-tenancy
    Every table carries a tenant_id and Postgres Row Level Security scopes rows to the caller's tenant. Page-level role gates are UX only — a bug in a component cannot leak another tenant's data.
  • Auth that survives SSR
    Email/password plus GitHub and Google OAuth, password reset, email confirmation, and a global role-aware middleware instead of the Supabase module's redirect.
  • Features as Nuxt Layers
    Marketing, notes, admin, account, billing, email, feedback, tour and analytics are independent layers. Adding a feature is one scaffold command plus one line in extends.
  • Billing
    Polar checkout, customer portal and webhook behind an adapter — a Merchant of Record, so it works from countries Stripe won't onboard.
  • One email shell
    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.
  • Accessibility, enforced
    Playwright and axe run every page in light and dark on a schedule. Contrast overrides live in one design-system layer.

The isolation test

Most starters claim multi-tenancy. This one logs in as a second tenant and asserts the first tenant's rows are invisible through the real HTTP surface — so the claim fails loudly the day someone widens a policy.
e2e/tenant-isolation.spec.ts
test('Globex cannot read Acme rows', async ({ page }) => {
  await login(page, GLOBEX_ADMIN.email)
  await page.goto('/notes')
  await expect(page.getByText(ACME_SECRET_NOTE_TITLE)).toHaveCount(0)
})

Free and MIT licensed

Clone it, rename it with one command, and start building your product instead of its scaffolding.