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