Create FasterCreate Faster
Business

Multitenant SaaS

B2B SaaS dashboard with organizations, custom RBAC, and link-based invitations. Ships as a turborepo with a Next.js web app, a Node batch worker, and shared packages.

Presentation

A production-ready B2B SaaS starter. You get a Next.js dashboard with email/password authentication (sign-up, sign-in, forgot/reset password), Better Auth's organization plugin for multi-tenancy, a custom RBAC system with runtime role creation, link-based invitations (no email provider required), full projects CRUD scoped per organization, an account/security/sessions/preferences profile area, and a Node batch worker for background jobs. Ships as a turborepo with shared auth, db, api, and ui packages, a one-command local setup, and an idempotent, faker-powered seed.

Composition

Apps:

Project addons:

Architecture

Turborepo structure

apps/
  web/              # Next.js app
  batch/            # Node worker
packages/
  api/              # tRPC routers
  auth/             # Better Auth config + permissions + types
  db/               # Drizzle schema + client
  ui/               # shadcn components
  config/           # Shared tsconfig
scripts/
  local-setup.ts    # One-command local bootstrap (env, docker, schema, seed)
  seed.ts           # Idempotent, flag-driven database seeder

Web app

src/
├── app/
│   ├── layout.tsx                                    # Root layout with AppProviders + Toaster
│   ├── (auth)/
│   │   ├── login/
│   │   │   ├── page.tsx
│   │   │   └── login-form.tsx                        # Email/password sign-in
│   │   ├── signup/
│   │   │   ├── page.tsx
│   │   │   └── signup-form.tsx                       # Email/password sign-up (honors ?redirect)
│   │   ├── forgot-password/
│   │   │   ├── page.tsx
│   │   │   └── forgot-password-form.tsx              # Request reset link
│   │   ├── reset-password/
│   │   │   ├── page.tsx                              # Reads ?token / ?error
│   │   │   └── reset-password-form.tsx               # New-password form
│   │   └── accept-invitation/[id]/
│   │       ├── page.tsx                              # Accept invitation landing page
│   │       └── accept-button.tsx                     # Client action to accept invite
│   ├── (onboarding)/
│   │   └── onboarding/page.tsx                       # Create first organization
│   └── (dashboard)/
│       ├── layout.tsx                                # Sidebar + auth guard + active org check
│       ├── page.tsx                                  # Dashboard home (project / member counters)
│       ├── projects/
│       │   ├── page.tsx                              # Project list
│       │   └── [id]/
│       │       ├── page.tsx                          # Project detail (server-side prefetch)
│       │       └── project.client.tsx                # Client view + edit dialog
│       ├── settings/
│       │   ├── general/page.tsx                      # Org name + danger zone (delete org)
│       │   ├── members/page.tsx                      # Member list + pending invitations
│       │   └── roles/page.tsx                        # Role list + permissions grid
│       └── profile/
│           ├── layout.tsx                            # Tab navigation + ViewTransition
│           ├── page.tsx                              # Redirect → /profile/account
│           ├── account/page.tsx                      # Account info + edit name
│           ├── security/page.tsx                     # Change password
│           ├── sessions/page.tsx                     # Active sessions + revoke
│           └── preferences/page.tsx                  # Theme switcher
├── components/
│   ├── members/                                      # Members table, invite dialog, invitation link dialog, pending invitations table
│   ├── navigation/                                   # App sidebar, org switcher, nav-user dropdown, sidebar links
│   ├── profile/                                      # Account form, security form, session list, preferences, tab nav
│   ├── projects/                                     # Project table, create/edit/delete dialogs, project form
│   ├── roles/                                        # Roles table, permissions grid, role form dialog
│   ├── can.tsx                                       # Permission gate component (<Can permissions={{ resource: ['action'] }}>)
│   └── query-boundary.tsx                            # Suspense + ErrorBoundary + QueryErrorResetBoundary
├── hooks/
│   └── use-permission.ts                             # usePermission({ resource: ['action'] }) → { allowed, loading }
├── lib/
│   └── constants.ts                                  # ROUTES with per-route { resource, action } permission gating
└── proxy.ts                                          # Next.js middleware (proxy export): auth guard + onboarding redirect

Auth package (packages/auth)

