A production-ready React SPA with Better Auth, RBAC, Admin Panel, and Organization Management.
- Features
- Quick Start
- Project Structure
- Unified Role Model
- Admin Panel
- Authentication
- Unit Testing
- E2E Testing
- Development
- Companion Backend
| Category | Features |
|---|---|
| Authentication | Login, signup, email verification, password reset |
| Authorization | Unified 3-role model (Admin, Manager, Member), role-based navigation |
| Admin Panel | Users, Sessions, Organizations, Roles & Permissions management |
| Organizations | Create orgs, invite members, manage roles, impersonation |
| UI | Tailwind CSS, shadcn/ui, responsive sidebar, dark mode |
| Testing | 327 Vitest unit tests (≥70% coverage) + 123 Playwright E2E tests |
- Node.js >= 20.x
- npm >= 10.x
- Backend API — nestjs-api-starter running on port 3000
cd ../nestjs-api-starter
npm install
npm run start:devcd spa-api-starter
npm install
npm run devUse the test admin account: delivered+e2e-test-user@resend.dev / password123
src/
├── app/ # Application layer
│ └── views/
│ ├── AppRoutes.tsx # Route configuration
│ └── RootLayout.tsx # Layout with sidebar
│
├── features/ # Feature modules
│ ├── Admin/ # Admin panel
│ │ ├── views/
│ │ │ ├── UsersPage.tsx
│ │ │ ├── SessionsPage.tsx
│ │ │ ├── OrganizationsPage.tsx
│ │ │ └── RolesPage.tsx
│ │ ├── services/ # API services
│ │ └── hooks/ # React Query hooks
│ │
│ ├── Auth/ # Authentication
│ │ └── views/
│ │ ├── LoginPage.tsx
│ │ ├── SignupPage.tsx
│ │ ├── ForgotPasswordPage.tsx
│ │ └── VerifyEmailPage.tsx
│ │
│ └── Dashboard/ # Dashboard
│
├── shared/ # Shared code
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── AdminRoute.tsx # Admin route guard
│ │ └── ProtectedRoute.tsx # Auth route guard
│ ├── context/
│ │ └── AuthContext.tsx # Auth provider
│ ├── hooks/
│ │ ├── useOrgRole.ts # Organization role hook
│ │ └── useIsImpersonating.ts
│ └── lib/
│ └── auth-client.ts # Better Auth client
│
└── e2e/ # Playwright tests
├── auth.spec.ts
├── admin.spec.ts
├── rbac-unified-roles.spec.ts
└── full-coverage.spec.ts
The frontend enforces the same 3-role model as the backend:
| Role | Access | Navigation |
|---|---|---|
| Admin | Full platform access | Users, Sessions, Organizations, Roles & Permissions |
| Manager | Organization-scoped | Dashboard, Invitations (no admin panel) |
| Member | Basic read access | Dashboard, Invitations (no admin panel) |
The sidebar automatically shows/hides items based on user role:
// Admin sees:
- Dashboard
- My Invitations
- Admin
- Users
- Sessions
- Organizations
- Roles & Permissions
// Manager/Member sees:
- Dashboard
- My InvitationsAdminRoute - Only allows admin role:
<Route path="admin/users" element={<AdminRoute><UsersPage /></AdminRoute>} />ProtectedRoute - Requires authentication:
<Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />import { useAuth } from "@shared/context/AuthContext";
function MyComponent() {
const { user, isAdmin } = useAuth();
if (isAdmin) {
// Show admin features
}
// user.role is 'admin' | 'manager' | 'member'
}| Route | Page | Features |
|---|---|---|
/admin/users |
Users | List, create, edit, ban/unban, change role, impersonate |
/admin/sessions |
Sessions | View sessions, revoke single/all |
/admin/organizations |
Organizations | Create, edit, delete, manage members, invite |
/admin/roles |
Roles & Permissions | View roles, manage permissions |
- Server-side paginated table with search
- Actions: Edit, Ban/Unban, Change Role, Impersonate, Delete
- Create new users with role assignment
- List all organizations
- Create/Edit/Delete organizations
- Manage members (add, remove, change role)
- Send invitations
- Cancel pending invitations
- View all roles (Admin, Manager, Member)
- See permissions assigned to each role
- Manage permission assignments
- Create custom roles
- Vitest — fast unit test runner (jsdom environment)
- React Testing Library — component rendering and interaction
- @testing-library/user-event — realistic user interactions
- v8 coverage — statement, branch, function, and line coverage
# Run all unit tests (with coverage report)
npm test
# Watch mode (re-runs on file changes)
npm run test:watch
# Run a specific file
npm test -- --run src/features/Auth/views/__tests__/LoginPage.test.tsxAll files must meet ≥ 70% for statements, branches, functions, and lines.
Statements : 96% Branches : 89% Functions : 92% Lines : 98%
src/
├── features/
│ └── Auth/
│ ├── schemas/
│ │ └── authSchemas.ts # Zod validation schemas
│ └── views/__tests__/
│ └── LoginPage.test.tsx
├── shared/
│ ├── components/__tests__/
│ │ └── OrganizationSwitcher.test.tsx
│ └── components/ui/__tests__/
│ ├── button.test.tsx
│ ├── dialog.test.tsx
│ ├── field.test.tsx
│ └── theme-toggle.test.tsx
└── features/Admin/services/__tests__/
├── adminService.users.test.ts
├── adminService.impersonation.test.ts
├── adminService.organizationService.test.ts
└── rbacService.crud.test.ts
Forms use react-hook-form with Zod schemas via @hookform/resolvers:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema } from "@/features/Auth/schemas/authSchemas";
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(loginSchema),
});| Route | Description |
|---|---|
/login |
Login page |
/signup |
Registration |
/forgot-password |
Request password reset |
/set-new-password |
Reset password with token |
/verify-email |
Email verification |
import { useAuth } from "@shared/context/AuthContext";
function MyComponent() {
const {
user, // Current user
isAuthenticated, // Boolean
isAdmin, // Boolean
isLoading, // Loading state
login, // (email, password) => Promise
logout, // () => Promise
} = useAuth();
}import { authClient } from "@shared/lib/auth-client";
// Direct API calls
await authClient.signIn.email({ email, password });
await authClient.signUp.email({ email, password, name });
await authClient.signOut();| File | Tests | Coverage |
|---|---|---|
auth.spec.ts |
17 | Authentication flows |
admin.spec.ts |
24 | Admin panel navigation |
rbac-unified-roles.spec.ts |
36 | Role-based access control |
full-coverage.spec.ts |
36 | CRUD operations |
rbac-impersonation.spec.ts |
10 | Impersonation UI |
# All tests (headless)
npm run test:e2e
# List all discovered tests
npm run test:e2e:list
# Watch tests run (headed)
npm run test:e2e:headed
# Interactive UI
npm run test:e2e:ui
# View HTML report manually (tests no longer auto-open/stick)
npm run test:e2e:report# Auth section
npm run test:e2e:auth
# Admin UI section
npm run test:e2e:admin
# RBAC + impersonation section
npm run test:e2e:rbac
# CRUD-heavy admin flows
npm run test:e2e:full-crud
# API-focused E2E checks
npm run test:e2e:apiUse isolated ports and database so Playwright does not fight your local dev session:
# Full isolated suite
npm run test:e2e:isolate
# Isolated auth-only
npm run test:e2e:isolate:auth
# Isolated admin-only
npm run test:e2e:isolate:adminIsolated mode uses:
- API:
http://127.0.0.1:3100 - Frontend:
http://127.0.0.1:4173 - DB:
postgresql://mravinale@localhost:5432/nestjs_api_starter_e2e
You can override these per run with:
E2E_API_BASE_URLE2E_FE_URLE2E_DATABASE_URLE2E_TEST_USER_EMAILE2E_TEST_USER_PASSWORD
Role-Based Access:
- ✅ Admin can access all admin pages
- ✅ Manager cannot access admin pages
- ✅ Member cannot access admin pages
- ✅ Direct URL access is blocked for non-admins
CRUD Operations:
- ✅ Create/Edit/Delete users
- ✅ Ban/Unban users
- ✅ Create/Edit/Delete organizations
- ✅ Add/Remove organization members
- ✅ Create/Delete roles
- ✅ Manage permissions
API Protection:
- ✅ Unauthenticated requests rejected (401/403)
npm run dev # Start dev server
npm run build # Build for production
npm run preview # Preview production build
npm run lint # ESLint
npm test # Run Vitest unit tests with coverage
npm run test:e2e # Run Playwright E2E tests- Create page in
src/features/Admin/views/ - Add service in
src/features/Admin/services/ - Add hooks in
src/features/Admin/hooks/ - Add route in
src/app/views/AppRoutes.tsxwithAdminRoute - Add navigation item in sidebar
// In AppRoutes.tsx
<Route
path="my-feature"
element={
<ProtectedRoute>
<MyFeaturePage />
</ProtectedRoute>
}
/>This SPA works with nestjs-api-starter:
# Terminal 1: Backend (port 3000)
cd nestjs-api-starter
npm run start:dev
# Terminal 2: Frontend (port 5173)
cd spa-api-starter
npm run devDefault API URL: http://localhost:3000
To customize, create .env:
VITE_API_URL=http://localhost:3000| Technology | Version | Purpose |
|---|---|---|
| React | 19.x | UI framework |
| TypeScript | 5.x | Type safety |
| Vite | 7.x | Build tool |
| Better Auth | 1.4.x | Auth client |
| React Router | 7.x | Routing |
| TanStack Query | 5.x | Server state |
| Tailwind CSS | 4.x | Styling |
| shadcn/ui | - | UI components |
| react-hook-form | 7.x | Form state management |
| Zod | 4.x | Schema validation |
| Vitest | 3.x | Unit testing |
| Playwright | 1.x | E2E testing |
MIT