Skip to content

Contributing Guide

Welcome to the MMR SaaS Platform! This guide will help you understand our development workflow, coding standards, and best practices.

Table of Contents


Getting Started

1. Setup Your Environment

Follow the LOCAL_SETUP.md guide to install prerequisites and configure your development environment.

Quick setup:

# Clone repository
git clone <repository-url>
cd MMR_SAAS_LAB

# Install dependencies
pnpm install

# Setup environment files
cp apps/backend/.env.example apps/backend/.env
cp apps/web/.env.example apps/web/.env.local

# Start Docker services (MySQL + Redis)
pnpm docker:up

# Run database migrations
pnpm db:migrate

# Start development servers
pnpm dev

2. Understand the Project Structure

MMR_SAAS_LAB/
├── apps/
│   ├── backend/          # NestJS API (Port 3001)
│   └── web/              # Next.js Frontend (Port 3000)
├── packages/
│   ├── types/            # Shared TypeScript types
│   ├── schemas/          # Zod validation schemas
│   ├── api-client/       # Shared API client
│   └── ui/               # Shared UI components
├── .claude/agents/       # AI Agent System (16 agents)
└── docs/                 # Documentation

3. Read Key Documentation


Development Workflow

Daily Development Process

  1. Pull latest changes:

    git checkout develop
    git pull origin develop
    

  2. Create feature branch:

    git checkout -b feature/your-feature-name
    

  3. Make changes and test:

    # Start dev servers
    pnpm dev
    
    # Run tests
    pnpm test
    
    # Check types
    pnpm type-check
    
    # Lint code
    pnpm lint
    

  4. Commit changes:

    git add .
    git commit -m "feat: add new feature description"
    

  5. Push and create PR:

    git push origin feature/your-feature-name
    # Open pull request on GitHub
    

Running Development Servers

# Both frontend and backend
pnpm dev

# Frontend only (port 3000)
pnpm dev:web

# Backend only (port 3001)
pnpm dev:back

Watching for Changes

Both servers support hot reload: - Backend: NestJS watches for TypeScript changes - Frontend: Next.js Fast Refresh updates React components instantly

Building for Production

# Build all apps
pnpm build

# Build specific app
pnpm build:web   # Frontend
pnpm build:back  # Backend

Using the AI Agent System

The platform includes 16 specialized AI agents that automate code generation for common development tasks.

When to Use Agents

Use Frontend Agents (10) When: - Creating new UI components or pages - Building forms with validation - Adding new React hooks - Implementing API client services - Working with TanStack Query

Use Backend Agents (6) When: - Creating new database entities - Building REST API endpoints - Implementing business logic services - Writing DTOs and validation - Creating unit and E2E tests

Frontend Agent Workflow (6 Steps)

Activation Command:

@orchestrate create <module-name> module

Example:

@orchestrate create products module

Output: 12+ production-ready files: 1. Types - TypeScript interfaces (packages/types/src/) 2. Schemas - Zod validation schemas (packages/schemas/src/) 3. Services - API client services (apps/web/src/services/) 4. Hooks - TanStack Query hooks (apps/web/src/hooks/) 5. Components - React components (apps/web/src/components/) 6. Pages - Next.js pages (apps/web/src/app/)

Detailed Workflow:

Step 1: @type-architect       → TypeScript types
Step 2: @schema-engineer       → Zod validation schemas
Step 3: @service-builder       → API services
Step 4: @hook-factory          → TanStack Query hooks
Step 5: @component-builder     → React components
Step 6: @page-generator        → Next.js pages

Backend Agent Workflow (5 Steps)

Activation Command:

@backend-orchestrate create <module-name> module

Example:

@backend-orchestrate create products module

Output: 8+ production-ready files: 1. Entity - TypeORM entity (apps/backend/src/modules/products/) 2. DTOs - Data transfer objects with validation 3. Repository - TenantScopedRepository (multi-tenant safe) 4. Service - Business logic implementation 5. Controller - REST endpoints with Swagger docs 6. Module - NestJS module configuration 7. Tests - Unit and E2E tests

Detailed Workflow:

Step 1: @entity-builder        → TypeORM entity
Step 2: @dto-builder           → DTOs with validation
Step 3: @service-builder       → Business logic service
Step 4: @controller-builder    → REST API controller
Step 5: @test-builder          → Unit & E2E tests

