Nuxt 4 · Supabase · Postgres RLS

Ship multi-tenant SaaS, not scaffolding.

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
01 — The guarantee

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 rendered page, the search box, and the SSR payload. Widen a policy by accident and this goes red.
e2e/tenant-isolation.spec.ts
test('a Globex admin cannot see Acme notes', async ({ page }) => {
  await login(page, ADMIN2.email)

  // The seeded Acme secret title must be absent from Globex's notes list.
  await page.goto('/notes')
  await expect(page.getByText(ACME_SECRET_NOTE_TITLE)).toHaveCount(0)
})

Every table carries a tenant_id, and RLS scopes rows through a current_tenant_id() security-definer helper that reads the caller's own profile. Page-level role gates are UX only — a bug in a component cannot leak another tenant's data, because the component never had the rows.

How multi-tenancy works
02 — Batteries included

Every subsystem is one env flag

Nothing half-works. Each subsystem is fully wired and switched off until you set its flag, so a bare clone runs end-to-end with zero third-party accounts — Supabase runs locally in Docker.
SubsystemFlagNeeds an account
Supabase — Postgres, Auth, RLS, Storagecore, always onNo (local via Docker)
Feedback widget → your own DBNUXT_PUBLIC_FEEDBACK_ENABLEDNo
Onboarding tour (driver.js)NUXT_PUBLIC_TOUR_ENABLEDNo
Email notificationsNUXT_PUBLIC_NOTIFICATIONS_ENABLEDResend
Billing — checkout, portal, webhooksNUXT_PUBLIC_BILLING_ENABLEDPolar
Product analytics + feature flagsNUXT_PUBLIC_POSTHOG_ENABLEDPostHog
GitHub / Google OAuth loginset the keysGitHub / Google
Error trackingset the DSNSentry
Log forwardingset the tokenBetterStack

pnpm setup writes the flags, pnpm doctor verifies them, and both read the same manifest — so a subsystem can't be half-configured without one of them saying so.

Configuration reference
03 — Proof, not adjectives

What CI won't let you break

The interesting part of a starter isn't what it ships on day one, it's what it refuses to let you regress on day ninety.
GateWhat it catches
pnpm lintoxlint + oxfmt, auto-fixing on pre-commit
pnpm lint:i18na key added to en but not sr, or a key nothing uses
pnpm testlogic + the email-shell drift test: auth templates that no longer match their generator
pnpm typechecka query that under-selects, against types generated from the live schema
pnpm test:e2etenant isolation, auth flows, notes CRUD
axe, twice dailycontrast and landmark failures, on every page, in both light and dark

Releases are cut by release-please from conventional commits; the user-facing changelog is curated separately, so shipping a refactor doesn't spam your users.

04 — Architecture

Features are layers, not folders

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.
  • Type-safe end to end
    Postgres schema → generated types → composables → UI. No ORM. Change a column and the build tells you every call site that cared.
  • Auth that survives SSR
    Email/password plus GitHub and Google OAuth, password reset, confirmation, and a role-aware global middleware instead of the Supabase module's redirect.
  • 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.
  • Bilingual from the start
    English and Serbian, with key parity enforced in CI. Dynamic keys are written so the usage checker can still resolve them.
  • Portable deploy
    Nitro output, so Vercel is the default and not a lock-in. Supabase runs locally in Docker for development and CI alike.
  • Decisions on the record
    The reversible-but-significant choices — Polar over Stripe, no ORM yet, layers over a monorepo — are written down as ADRs with their trade-offs.

Free and MIT licensed

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