src/
├── auth.ts             # Better Auth config (organization plugin, drizzle adapter)
├── auth-client.ts      # Client with organizationClient + inferAdditionalFields
├── permissions.ts      # RBAC catalog: 5 resources × 15 actions, 3 built-in roles
└── types.ts            # Session type via auth.$Infer.Session

API package (packages/api)

src/
├── root.ts             # appRouter composition (project, organization, member, role, invitation, session, user)
├── middleware/
│   └── rbac.ts         # orgProcedure, permissionProcedure(resource, action), assertInScope
└── router/
    ├── project.ts      # Project CRUD scoped to active org
    ├── organization.ts # Update / delete org + active membership
    ├── member.ts       # List members, change role, remove
    ├── role.ts         # Catalog + dynamic role CRUD via Better Auth org-roles API
    ├── invitation.ts   # Create, list, cancel invitations
    ├── session.ts      # List + revoke sessions
    └── user.ts         # Profile read + update

What's included

Authentication flows

Email/password auth backed by Better Auth, with all four standard flows pre-wired:

  • Sign-in (/login) — honors ?redirect=… and pushes the user to the requested page after login.
  • Sign-up (/signup) — same ?redirect behavior, creates the user via authClient.signUp.email.
  • Forgot password (/forgot-password) — calls authClient.requestPasswordReset and shows a success state without leaking whether the email exists.
  • Reset password (/reset-password?token=…) — reads the token from search params, calls authClient.resetPassword, and routes errors via ?error=….

src/proxy.ts exposes a single proxy export (Next.js middleware) that allow-lists /login, /signup, /forgot-password, /reset-password, /accept-invitation, and /api/auth, and redirects everything else to /login (preserving the original path as ?redirect) or to /onboarding when the user has no organizations.

Multi-tenant organizations

Organizations are first-class entities powered by Better Auth's organization plugin. Each user belongs to one or more organizations with their own members, roles, and projects. After sign-in, unauthenticated users and users with no organization are redirected to /onboarding to create their first org. The dashboard layout reads the active organization from the session and gates all API calls to it.

Custom RBAC

The permission system is defined in packages/auth/src/permissions.ts using Better Auth's createAccessControl with dynamicAccessControl enabled to support runtime role creation:

Resources and actions:

ResourceActions
organizationupdate, delete
memberinvite, update, remove
invitationcreate, cancel
accreate, read, update, delete
projectcreate, read, update, delete

The role-management resource is named ac (access control), not role — Better Auth's dynamic-access-control endpoints (createOrgRole, updateOrgRole, deleteOrgRole) require this internal name when checking permissions on custom org roles.

Built-in roles (immutable defaults):

RolePermissions
ownerFull access to all resources
adminAll member, invitation, role (ac), project actions; cannot delete the organization
memberRead-only project access

Admins can create additional roles with custom permission combinations via the Roles UI. Custom roles are stored in the database and resolved dynamically at runtime.

The <Can> component and usePermission hook provide declarative permission checks in the UI:

<Can permissions={{ project: ['create'] }}>
  <CreateProjectButton />
</Can>
const { allowed, loading } = usePermission({ project: ['create'] });

On the server, tRPC enforces the same model through two composable procedures in packages/api/src/middleware/rbac.ts:

  • orgProcedure — requires session.activeOrganizationId and injects ctx.orgId.
  • permissionProcedure(resource, action) — extends orgProcedure and calls auth.api.hasPermission so custom roles resolve at runtime.
  • assertInScope(row, ctx) — narrows a row's organizationId against ctx.orgId and throws NOT_FOUND on mismatch, preventing cross-org data leaks in router handlers.

No email sending is required. An admin creates an invitation in the Members settings page, copies the generated link, and shares it manually (Slack, email, etc.). The recipient opens /accept-invitation/[id] to join the organization. Pending invitations are listed in a separate table with the option to copy or revoke them.

Org switcher

The sidebar includes an org switcher component that lists all organizations the user belongs to. Switching orgs updates the active organization in the session without a full page reload.

Projects CRUD

Full create, read, update, delete for projects scoped to the active organization. Project form with Zod validation via TanStack Form. Project table with inline actions gated by the project:update and project:delete permissions.

Settings pages

Three settings pages under /settings:

  • General — rename the organization; danger zone with delete organization (owner only)
  • Members — member list with role assignment; pending invitations table with copy-link and revoke actions
  • Roles — role list with a permission grid for creating and editing custom roles (admin and owner)

Profile with View Transitions

Route-based profile tabs using React 19 ViewTransition API with directional slide animations. Four sections:

  • Account — user info (email, role, joined date) + edit display name
  • Security — change password (revokes other sessions)
  • Sessions — list active sessions with device/IP info, revoke individual or all others
  • Preferences — theme switcher (light/dark/system)

QueryBoundary

Reusable component wrapping Suspense + ErrorBoundary + QueryErrorResetBoundary. Used throughout for tRPC prefetch error handling with retry UI.

Local setup

scripts/local-setup.ts is a one-command bootstrap. It copies every .env.example to .env, starts the database with docker compose up -d --wait, applies the schema, then seeds demo data:

bun run local-setup

Each step is labelled and the script exits on the first failure.

Database seed

scripts/seed.ts is idempotent (safe to re-run) and flag-driven via node:util parseArgs:

FlagEffect
--fixturesAdd demo data (extra members, projects, pending invitations) generated with faker
--resetWipe existing data before seeding
--forceAllow --reset against a non-local database
--helpShow usage

Core seeding always provisions, via Better Auth's API and direct Drizzle inserts:

  • Users — owner@example.com and member@example.com, both with password password
  • Organization — Acme Inc. (slug acme) with the owner as owner and the second user as member

With --fixtures, faker adds four extra members, six projects, and three pending invitations scoped to the organization (deterministic via a fixed seed).

The seed runs from the repo root, so db:seed loads apps/web/.env via --env-file to resolve DATABASE_URL and BETTER_AUTH_SECRET. A --reset against a non-local DATABASE_URL is refused unless --force is passed.

Warning: These credentials are for local development only. Change passwords or disable seed accounts before deploying to any shared environment.

What's intentionally excluded

FeatureNotes
Email sendingInvitations use copy-paste links; no SMTP or transactional email provider
Billing / subscriptionsNo Stripe or payment integration
Audit logMember and permission changes are not logged
Sub-organizations / teamsOne organization level only; no nested team hierarchy
WebhooksNo outbound event notifications

Extra dependencies

App (apps/web) and root extras declared by the blueprint:

PackagePurpose
@hugeicons/reactIcon library used throughout the dashboard
@hugeicons/core-free-iconsIcon set for @hugeicons/react
lucide-reactInline icons for navigation and dialogs
react-error-boundaryQueryBoundary error handling
sonnerToast notifications
zodForm validation schemas
@faker-js/fakerDemo data for the seed fixtures (dev only)

Shared UI package (packages/ui) extras:

PackagePurpose
@tanstack/react-formBacks the shuip tanstack-form field components shipped in packages/ui/src/components/ui/shuip/tanstack-form/*

Environment variables

Extra env vars declared by the blueprint (on top of those provided by better-auth, drizzle, and postgres):

VariableScopePurpose
NEXT_PUBLIC_APP_URLapps/web/.env.examplePublic origin used for invitation links and password-reset redirects

Root scripts

ScriptCommandPurpose
local-setupbun scripts/local-setup.tsOne-command local bootstrap (env, docker, schema, seed)
db:pushturbo db:pushPush schema to database
db:generateturbo db:generateGenerate migration files
db:migrateturbo db:migrateApply migrations
db:studioturbo db:studioOpen Drizzle Studio
db:seedbun --env-file=apps/web/.env scripts/seed.tsSeed core data (--fixtures for demo data)
startturbo startStart production server

CLI usage

bunx create-faster myproject \
  --blueprint multitenant-saas \
  --linter biome \
  --git \
  --pm bun

Getting started

After generating the project:

# Install dependencies
bun install

# Copy env files, start the database, apply the schema, and seed demo data
bun run local-setup

# Start the dev server
bun run dev

Login with owner@example.com / password to explore the full feature set, or member@example.com / password for a restricted view.

Agent context

This blueprint ships AGENTS.md + docs/agents/ guides (architecture, auth & RBAC, multi-tenancy, data layer) so AI coding agents understand the project out of the box. See Agent Context.

On this page