Agent Best Practices

  1. Use descriptive module names: products, invoices, customers
  2. Review generated code: Always review and adjust as needed
  3. Run tests after generation: Ensure generated code passes tests
  4. Follow up with manual adjustments: Customize business logic
  5. Commit agent output: Treat generated code as a starting point

Learn More: .claude/agents/README.md


Coding Standards

TypeScript Conventions

Use Strict Mode:

// tsconfig.json already enforces strict mode
// Always use explicit types, avoid 'any'

// ❌ Bad
function process(data: any) {
  return data.value;
}

// ✅ Good
function process(data: { value: number }): number {
  return data.value;
}

Interface vs Type: - Prefer interface for object shapes - Use type for unions, intersections, utility types

// ✅ Good
interface User {
  id: number;
  email: string;
}

type UserRole = 'admin' | 'staff' | 'client';
type UserWithRole = User & { role: UserRole };

Null Handling:

// Use optional chaining and nullish coalescing
const value = user?.profile?.name ?? 'Unknown';

// Avoid non-null assertions unless absolutely certain
// ❌ Bad: user!.profile!.name
// ✅ Good: Use optional chaining or type guards

NestJS Patterns

Module Structure

Each module should have:

modules/products/
├── products.module.ts        # Module definition
├── products.controller.ts    # REST endpoints
├── products.service.ts       # Business logic
├── products.repository.ts    # Database queries (extends TenantScopedRepository)
├── product.entity.ts         # TypeORM entity (extends BaseEntity)
├── dto/
│   ├── create-product.dto.ts
│   ├── update-product.dto.ts
│   └── product-query.dto.ts
└── __tests__/
    ├── products.service.spec.ts
    └── products.e2e-spec.ts

Controller Pattern

import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
import { Auth } from '../auth/decorators/auth.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { User } from '../users/user.entity';

@Controller('products')
@Auth()  // Require authentication for all routes
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get()
  async findAll(@Query() query: ProductQueryDto) {
    return this.productsService.findAll(query);
  }

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.findOne(id);
  }

  @Post()
  @Auth('admin', 'manager')  // Specific roles
  async create(
    @Body() createDto: CreateProductDto,
    @CurrentUser() user: User,
  ) {
    return this.productsService.create(createDto);
  }

  @Put(':id')
  @Auth('admin', 'manager')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateDto: UpdateProductDto,
  ) {
    return this.productsService.update(id, updateDto);
  }

  @Delete(':id')
  @Auth('admin')  // Admin only
  async remove(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.remove(id);
  }
}

Service Pattern

import { Injectable, NotFoundException } from '@nestjs/common';
import { ProductsRepository } from './products.repository';
import { CreateProductDto, UpdateProductDto, ProductQueryDto } from './dto';
import { Product } from './product.entity';

@Injectable()
export class ProductsService {
  constructor(
    private readonly productsRepository: ProductsRepository,
  ) {}

  async findAll(query: ProductQueryDto): Promise<Product[]> {
    // TenantScopedRepository automatically adds WHERE organizationId = :organizationId
    return this.productsRepository.find({
      where: {
        ...(query.search && { name: Like(`%${query.search}%`) }),
      },
      skip: (query.page - 1) * query.limit,
      take: query.limit,
    });
  }

  async findOne(id: number): Promise<Product> {
    const product = await this.productsRepository.findOne({
      where: { id },
    });

    if (!product) {
      throw new NotFoundException(`Product with ID ${id} not found`);
    }

    return product;
  }

  async create(createDto: CreateProductDto): Promise<Product> {
    const product = this.productsRepository.create(createDto);
    // organizationId is automatically set by TenantContext
    return this.productsRepository.save(product);
  }

  async update(id: number, updateDto: UpdateProductDto): Promise<Product> {
    const product = await this.findOne(id);
    Object.assign(product, updateDto);
    return this.productsRepository.save(product);
  }

  async remove(id: number): Promise<void> {
    const product = await this.findOne(id);
    await this.productsRepository.remove(product);
  }
}

Next.js Patterns

App Router Structure

app/
├── layout.tsx                # Root layout
├── page.tsx                  # Home page (/)
├── (auth)/                   # Route group (no path segment)
│   ├── login/
│   │   └── page.tsx          # /login
│   └── signup/
│       └── page.tsx          # /signup
├── dashboard/
│   ├── layout.tsx            # Dashboard layout
│   ├── page.tsx              # /dashboard
│   └── products/
│       ├── page.tsx          # /dashboard/products (list)
│       ├── create/
│       │   └── page.tsx      # /dashboard/products/create
│       └── [id]/
│           ├── page.tsx      # /dashboard/products/[id] (view)
│           └── edit/
│               └── page.tsx  # /dashboard/products/[id]/edit
└── api/                      # API routes
    └── auth/
        └── [...nextauth]/
            └── route.ts

Server vs Client Components

// Server Component (default, RSC)
// ✅ Use for: Data fetching, accessing backend directly, SEO
export default async function ProductsPage() {
  const products = await fetch('http://localhost:3001/api/products').then(r => r.json());

  return (
    <div>
      <h1>Products</h1>
      <ProductList products={products} />
    </div>
  );
}

// Client Component
// ✅ Use for: Interactivity, state, effects, browser APIs
'use client';

import { useState } from 'react';

export function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  // ... interactive logic
}

Data Fetching with TanStack Query

// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productsApi } from '@/services/products.service';

export function useProducts(query?: ProductQueryParams) {
  return useQuery({
    queryKey: ['products', query],
    queryFn: () => productsApi.getAll(query),
  });
}

export function useCreateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateProductDto) => productsApi.create(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });
}

// Component usage
'use client';

export function ProductsPage() {
  const { data: products, isLoading } = useProducts();
  const createProduct = useCreateProduct();

  if (isLoading) return <Spinner />;

  return <ProductList products={products} />;
}

File Naming Conventions

  • Backend:
  • Entities: product.entity.ts
  • Controllers: products.controller.ts
  • Services: products.service.ts
  • DTOs: create-product.dto.ts
  • Tests: products.service.spec.ts, products.e2e-spec.ts

  • Frontend:

  • Components: ProductList.tsx (PascalCase)
  • Pages: page.tsx (Next.js convention)
  • Hooks: useProducts.ts
  • Services: products.service.ts
  • Types: product.types.ts

Import Organization

Organize imports in this order:

// 1. External dependencies
import { Controller, Get, Post } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';

// 2. Internal modules (absolute imports)
import { Auth } from '@/modules/auth/decorators/auth.decorator';
import { User } from '@/modules/users/user.entity';

// 3. Shared packages
import { CreateProductDto } from '@mmr/types';
import { productSchema } from '@mmr/schemas';

// 4. Relative imports
import { ProductsService } from './products.service';
import { Product } from './product.entity';


Multi-Tenancy Guidelines

Critical: All database operations must be tenant-aware to prevent data leaks.

Always Use TenantScopedRepository

// ✅ CORRECT: Extends TenantScopedRepository
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { TenantScopedRepository } from '../../common/repositories/tenant-scoped.repository';

@Injectable()
export class ProductsRepository extends TenantScopedRepository<Product> {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {
    super(
      productRepository.target,
      productRepository.manager,
      productRepository.queryRunner,
    );
  }
}

// All queries automatically filtered by organizationId:
// - find(), findOne(), findBy()
// - count(), findAndCount()
// - createTenantQueryBuilder()
// ❌ WRONG: Direct Repository usage (data leak risk!)
@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private productRepository: Repository<Product>,  // NO!
  ) {}

  async findAll() {
    // This returns products from ALL organizations!
    return this.productRepository.find();  // DANGEROUS!
  }
}

Tenant Context Requirements

Every request must have a tenant context set:

  1. Subdomain Middleware extracts organization from subdomain
  2. JWT Auth provides organizationId from user token
  3. TenantContextInterceptor sets context for the request
// TenantContext is automatically available in services
import { TenantContext } from '@/common/utils/tenant-context';

async someOperation() {
  const organizationId = TenantContext.getCurrentTenantId();
  const userId = TenantContext.getCurrentUserId();

  // Use for manual queries or logging
  console.log(`User ${userId} from org ${organizationId}`);
}

Security Considerations

Do: - ✅ Always extend TenantScopedRepository for custom repositories - ✅ Use createTenantQueryBuilder() for complex queries - ✅ Test tenant isolation in E2E tests - ✅ Review all raw SQL queries for tenant filtering

Don't: - ❌ Use Repository directly from TypeORM - ❌ Use EntityManager.query() without manual tenant filter - ❌ Create cross-tenant queries unless explicitly admin-only - ❌ Trust client-provided organizationId (always use context)

Learn More: apps/backend/docs/TENANT_ISOLATION.md


Testing

Unit Tests

# Run all unit tests
pnpm test

# Run specific test file
pnpm test products.service.spec

# Watch mode
pnpm test:watch

# Coverage
pnpm test:cov

Unit Test Pattern (Backend)

// products.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ProductsService } from './products.service';
import { ProductsRepository } from './products.repository';
import { Product } from './product.entity';

describe('ProductsService', () => {
  let service: ProductsService;
  let repository: ProductsRepository;

  const mockRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
    remove: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ProductsService,
        {
          provide: ProductsRepository,
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<ProductsService>(ProductsService);
    repository = module.get<ProductsRepository>(ProductsRepository);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('findAll', () => {
    it('should return array of products', async () => {
      const mockProducts = [{ id: 1, name: 'Product 1' }];
      mockRepository.find.mockResolvedValue(mockProducts);

      const result = await service.findAll({});

      expect(result).toEqual(mockProducts);
      expect(repository.find).toHaveBeenCalled();
    });
  });

  describe('findOne', () => {
    it('should return a product', async () => {
      const mockProduct = { id: 1, name: 'Product 1' };
      mockRepository.findOne.mockResolvedValue(mockProduct);

      const result = await service.findOne(1);

      expect(result).toEqual(mockProduct);
    });

    it('should throw NotFoundException when product not found', async () => {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
    });
  });
});

E2E Tests

# Run E2E tests
pnpm test:e2e

# Run specific E2E test
pnpm test:e2e products.e2e-spec

E2E Test Pattern (Backend)

// products.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Products API (e2e)', () => {
  let app: INestApplication;
  let authToken: string;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();

    // Login to get auth token
    const loginResponse = await request(app.getHttpServer())
      .post('/api/auth/login')
      .send({ email: 'admin@example.com', password: 'password' });

    authToken = loginResponse.body.tokens.accessToken;
  });

  afterAll(async () => {
    await app.close();
  });

  describe('GET /api/products', () => {
    it('should return products array', () => {
      return request(app.getHttpServer())
        .get('/api/products')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200)
        .expect((res) => {
          expect(Array.isArray(res.body.data)).toBe(true);
        });
    });

    it('should return 401 without auth token', () => {
      return request(app.getHttpServer())
        .get('/api/products')
        .expect(401);
    });
  });

  describe('POST /api/products', () => {
    it('should create new product', () => {
      return request(app.getHttpServer())
        .post('/api/products')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'New Product',
          price: 99.99,
          sku: 'PROD-001',
        })
        .expect(201)
        .expect((res) => {
          expect(res.body.data).toHaveProperty('id');
          expect(res.body.data.name).toBe('New Product');
        });
    });
  });
});

Multi-Tenant Test Strategy

Always test tenant isolation:

describe('Tenant Isolation', () => {
  it('should only return products from current organization', async () => {
    // Set tenant context for Org 1
    await TenantContext.run(
      { tenantId: '1', userId: '1', roles: [] },
      async () => {
        const products = await productsRepository.find();

        // All products should belong to Org 1
        expect(products.every(p => p.organizationId === 1)).toBe(true);
      },
    );
  });

  it('should prevent cross-tenant access', async () => {
    // User from Org 1 trying to access Org 2's product
    await TenantContext.run(
      { tenantId: '1', userId: '1', roles: [] },
      async () => {
        // Product 999 belongs to Org 2
        const product = await productsRepository.findOne({ where: { id: 999 } });

        // Should return null (not found in this org)
        expect(product).toBeNull();
      },
    );
  });
});

Pull Request Process

1. Create Feature Branch

git checkout -b feature/add-product-module

2. Make Changes and Commit

Follow Conventional Commits:

feat: add product module with CRUD operations
fix: resolve authentication token expiry issue
docs: update API documentation for products
refactor: simplify tenant context logic
test: add E2E tests for product endpoints
chore: update dependencies to latest versions

3. Push to Remote

git push origin feature/add-product-module

4. Open Pull Request

PR Title: Follow conventional commit format

feat: add product module with CRUD operations

PR Description Template:

## Summary
Brief description of changes

## Changes
- Added Product entity with multi-tenant support
- Implemented CRUD endpoints
- Added validation DTOs
- Created unit and E2E tests

## Testing
- [ ] Unit tests pass
- [ ] E2E tests pass
- [ ] Tested manually with Postman
- [ ] Verified tenant isolation

## Checklist
- [ ] Code follows project conventions
- [ ] All tests pass
- [ ] No linting errors
- [ ] Documentation updated
- [ ] Breaking changes documented

## Related Issues
Closes #123

5. Code Review Checklist

For Reviewers: - [ ] Code follows TypeScript/NestJS/Next.js best practices - [ ] Multi-tenant isolation is maintained - [ ] Tests cover main functionality - [ ] No security vulnerabilities (SQL injection, XSS, etc.) - [ ] Performance considerations addressed - [ ] Error handling is comprehensive - [ ] Code is well-documented - [ ] No console.log or debugging code left - [ ] Environment variables are documented

6. Addressing Feedback

# Make requested changes
git add .
git commit -m "fix: address PR feedback - improve error handling"
git push origin feature/add-product-module

7. Merge

After approval, the PR will be merged using Squash and Merge strategy.


Common Development Tasks

Adding a New Backend Module

  1. Generate module structure:

    @backend-orchestrate create products module
    

  2. Review generated files:

  3. Entity (product.entity.ts)
  4. DTOs (create-product.dto.ts, update-product.dto.ts)
  5. Repository (products.repository.ts)
  6. Service (products.service.ts)
  7. Controller (products.controller.ts)
  8. Module (products.module.ts)
  9. Tests (*.spec.ts, *.e2e-spec.ts)

  10. Customize business logic:

  11. Add custom methods to service
  12. Implement relationships in entity
  13. Add custom validation rules

  14. Run tests:

    pnpm test products.service.spec
    pnpm test:e2e products.e2e-spec
    

Adding a New Frontend Page

  1. Generate module:

    @orchestrate create products module
    

  2. Review generated files:

  3. Types (packages/types/src/products/)
  4. Schemas (packages/schemas/src/products/)
  5. Services (apps/web/src/services/products.service.ts)
  6. Hooks (apps/web/src/hooks/useProducts.ts)
  7. Components (apps/web/src/components/products/)
  8. Pages (apps/web/src/app/dashboard/products/)

  9. Customize UI:

  10. Adjust component styling
  11. Add custom form fields
  12. Implement additional features

  13. Test manually:

  14. Navigate to http://localhost:3000/dashboard/products
  15. Test CRUD operations
  16. Verify validation

Creating Database Migrations

# Generate migration from entity changes
pnpm migration:generate AddProductsTable

# Review generated migration in apps/backend/src/database/migrations/

# Run migration
pnpm db:migrate

# Revert if needed
pnpm migration:revert

Manual Migration Example:

// 1234567890123-AddProductsTable.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class AddProductsTable1234567890123 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'products',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'name',
            type: 'varchar',
            length: '255',
          },
          {
            name: 'organization_id',
            type: 'int',
          },
          // ... more columns
        ],
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('products');
  }
}

Adding API Client Methods

// apps/web/src/services/products.service.ts
import { apiClient } from '@mmr/api-client';
import type { Product, CreateProductDto, ProductQueryParams } from '@mmr/types';

export const productsApi = {
  async getAll(params?: ProductQueryParams): Promise<Product[]> {
    const { data } = await apiClient.get('/products', { params });
    return data.data;
  },

  async getOne(id: number): Promise<Product> {
    const { data } = await apiClient.get(`/products/${id}`);
    return data.data;
  },

  async create(dto: CreateProductDto): Promise<Product> {
    const { data } = await apiClient.post('/products', dto);
    return data.data;
  },

  async update(id: number, dto: Partial<CreateProductDto>): Promise<Product> {
    const { data } = await apiClient.put(`/products/${id}`, dto);
    return data.data;
  },

  async delete(id: number): Promise<void> {
    await apiClient.delete(`/products/${id}`);
  },
};

Questions or Issues?


Version: 2.0.0 | Last Updated: 2026-06-20