Architecture
What the template includes
Authentication — Email/password login and signup with Supabase Auth. Global route middleware protects every page. Onboarding flow for first-time users. Invite-based account creation for team members.
Multi-tenancy — Teams are the data isolation boundary. Every row is scoped to a team via RLS policies. Users can belong to multiple teams and switch between them from the sidebar. A team-id cookie persists the selection.
Role-based access — Owner, admin, and member roles with a centralised permissions system. One file (shared/permissions.ts) defines every role and what it can do. The authUser server utility and useUserRole composable both read from this map. The <CanAccess> component gates UI elements declaratively. See Permissions for full details.
Team management — Create teams, update name and avatar, configure webhook URLs, delete teams with type-to-confirm safety. All behind a clean settings UI built with Nuxt UI dashboard components.
Member management — Invite people by email, assign roles, change roles live, remove members with confirmation modals. Pending invitations section with copy-link and revoke actions.
Realtime — Supabase Realtime keeps everything in sync across all connected clients. Member list, invitations, team settings, and the sidebar all update instantly when another user makes a change. When a user is removed from their last team, they are redirected out immediately.
Webhooks — Every team mutation (invite created, member removed, role changed, team updated/deleted) fires a webhook to a configurable URL. Payloads include full context. See Invitations & Webhooks for details.
Avatar uploads — Reusable upload component for both user profiles and team avatars. Handles upload, delete, and preview. Backed by Supabase Storage.
Confirmation modals — Two-tier destructive action protection. Standard actions get a confirm dialog. High-impact actions (like deleting a team) require typing a confirmation string.
Dashboard UI — Built entirely with Nuxt UI's dashboard components: collapsible/resizable sidebar, team switcher dropdown, navigation menu, command palette search, user profile dropdown with theme toggle. Responsive and polished out of the box.
Stack
- Nuxt 3 — Vue 3 with SSR, file-based routing, auto-imports
- Supabase — Auth, Postgres with RLS, Realtime, Storage
- Nuxt UI — Component library with dashboard layout primitives
- TypeScript — End-to-end type safety with generated database types
Server architecture
All database writes go through Nitro server routes using the Supabase service role key. Vue components never call Supabase directly for mutations.
Why:
- RLS is your first layer of defence, but not your only one. Server routes give you a second layer you fully control.
- The anon key is public and exposed in the browser. With server routes, all sensitive operations require a valid session verified on your server first.
- Business logic (validation, permission checks, side effects) lives in one clear place instead of being scattered across components.
- The service role key bypasses RLS and must never be exposed to the browser. Keeping it exclusively in Nitro server routes makes it invisible to users.
The pattern: Vue components call your own API routes → your server verifies identity and permission → then performs the database operation using the service role key.
Deployment model: internal app vs SaaS
This template ships configured as an internal app: you build it for a specific business, you control who signs up, and kicked members cannot create their own parallel teams inside your app. The multi-tenancy (teams, roles, RLS) is still fully in place — it just behaves like "one organization, many members" rather than "many organizations, self-serve".
Flipping to SaaS
Flipping the template into a SaaS model (self-serve signup, anyone can create a team, one codebase serving many unrelated businesses) is four small edits. None of the architecture changes — teams remain the tenant boundary, RLS policies remain team-scoped, authUser still enforces roles. You are only relaxing the guardrails that make sense for an internal deployment.
1. Allow deleting your only team — server/api/teams/[teamId]/index.delete.ts
The internal app refuses to delete an owner's last team so they are not stranded on /no-team. In SaaS that is fine because they can just create a new one. Remove the otherTeamCount guard block.
2. Ungate "Create team" in the sidebar — app/components/layout/sidebar/Header.vue
The Create team menu item is gated behind the settings.team permission (owner of the current team), because in an internal app a kicked member spinning up their own shadow team would defeat the point of kicking them. In SaaS, team creation is a user-level action — any authenticated user should be able to create their own workspace. Remove the can("settings.team") conditional so the menu item always renders.
3. Give /no-team a recovery path — app/pages/no-team.vue
The internal app treats /no-team as an unreachable-in-normal-flow dead end and tells the user to contact their admin. In SaaS it is a legitimate state (new signup who has not created a workspace yet, or someone who just left their last team) and needs a "Create a team" button. Replace the sign-out fallback with a primary button opening LayoutCreateTeamModal — the modal already exists and the create flow already refreshes the JWT's has_team claim, so no additional wiring is needed.
4. Decide on public signup
This is the one that is already SaaS-shaped: pages/auth/signup.vue is publicly reachable and anyone with a valid email can create an account. For SaaS, keep it. For a strict internal app where accounts only come into existence via invitation, delete pages/auth/signup.vue and components/auth/SignupForm.vue, remove the "Sign up" link from components/auth/LoginForm.vue, and disable signups in the Supabase dashboard (Authentication → Providers → Email → Disable signups). The /invite/[token] flow handles invite-only account creation independently and will keep working.
What you do not need to change
- RLS policies — already team-scoped, correct for both models.
- Permissions and
authUser— owner/admin/member roles work the same way in SaaS. The difference is just how many teams a user can belong to. - Onboarding auto-creating a first team — fine for both. SaaS users expect to land in a workspace; they can rename it afterward in settings.
- Invite flow, webhooks, avatar storage, team context resolution — all model-agnostic.
What SaaS additionally needs (not provided)
Out of scope for this template, but worth naming so you know what is missing: billing/subscriptions (Stripe + a subscriptions table keyed to team_id), seat/plan enforcement in middleware, per-team usage quotas, email verification enforcement (toggle in Supabase), and public marketing pages. The current app is auth-walled from /, so you would add an unauthenticated landing route as well.