Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Multi-Tenant Notes Application

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.

οΏ½ Demo

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.

οΏ½πŸ— Architecture Overview

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)                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ” Multi-Tenancy Architecture

Tenant Isolation Strategy

This application implements database-level multi-tenancy with row-level isolation using HTTP headers:

  1. Tenant Identification: Tenants are identified via X-Tenant-Id HTTP header (NOT subdomain)
  2. Tenant Middleware: Extracts and validates tenant ID from request headers (applied only to Notes module)
  3. Data Isolation: All queries are automatically filtered by tenant_id
  4. Shared Database: Single PostgreSQL instance with tenant_id foreign keys

Request Flow

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

Data Model

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   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.

πŸ›‘ Security Model

Role-Based Access Control (RBAC)

  • ADMIN: Full access to tenant/user management via /admin endpoints. Admin users have tenantId = null and cannot access notes endpoints
  • TENANT_USER: Access to notes within their assigned tenant via /notes endpoints. Must provide X-Tenant-Id header

Authentication Flow

  1. User submits login with email/password to POST /auth/login
  2. Backend validates credentials against hashed password (bcrypt with salt rounds = 10)
  3. JWT token issued with payload: { sub: userId, email, role, tenantId }
  4. Client stores token + tenantId + role + tenantName in localStorage
  5. Subsequent requests include:
    • Authorization: Bearer <token> header (always required)
    • X-Tenant-Id: <tenant_uuid> header (required for notes endpoints)
  6. JWT Strategy validates token and attaches user object to request
  7. Guards validate role and tenant access before reaching controllers

πŸš€ Tech Stack

Backend (multi-tenant-notes)

  • 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)

Frontend (notes-ui)

  • 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)

Infrastructure

  • Containerization: Docker & Docker Compose
  • Database (Dev): PostgreSQL 16 (containerized on port 5432)
  • Database (Test): PostgreSQL 16 (containerized on port 5433)

πŸ“ Project Structure

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)

πŸƒ Getting Started

Prerequisites

  • Node.js 20+ (backend uses Node.js features)
  • Docker & Docker Compose (for PostgreSQL)
  • npm (or yarn/pnpm)

Installation

  1. Install backend dependencies:

    cd multi-tenant-notes
    npm install
  2. Install frontend dependencies:

    cd notes-ui
    npm install
  3. Set up environment variables:

    Create .env in multi-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.local in notes-ui/:

    NEXT_PUBLIC_API_URL=http://localhost:3001
  4. Start PostgreSQL:

    cd multi-tenant-notes
    docker-compose up -d

    This starts two PostgreSQL containers:

    • Development DB on port 5432
    • Test DB on port 5433
  5. Run database migrations:

    cd multi-tenant-notes
    npm run migration:run

Running the Application

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

πŸ”‘ Default Admin Credentials

The application auto-seeds an admin user on startup (via AdminController.onModuleInit()):

  • Email: admin@local.test
  • Password: admin123

Use these credentials to:

  1. Log in to the application
  2. Create tenants
  3. Create users for each tenant

πŸ“‹ API Endpoints

Authentication (No auth required)

POST /auth/login

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

Admin Endpoints (Requires: JWT + Role=ADMIN)

All admin endpoints require Authorization: Bearer <token> header with ADMIN role.

POST /admin/tenants

Create a new tenant

Headers: Authorization: Bearer <token>

Request Body:

{
  "name": "Acme Corp"
}

Response:

{
  "id": "uuid",
  "name": "Acme Corp",
  "createdAt": "2026-02-09T..."
}

GET /admin/tenants

List all tenants (ordered by createdAt DESC)

Headers: Authorization: Bearer <token>

Response:

[
  {
    "id": "uuid",
    "name": "Acme Corp",
    "createdAt": "2026-02-09T..."
  }
]

POST /admin/tenants/:tenantId/users

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

GET /admin/tenants/:tenantId/users

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..."
  }
]

Notes Endpoints (Requires: JWT + Role=TENANT_USER + X-Tenant-Id header)

All notes endpoints require:

  • Authorization: Bearer <token> header
  • X-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 /notes

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..."
  }
]

POST /notes

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 /notes/:id

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

PATCH /notes/:id

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 /notes/:id

Delete a note

Headers:

Authorization: Bearer <token>
X-Tenant-Id: <tenant-uuid>

Response:

{
  "deleted": true
}

πŸ§ͺ Testing

Backend Unit Tests

cd multi-tenant-notes
npm test

Runs Jest unit tests for:

  • Controllers (auth, admin, notes)
  • Services (auth, admin, notes)

Backend E2E Tests

cd multi-tenant-notes
npm run test:e2e

Runs end-to-end tests using the test database (port 5433).

Frontend

No tests currently implemented.

πŸ”§ Development

Generate Migration

cd multi-tenant-notes
npm run migration:generate

This creates a new migration file in src/db/migrations/ based on entity changes.

Run Migrations

cd multi-tenant-notes
npm run migration:run

Applies pending migrations to the database.

Build for Production

Backend:

cd multi-tenant-notes
npm run build
npm run start:prod

Frontend:

cd notes-ui
npm run build
npm run start

🎯 Key Features

  1. Multi-Tenancy: Complete tenant isolation via X-Tenant-Id HTTP header
  2. Authentication: Secure JWT-based authentication with bcrypt password hashing (10 salt rounds)
  3. Authorization: Role-based access control (ADMIN, TENANT_USER)
  4. Auto Admin Seeding: Default admin user created on server startup
  5. Layered Security: Multiple guards (JWT, Roles, Tenant) ensuring proper access control
  6. CRUD Operations: Full note management scoped by tenant
  7. Type Safety: End-to-end TypeScript with strict validation
  8. Database Migrations: TypeORM migrations for version-controlled schema changes
  9. DTO Validation: Request payload validation with class-validator
  10. Testing Infrastructure: Unit and E2E test support

πŸ”„ Application Workflow

Initial Setup Flow

  1. Start PostgreSQL containers via Docker Compose
  2. Run TypeORM migrations to create schema
  3. Start NestJS backend β†’ auto-seeds admin@local.test
  4. Start Next.js frontend

Admin Workflow

  1. Admin logs in with admin@local.test / admin123
  2. Frontend redirects to / admin/tenants
  3. Admin creates tenants via UI (calls POST /admin/tenants)
  4. For each tenant, admin creates users (calls POST /admin/tenants/:id/users)
  5. Admin provides credentials to tenant users

Tenant User Workflow

  1. Tenant user logs in with provided credentials
  2. Backend returns JWT + tenantId + tenantName
  3. Frontend stores token, tenantId, role, tenantName in localStorage
  4. Frontend redirects to /notes
  5. User creates/views/edits/deletes notes
  6. Each API request includes:
    • Authorization: Bearer <token> header
    • X-Tenant-Id: <uuid> header (extracted from localStorage)
  7. Backend validates:
    • JWT is valid
    • User has TENANT_USER role
    • X-Tenant-Id matches user's JWT tenantId
    • Tenant exists in database
  8. Notes are filtered by tenant_id automatically

🚨 Important Implementation Notes

1. Notes are Tenant-Scoped, NOT User-Scoped

  • The notes table has NO user_id column
  • 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

2. X-Tenant-Id Header is Required for Notes Endpoints

  • The TenantMiddleware is only applied to the NotesModule
  • It is NOT applied globally
  • Requests to /notes endpoints without X-Tenant-Id header will fail with 400 Bad Request
  • Admin endpoints do NOT require this header

3. Admin Users Have No Tenant

  • ADMIN role users have tenantId = null in the database
  • Admins cannot access /notes endpoints (they have no tenantId to match)
  • Admins can only access /admin endpoints
  • Admins manage tenants and users, but don't create/view notes

4. Tenant Validation is Strict

The TenantGuard performs two validations:

  1. User-Tenant match: User's JWT tenantId must equal X-Tenant-Id header
  2. Tenant existence: The tenant ID must exist in the database

If either check fails, a 403 Forbidden exception is thrown.

5. CORS Configuration

  • Backend CORS is configured in main.ts
  • Default: allows requests from http://localhost:3000
  • Configurable via CORS_ORIGIN environment variable
  • Must be updated for production deployments

6. Password Security

  • Passwords are hashed using bcrypt with 10 salt rounds
  • Passwords are never returned in API responses
  • JWT tokens do NOT contain passwords

7. No Subdomain Routing

  • 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

πŸ“ License

UNLICENSED - Private project

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages