A multi-tenant SaaS notes application built with Next.js, demonstrating enterprise-grade multi-tenancy, authentication, and subscription management patterns.
NoteApp is a production-ready multi-tenant application where multiple organizations (tenants) can securely manage their users and notes with complete data isolation. Built as a Next.js SaaS boilerplate with role-based access control and subscription feature gating.
- Multi-Tenancy - Strict tenant isolation using shared schema with
organizationIdfiltering - Notes Management - Full CRUD operations with tenant-aware access control
- Team Collaboration - User invitations, role management, and permissions
- JWT Authentication - Secure token-based authentication with role-based authorization
- Subscription Tiers
- Free Plan: 3 users, 50 notes limit
- Pro Plan: Unlimited users and notes
- Admin Controls - Invite users and upgrade subscriptions
- Usage Tracking - Monitor notes and user limits per organization
- API Access - RESTful API with tenant isolation
- Framework: Next.js 14+ with App Router
- Language: TypeScript
- Database: PostgreSQL with Prisma ORM
- Authentication: BetterAuth (JWT-based with OAuth support)
- Payments: Polar.sh for subscription management
- Email: Resend
- UI: React + shadcn/ui + Tailwind CSS
- State Management: Zustand
- Forms: React Hook Form + Zod validation
- Deployment: Vercel
- Node.js 18+
- PostgreSQL database
- npm or yarn
- Clone the repository:
git clone <repository-url>
cd noteapp- Install dependencies:
npm install- Set up environment variables:
cp .env.example .envConfigure your .env file:
BETTER_AUTH_SECRET=your_secret_key_here
BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
# Polar.sh Configuration
POLAR_ACCESS_TOKEN=your_polar_access_token_here
POLAR_WEBHOOK_SECRET=your_polar_webhook_secret_here
NEXT_PUBLIC_FREE_PLAN_ID=your_free_plan_id_here
NEXT_PUBLIC_PRO_PLAN_ID=your_pro_plan_id_here
# Email
RESEND_API_KEY=your_resend_api_key_here
# OAuth (Optional)
GOOGLE_CLIENT_ID=your_google_client_id_here
GOOGLE_CLIENT_SECRET=your_google_client_secret_here- Generate Prisma client and push schema:
npx prisma generate
npx prisma db push- Start the development server:
npm run devVisit http://localhost:3000 to access the application.
noteapp/
├── prisma/
│ └── schema.prisma # Database schema
├── public/ # Static assets
├── src/
│ ├── app/
│ │ ├── api/ # API routes (thin wrappers)
│ │ │ ├── accept-invitation/
│ │ │ ├── notes/ # Notes CRUD endpoints
│ │ │ ├── organizations/
│ │ │ └── users/
│ │ ├── dashboard/ # Protected dashboard routes
│ │ │ ├── settings/
│ │ │ └── users/
│ │ ├── login/ # Login page
│ │ ├── signup/ # Signup page
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── emails/ # Email templates
│ │ ├── forms/ # Form components
│ │ ├── settings/ # Settings components
│ │ ├── theme/ # Theme components
│ │ ├── ui/ # shadcn/ui components
│ │ ├── app-sidebar.tsx
│ │ ├── nav-main.tsx
│ │ ├── nav-projects.tsx
│ │ ├── nav-user.tsx
│ │ └── team-switcher.tsx
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utility functions
│ │ ├── auth.ts # BetterAuth configuration
│ │ └── utils.ts # Helper functions
│ ├── server/ # Business logic (single source of truth)
│ │ ├── notes.ts # Notes operations
│ │ ├── organizations.ts # Organization operations
│ │ └── users.ts # User operations
│ ├── types/ # TypeScript types
│ └── zustand/ # State management
│ └── providers/
├── .env.example # Environment variables template
├── components.json # shadcn/ui config
├── eslint.config.mjs
├── middleware.ts # Next.js middleware
├── next.config.ts
├── next-env.d.ts
├── package.json
└── tsconfig.json
Approach: Shared database, shared schema with tenant isolation via organizationId
All database queries are scoped to the authenticated user's organization:
// Example: Tenant-isolated query
const notes = await prisma.note.findMany({
where: {
organizationId: activeOrganization.id,
userId: user.id,
},
});Schema Example:
model Note {
id String @id @default(cuid())
organizationId String
userId String
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id])
user User @relation(fields: [userId], references: [id])
}- User logs in via
/api/auth/login - JWT token generated and returned
- Token validated by middleware on protected routes
- User's organization context loaded for all requests
Free Plan Limits:
- 3 users per organization
- 50 notes per organization
Pro Plan:
- Unlimited users and notes
- Only accessible to Admin users for subscription management
All endpoints enforce tenant isolation:
POST /api/notes- Create noteGET /api/notes- List all notes (tenant-scoped)GET /api/notes/:id- Get single notePUT /api/notes/:id- Update noteDELETE /api/notes/:id- Delete note
- Business Logic: Write all operations in
src/server/functions - API Routes: Keep routes thin - just call server functions
- State Updates: Update Zustand stores after successful mutations
- Type Safety: Define interfaces in
src/types/
// 1. Define server function (src/server/notes.ts)
export async function createNote(data: CreateNoteInput) {
// Business logic here
}
// 2. Call from API route (src/app/api/notes/route.ts)
export async function POST(request: Request) {
const result = await createNote(data);
return NextResponse.json(result);
}
// 3. Update state in component
const addNote = async (data) => {
const note = await fetch("/api/notes", {
method: "POST",
body: JSON.stringify(data),
});
notesStore.addNote(note); // Update Zustand
};# Development
npm run dev # Start dev server
npm run lint # Run ESLint
npm run build # Production build
# Database
npx prisma generate # Generate Prisma client
npx prisma db push # Push schema changes
npx prisma studio # Open database GUIGET /api/health
Response: { "status": "ok" }POST /api/auth/login
Body: { "email": "admin@acme.test", "password": "password" }
Response: { "token": "jwt-token", "user": {...} }# Create Note
POST /api/notes
Headers: { "Authorization": "Bearer <token>" }
Body: { "title": "My Note", "content": "Note content" }
# List Notes
GET /api/notes
Headers: { "Authorization": "Bearer <token>" }
# Update Note
PUT /api/notes/:id
Headers: { "Authorization": "Bearer <token>" }
Body: { "title": "Updated Title", "content": "Updated content" }
# Delete Note
DELETE /api/notes/:id
Headers: { "Authorization": "Bearer <token>" }- Push your code to GitHub
- Import project in Vercel
- Configure environment variables
- Deploy
BETTER_AUTH_SECRET=your_production_secret_here
BETTER_AUTH_URL=https://yourdomain.com
NEXT_PUBLIC_APP_URL=https://yourdomain.com
DATABASE_URL="postgresql://..."
POLAR_ACCESS_TOKEN=your_polar_access_token
POLAR_WEBHOOK_SECRET=your_polar_webhook_secret
NEXT_PUBLIC_FREE_PLAN_ID=your_free_plan_id
NEXT_PUBLIC_PRO_PLAN_ID=your_pro_plan_id
RESEND_API_KEY=your_resend_api_key
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secretEnsure your production database is set up:
npx prisma db pushThe application includes comprehensive validation coverage:
Automated Test Coverage:
- ✅ Health endpoint availability
- ✅ Authentication flow
- ✅ Tenant isolation enforcement
- ✅ Role-based access restrictions
- ✅ Subscription limits and upgrades
- ✅ CRUD operations
- ✅ Frontend accessibility
- Multi-tenant architecture with data isolation
- JWT-based authentication
- Role-based authorization (Admin/Member)
- Free and Pro subscription tiers
- Notes CRUD with tenant scoping
- User invitation system
- Subscription upgrade endpoint
- Usage limits enforcement
- Responsive frontend UI
- API health monitoring
- Production deployment on Vercel
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Follow the code organization patterns in
src/server/for business logic - Ensure all database queries include
organizationIdfiltering - Update Zustand stores after mutations
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE file for details
For issues and questions:
- Open an issue on GitHub
- Check the implementation details above
- Review the code examples in
src/server/
Built with Next.js as a SaaS boilerplate demonstrating multi-tenant architecture patterns