Skip to content

Clutterscore is the self-service AI janitor that continuously audits, scores, and cleans your entire digital workspace (Google Workspace, Microsoft 365, Slack, Notion, Dropbox, Linear, Jira, Figma, etc.).

Notifications You must be signed in to change notification settings

Oseni03/Clutterscore

Repository files navigation

NoteApp

A multi-tenant SaaS notes application built with Next.js, demonstrating enterprise-grade multi-tenancy, authentication, and subscription management patterns.

Overview

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.


πŸš€ Features

Core Functionality

  • Multi-Tenancy - Strict tenant isolation using shared schema with organizationId filtering
  • 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

SaaS Features

  • 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

πŸ—οΈ Tech Stack

  • 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

πŸš€ Getting Started

Prerequisites

  • Node.js 18+
  • PostgreSQL database
  • npm or yarn

Installation

  1. Clone the repository:
git clone <repository-url>
cd noteapp
  1. Install dependencies:
npm install
  1. Set up environment variables:
cp .env.example .env

Configure 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
  1. Generate Prisma client and push schema:
npx prisma generate
npx prisma db push
  1. Start the development server:
npm run dev

Visit http://localhost:3000 to access the application.


πŸ“‚ Project Structure

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

πŸ” Key Implementation Details

1. Multi-Tenancy Architecture

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])
}

2. Authentication Flow

  1. User logs in via /api/auth/login
  2. JWT token generated and returned
  3. Token validated by middleware on protected routes
  4. User's organization context loaded for all requests

3. Subscription Feature Gating

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

4. Notes API Endpoints

All endpoints enforce tenant isolation:

  • POST /api/notes - Create note
  • GET /api/notes - List all notes (tenant-scoped)
  • GET /api/notes/:id - Get single note
  • PUT /api/notes/:id - Update note
  • DELETE /api/notes/:id - Delete note

🎯 Development Guidelines

Code Organization Pattern

  1. Business Logic: Write all operations in src/server/ functions
  2. API Routes: Keep routes thin - just call server functions
  3. State Updates: Update Zustand stores after successful mutations
  4. Type Safety: Define interfaces in src/types/

Example Workflow

// 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 Commands

# 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 GUI

🌐 API Reference

Health Check

GET /api/health
Response: { "status": "ok" }

Authentication

POST /api/auth/login
Body: { "email": "admin@acme.test", "password": "password" }
Response: { "token": "jwt-token", "user": {...} }

Notes Operations

# 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>" }

🚒 Deployment

Vercel Deployment (Recommended)

  1. Push your code to GitHub
  2. Import project in Vercel
  3. Configure environment variables
  4. Deploy

Environment Variables for Production

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_secret

Database Setup

Ensure your production database is set up:

npx prisma db push

πŸ§ͺ Testing

The 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

πŸ“‹ Features Checklist

  • 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

🀝 Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Follow the code organization patterns in src/server/ for business logic
  4. Ensure all database queries include organizationId filtering
  5. Update Zustand stores after mutations
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

πŸ“„ License

MIT License - see LICENSE file for details


πŸ“ž Support

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

About

Clutterscore is the self-service AI janitor that continuously audits, scores, and cleans your entire digital workspace (Google Workspace, Microsoft 365, Slack, Notion, Dropbox, Linear, Jira, Figma, etc.).

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages