A full-stack multi-tenant notes application built with NestJS and Next.js, featuring role-based access control, tenant isolation via HTTP headers, and JWT authentication.
Watch the complete application walkthrough:
https://github.com/user-attachments/assets/Demo.mp4
The demo shows: admin login, tenant creation, user management, and full notes CRUD operations.
This monorepo contains two applications:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Next.js Frontend (notes-ui) β β
β β - React 19 + TypeScript β β
β β - Tailwind CSS 4 β β
β β - Client-side routing & localStorage auth β β
β β - Port: 3000 β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β HTTP/REST API
β Headers: Authorization, X-Tenant-Id
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β NestJS Backend (multi-tenant-notes) β β
β β Port: 3001 β β
β β β β
β β βββββββββββββββ ββββββββββββββββ ββββββββββββββ β β
β β β Auth β β Admin β β Notes β β β
β β β Module β β Module β β Module β β β
β β βββββββββββββββ ββββββββββββββββ ββββββββββββββ β β
β β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Middleware & Guards β β β
β β β - Tenant Middleware (X-Tenant-Id extraction) β β β
β β β - JWT Auth Guard (authentication) β β β
β β β - Roles Guard (ADMIN/TENANT_USER) β β β
β β β - Tenant Guard (tenant isolation validation) β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β TypeORM
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Persistence Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PostgreSQL Database (Port 5432) β β
β β β β
β β ββββββββββββ ββββββββββββ ββββββββββββ β β
β β β Tenants β β Users β β Notes β β β
β β ββββββββββββ ββββββββββββ ββββββββββββ β β
β β β β β β β
β β βββββββββββββββ΄βββββββββββββββ β β
β β (tenant_id foreign keys) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
This application implements database-level multi-tenancy with row-level isolation using HTTP headers:
- Tenant Identification: Tenants are identified via
X-Tenant-IdHTTP header (NOT subdomain) - Tenant Middleware: Extracts and validates tenant ID from request headers (applied only to Notes module)
- Data Isolation: All queries are automatically filtered by tenant_id
- Shared Database: Single PostgreSQL instance with tenant_id foreign keys
1. Client Request β POST http://localhost:3001/notes
Headers:
- Authorization: Bearer <jwt_token>
- X-Tenant-Id: <tenant_uuid>
2. JWT Guard validates token and extracts user info (userId, role, tenantId)
3. Roles Guard validates user has required role (TENANT_USER for notes)
4. Tenant Middleware extracts tenant ID from X-Tenant-Id header
- Throws BadRequestException if header missing
- Attaches tenantId to request object
5. Tenant Guard validates tenant access:
- Checks user's JWT tenantId matches X-Tenant-Id header
- Validates tenant exists in database
- Throws ForbiddenException if mismatch
6. Notes Service filters all queries by tenant_id
7. Response returned to client
βββββββββββββββ
β Tenant β
βββββββββββββββ€
β id (PK) β UUID
β name β VARCHAR(120) UNIQUE
β created_at β TIMESTAMP
βββββββββββββββ
β²
β
β tenant_id (nullable for ADMIN)
β
βββββββββββββββ
β User β
βββββββββββββββ€
β id (PK) β UUID
β email β VARCHAR(255) UNIQUE
β password_ β VARCHAR(255)
β hash β
β role β ENUM('ADMIN', 'TENANT_USER')
β tenant_id β UUID (NULL for ADMIN)
β created_at β TIMESTAMP
βββββββββββββββ
βββββββββββββββ
β Note β
βββββββββββββββ€
β id (PK) β UUID
β tenant_id β UUID (indexed)
β title β VARCHAR(200)
β content β TEXT
β created_at β TIMESTAMP
β updated_at β TIMESTAMP
βββββββββββββββ
β
β tenant_id
βΌ
[Tenant]Important: Notes do NOT have a user_id field. Notes are scoped only by tenant, not by individual user. All users within a tenant can see all notes for that tenant.
- ADMIN: Full access to tenant/user management via
/adminendpoints. Admin users havetenantId = nulland cannot access notes endpoints - TENANT_USER: Access to notes within their assigned tenant via
/notesendpoints. Must provide X-Tenant-Id header
- User submits login with email/password to
POST /auth/login - Backend validates credentials against hashed password (bcrypt with salt rounds = 10)
- JWT token issued with payload:
{ sub: userId, email, role, tenantId } - Client stores token + tenantId + role + tenantName in localStorage
- Subsequent requests include:
Authorization: Bearer <token>header (always required)X-Tenant-Id: <tenant_uuid>header (required for notes endpoints)
- JWT Strategy validates token and attaches user object to request
- Guards validate role and tenant access before reaching controllers
- Framework: NestJS 11.0.1
- Database: PostgreSQL 16
- ORM: TypeORM 0.3.28
- Authentication: @nestjs/jwt 11.0.2, Passport JWT 4.0.1
- Password Hashing: bcrypt 6.0.0
- Validation: class-validator 0.14.3, class-transformer 0.5.1
- Config: @nestjs/config 4.0.3
- Port: 3001 (configurable via APP_PORT env var)
- CORS: Enabled for http://localhost:3000 (configurable via CORS_ORIGIN env var)
- Framework: Next.js 16.1.6 (App Router)
- UI Library: React 19.2.3
- Styling: Tailwind CSS 4 (postcss-based)
- Language: TypeScript 5
- Port: 3000 (default Next.js dev server)
- State Management: React hooks + localStorage for auth
- API Client: Native fetch with custom helper (apiFetch)
- Containerization: Docker & Docker Compose
- Database (Dev): PostgreSQL 16 (containerized on port 5432)
- Database (Test): PostgreSQL 16 (containerized on port 5433)
Technical Test/
βββ multi-tenant-notes/ # NestJS Backend
β βββ src/
β β βββ main.ts # Bootstrap & CORS configuration
β β βββ app.module.ts # Root module with TypeORM config
β β βββ admin/ # Admin module (tenant & user management)
β β β βββ admin.controller.ts
β β β βββ admin.service.ts
β β β βββ dto/ # CreateTenantDto, CreateUserDto
β β βββ auth/ # Authentication module
β β β βββ auth.controller.ts
β β β βββ auth.service.ts
β β β βββ jwt.strategy.ts
β β β βββ dto/ # LoginDto
β β βββ notes/ # Notes CRUD module
β β β βββ notes.controller.ts
β β β βββ notes.service.ts
β β β βββ notes.module.ts # Applies TenantMiddleware
β β β βββ dto/ # CreateNoteDto, UpdateNoteDto
β β βββ common/
β β β βββ guards/
β β β β βββ jwt-auth-guard.ts # JWT validation
β β β β βββ roles.guard.ts # Role-based authorization
β β β β βββ roles.decorator.ts
β β β βββ decorators/
β β β β βββ tenant-id.decorator.ts # @TenantId() decorator
β β β βββ tenancy/
β β β βββ tenant.middleware.ts # X-Tenant-Id extraction
β β β βββ tenant.guard.ts # Tenant validation
β β β βββ tenant-request.type.ts
β β βββ db/
β β βββ entities/
β β β βββ tenant.entity.ts
β β β βββ user.entity.ts
β β β βββ note.entity.ts
β β βββ migrations/
β β βββ 1770621640822-init.ts
β βββ docker-compose.yml # PostgreSQL setup (dev + test DBs)
β βββ typeorm.config.ts # TypeORM CLI configuration
β βββ package.json
β βββ .env # Environment variables (create this)
β
βββ notes-ui/ # Next.js Frontend
βββ src/
β βββ app/
β β βββ page.tsx # Root page (redirects to login/notes)
β β βββ layout.tsx # Root layout
β β βββ globals.css # Global styles
β β βββ admin/
β β β βββ tenants/
β β β βββ page.tsx # Tenant management
β β β βββ [tenantId]/users/
β β β βββ page.tsx # User management
β β βββ login/
β β β βββ page.tsx # Login page
β β βββ notes/
β β βββ page.tsx # Notes CRUD
β βββ lib/
β βββ auth.ts # Auth helpers & apiFetch
βββ package.json
βββ .env.local # Environment variables (create this)
- Node.js 20+ (backend uses Node.js features)
- Docker & Docker Compose (for PostgreSQL)
- npm (or yarn/pnpm)
-
Install backend dependencies:
cd multi-tenant-notes npm install -
Install frontend dependencies:
cd notes-ui npm install -
Set up environment variables:
Create
.envinmulti-tenant-notes/:# Database Configuration DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres DB_NAME=notes_db DB_TEST_NAME=notes_test_db # JWT Configuration JWT_SECRET=your-super-secret-jwt-key-change-in-production # Application Configuration APP_PORT=3001 CORS_ORIGIN=http://localhost:3000
Create
.env.localinnotes-ui/:NEXT_PUBLIC_API_URL=http://localhost:3001
-
Start PostgreSQL:
cd multi-tenant-notes docker-compose up -dThis starts two PostgreSQL containers:
- Development DB on port 5432
- Test DB on port 5433
-
Run database migrations:
cd multi-tenant-notes npm run migration:run
Start both applications in separate terminals:
Terminal 1 - Backend:
cd multi-tenant-notes
npm run start:dev- Backend runs on
http://localhost:3001 - Auto-seeds admin user:
admin@local.test / admin123 - Watch mode enabled (auto-reload on changes)
Terminal 2 - Frontend:
cd notes-ui
npm run dev- Frontend runs on
http://localhost:3000 - Hot module reload enabled
The application auto-seeds an admin user on startup (via AdminController.onModuleInit()):
- Email:
admin@local.test - Password:
admin123
Use these credentials to:
- Log in to the application
- Create tenants
- Create users for each tenant
Authenticate user and receive JWT token
Request Body:
{
"email": "admin@local.test",
"password": "admin123"
}Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tenantId": "uuid-or-null",
"tenantName": "Tenant Name or null",
"userId": "uuid",
"email": "admin@local.test",
"role": "ADMIN"
}All admin endpoints require Authorization: Bearer <token> header with ADMIN role.
Create a new tenant
Headers: Authorization: Bearer <token>
Request Body:
{
"name": "Acme Corp"
}Response:
{
"id": "uuid",
"name": "Acme Corp",
"createdAt": "2026-02-09T..."
}List all tenants (ordered by createdAt DESC)
Headers: Authorization: Bearer <token>
Response:
[
{
"id": "uuid",
"name": "Acme Corp",
"createdAt": "2026-02-09T..."
}
]Create a user for a specific tenant
Headers: Authorization: Bearer <token>
Request Body:
{
"email": "user@acme.com",
"password": "password123"
}Response:
{
"id": "uuid",
"email": "user@acme.com",
"role": "TENANT_USER",
"tenantId": "tenant-uuid",
"createdAt": "2026-02-09T..."
}List all users for a specific tenant
Headers: Authorization: Bearer <token>
Response:
[
{
"id": "uuid",
"email": "user@acme.com",
"role": "TENANT_USER",
"tenantId": "tenant-uuid",
"createdAt": "2026-02-09T..."
}
]All notes endpoints require:
Authorization: Bearer <token>headerX-Tenant-Id: <tenant-uuid>header
The tenant ID in the header MUST match the user's tenantId from the JWT, or request will fail with 403 Forbidden.
Get all notes for the tenant (ordered by createdAt DESC)
Headers:
Authorization: Bearer <token>
X-Tenant-Id: <tenant-uuid>
Response:
[
{
"id": "uuid",
"tenantId": "tenant-uuid",
"title": "My Note",
"content": "Note content here",
"createdAt": "2026-02-09T...",
"updatedAt": "2026-02-09T..."
}
]Create a new note
Headers:
Authorization: Bearer <token>
X-Tenant-Id: <tenant-uuid>
Content-Type: application/json
Request Body:
{
"title": "My Note",
"content": "Note content here"
}Response:
{
"id": "uuid",
"tenantId": "tenant-uuid",
"title": "My Note",
"content": "Note content here",
"createdAt": "2026-02-09T...",
"updatedAt": "2026-02-09T..."
}Get a specific note (must belong to tenant)
Headers:
Authorization: Bearer <token>
X-Tenant-Id: <tenant-uuid>
Response: Same as note object above, or 404 if not found
Update a note (only title and/or content)
Headers:
Authorization: Bearer <token>
X-Tenant-Id: <tenant-uuid>
Content-Type: application/json
Request Body:
{
"title": "Updated Title",
"content": "Updated content"
}Response: Updated note object
Delete a note
Headers:
Authorization: Bearer <token>
X-Tenant-Id: <tenant-uuid>
Response:
{
"deleted": true
}cd multi-tenant-notes
npm testRuns Jest unit tests for:
- Controllers (auth, admin, notes)
- Services (auth, admin, notes)
cd multi-tenant-notes
npm run test:e2eRuns end-to-end tests using the test database (port 5433).
No tests currently implemented.
cd multi-tenant-notes
npm run migration:generateThis creates a new migration file in src/db/migrations/ based on entity changes.
cd multi-tenant-notes
npm run migration:runApplies pending migrations to the database.
Backend:
cd multi-tenant-notes
npm run build
npm run start:prodFrontend:
cd notes-ui
npm run build
npm run start- Multi-Tenancy: Complete tenant isolation via X-Tenant-Id HTTP header
- Authentication: Secure JWT-based authentication with bcrypt password hashing (10 salt rounds)
- Authorization: Role-based access control (ADMIN, TENANT_USER)
- Auto Admin Seeding: Default admin user created on server startup
- Layered Security: Multiple guards (JWT, Roles, Tenant) ensuring proper access control
- CRUD Operations: Full note management scoped by tenant
- Type Safety: End-to-end TypeScript with strict validation
- Database Migrations: TypeORM migrations for version-controlled schema changes
- DTO Validation: Request payload validation with class-validator
- Testing Infrastructure: Unit and E2E test support
- Start PostgreSQL containers via Docker Compose
- Run TypeORM migrations to create schema
- Start NestJS backend β auto-seeds admin@local.test
- Start Next.js frontend
- Admin logs in with
admin@local.test / admin123 - Frontend redirects to
/ admin/tenants - Admin creates tenants via UI (calls
POST /admin/tenants) - For each tenant, admin creates users (calls
POST /admin/tenants/:id/users) - Admin provides credentials to tenant users
- Tenant user logs in with provided credentials
- Backend returns JWT + tenantId + tenantName
- Frontend stores token, tenantId, role, tenantName in localStorage
- Frontend redirects to
/notes - User creates/views/edits/deletes notes
- Each API request includes:
Authorization: Bearer <token>headerX-Tenant-Id: <uuid>header (extracted from localStorage)
- Backend validates:
- JWT is valid
- User has TENANT_USER role
- X-Tenant-Id matches user's JWT tenantId
- Tenant exists in database
- Notes are filtered by tenant_id automatically
- The
notestable has NOuser_idcolumn - All notes belong to a tenant, not individual users
- All users within the same tenant can see ALL notes for that tenant
- This is a design choice for this implementation
- The
TenantMiddlewareis only applied to theNotesModule - It is NOT applied globally
- Requests to
/notesendpoints withoutX-Tenant-Idheader will fail with 400 Bad Request - Admin endpoints do NOT require this header
- ADMIN role users have
tenantId = nullin the database - Admins cannot access
/notesendpoints (they have no tenantId to match) - Admins can only access
/adminendpoints - Admins manage tenants and users, but don't create/view notes
The TenantGuard performs two validations:
- User-Tenant match: User's JWT
tenantIdmust equalX-Tenant-Idheader - Tenant existence: The tenant ID must exist in the database
If either check fails, a 403 Forbidden exception is thrown.
- Backend CORS is configured in
main.ts - Default: allows requests from
http://localhost:3000 - Configurable via
CORS_ORIGINenvironment variable - Must be updated for production deployments
- Passwords are hashed using bcrypt with 10 salt rounds
- Passwords are never returned in API responses
- JWT tokens do NOT contain passwords
- Despite the name "multi-tenant", this does NOT use subdomain-based routing
- Tenants are identified by UUID passed via HTTP header
- No DNS or subdomain configuration required
UNLICENSED - Private project