### Clone and Install Dependencies Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/README.md Clone the repository and install project dependencies using npm. Ensure you have Node.js 24+ installed. ```bash git clone https://github.com/YOUR_USERNAME/Multi-Tenant-SaaS-Starter-NestJS.git cd Multi-Tenant-SaaS-Starter-NestJS npm install ``` -------------------------------- ### Install Dependencies Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Install project dependencies using npm. Ensure you have Node.js 18+, PostgreSQL 14+, and Redis 6+ installed. ```bash npm install ``` -------------------------------- ### Start Development Server Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Start the NestJS development server. The API will be available at http://localhost:3000/api and Swagger Docs at http://localhost:3000/docs. ```bash npm run start:dev ``` -------------------------------- ### Tenant-Scoped and Global Caching with CacheService Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Demonstrates using CacheService for tenant-specific data with automatic namespacing and global caching. Includes examples for setting, getting, and invalidating cache entries, as well as rate limiting. ```typescript // src/cache/cache.service.ts @Injectable() export class DashboardService { constructor(private readonly cache: CacheService) {} async getDashboard(tenantId: string) { // Tenant-scoped cache const cached = await this.cache.getTenantCache(tenantId, 'dashboard:main'); if (cached) return cached; const data = await this.computeExpensiveDashboard(tenantId); await this.cache.setTenantCache(tenantId, 'dashboard:main', data, 120); // 2 min TTL return data; } async invalidateDashboard(tenantId: string) { await this.cache.invalidateTenantCache(tenantId, 'dashboard:main'); // Or nuke everything for a tenant: await this.cache.invalidateAllTenantCache(tenantId); } // Global (non-tenant) cache async getExchangeRates() { const rates = await this.cache.getGlobalCache('exchange_rates'); if (!rates) { const fresh = await fetchRates(); await this.cache.setGlobalCache('exchange_rates', fresh, 3600); // 1 hr return fresh; } return rates; } // Rate limiting (sliding window) async checkLimit(userId: string) { const result = await this.cache.checkRateLimit(`user:${userId}:api`, 100, 60); // { allowed: true, remaining: 97, resetIn: 45 } if (!result.allowed) throw new TooManyRequestsException(); return result; } } ``` -------------------------------- ### Set Up Environment Variables Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Copy the example environment file and configure it with your local database credentials. ```bash cp .env.example .env ``` -------------------------------- ### Get Billing Overview with GET /api/billing Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Retrieves billing information including plan, status, and current usage against limits. Requires `billing.read` permission. ```bash curl -s http://localhost:3000/api/billing \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # { # "plan": "FREE", # "status": "ACTIVE", # "usage": { # "members": { "current": 2, "limit": 3 } # } # } ``` -------------------------------- ### Setup Database with Prisma Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/README.md Initialize and seed the database using Prisma. This command applies migrations and populates the database with initial data. ```bash npx prisma migrate dev npx prisma db seed ``` -------------------------------- ### Run with Docker Compose Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/README.md Alternatively, start the application and its dependencies using Docker Compose. This is useful for a consistent development environment. ```bash docker-compose up -d ``` -------------------------------- ### Get Billing Plan Limits with GET /api/billing/limits Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Fetches the hard limits for the tenant's current plan. Requires `billing.read` permission. ```bash curl -s http://localhost:3000/api/billing/limits \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # FREE plan: # { "members": 3, "projects": 5, "apiCalls": 1000, "storage": 100 } # PRO plan: # { "members": 25, "projects": 100, "apiCalls": 50000, "storage": 10000 } ``` -------------------------------- ### Configure Environment Variables Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/README.md Copy the example environment file and update it with your specific database credentials and secrets. This is crucial for connecting to your database and other services. ```bash cp .env.example .env # Edit .env with your database URL and secrets ``` -------------------------------- ### Environment Configuration Example (.env) Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt This is a minimum required environment configuration for local development. Ensure sensitive values like JWT secrets are changed for production. ```bash # .env (minimum required for local development) DATABASE_URL=postgresql://postgres:postgres@localhost:5432/saas # JWT JWT_ACCESS_SECRET=change-me-in-production JWT_REFRESH_SECRET=change-me-in-production JWT_ACCESS_EXPIRES_IN=15m JWT_REFRESH_EXPIRES_IN=7d # Server PORT=3000 NODE_ENV=development # Redis REDIS_HOST=localhost REDIS_PORT=6379 # Providers (mock/console are zero-config for local dev) BILLING_PROVIDER=mock # stripe | paddle | mock EMAIL_PROVIDER=console # smtp | resend | console LOG_PROVIDER=pino # console | pino | winston | loki LOG_LEVEL=info LOG_PRETTY=true # Tracing (Tempo via OTLP) TRACING_ENABLED=true OTEL_SERVICE_NAME=saas-api OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318 # Rate Limiting RATE_LIMIT_TTL=60 RATE_LIMIT_LIMIT=100 # Stripe (only when BILLING_PROVIDER=stripe) STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PRICE_PRO_MONTHLY=price_... # Resend (only when EMAIL_PROVIDER=resend) RESEND_API_KEY=re_... RESEND_FROM=onboarding@resend.dev # SMTP (only when EMAIL_PROVIDER=smtp) SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_USER= SMTP_PASS= SMTP_FROM=noreply@example.com # Frontend base URL (used in email links) FRONTEND_URL=http://localhost:3001 ``` -------------------------------- ### Conventional Commit Examples Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Examples of commit messages following the Conventional Commits format for various types like feat, fix, docs, and test. ```markdown feat(auth): add password reset functionality fix(billing): correct subscription renewal date calculation docs(readme): add deployment instructions test(tenants): add e2e tests for tenant creation ``` -------------------------------- ### Fetch Tenant by ID or Slug - GET /api/tenants/:id and GET /api/tenants/slug/:slug Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Fetches a tenant by its UUID or human-readable slug. Useful for resolving tenants during onboarding flows. Returns a 404 if the tenant is not found. ```bash curl -s http://localhost:3000/api/tenants/tenant-uuid \ -H "Authorization: Bearer $ACCESS_TOKEN" # { "id": "tenant-uuid", "name": "Acme Corp", "slug": "acme-corp", ... } curl -s http://localhost:3000/api/tenants/slug/acme-corp \ -H "Authorization: Bearer $ACCESS_TOKEN" # { "id": "tenant-uuid", "name": "Acme Corp", "slug": "acme-corp", ... } # Not found → 404 # { "statusCode": 404, "message": "Tenant not found" } ``` -------------------------------- ### Billing - GET /api/billing Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns billing overview including plan, status, and current usage vs. limits. Requires `billing.read` permission. ```APIDOC ## GET /api/billing ### Description Retrieves an overview of the billing status for the current tenant, including their plan, account status, and resource usage against defined limits. ### Method GET ### Endpoint /api/billing ### Parameters #### Query Parameters None #### Request Body None ### Response #### Success Response (200) - **plan** (string) - The current subscription plan (e.g., "FREE", "PRO"). - **status** (string) - The current status of the billing account (e.g., "ACTIVE"). - **usage** (object) - An object detailing current resource usage against limits. - **members** (object) - **current** (number) - The current number of members. - **limit** (number) - The maximum allowed number of members. #### Response Example { "plan": "FREE", "status": "ACTIVE", "usage": { "members": { "current": 2, "limit": 3 } } } ``` -------------------------------- ### Get Paginated Audit Logs with GET /api/audit Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Retrieves paginated audit logs for the current tenant. Requires `audit.read` permission (OWNER or ADMIN). ```bash curl -s "http://localhost:3000/api/audit?skip=0&take=20" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # [ # { # "id": "...", "action": "user.invited", "entity": "Membership", # "entityId": "mem-uuid", "metadata": { "email": "bob@example.com" }, # "tenantId": "tenant-uuid", "userId": "alice-uuid", # "user": { "id": "...", "email": "alice@example.com" }, # "createdAt": "2026-01-27T12:00:00.000Z" # } # ] ``` -------------------------------- ### NestJS File Structure Example Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Illustrates a typical module file structure within the 'src' directory for a NestJS application, including DTOs, guards, interfaces, controllers, services, and modules. ```treeview src/ ├── module-name/ │ ├── dto/ # Data Transfer Objects │ ├── guards/ # Module-specific guards │ ├── interfaces/ # TypeScript interfaces │ ├── module-name.controller.ts │ ├── module-name.service.ts │ ├── module-name.module.ts │ └── index.ts # Public exports ``` -------------------------------- ### Get Current Tenant Details - GET /api/tenants/current Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns details for the tenant identified by the active request context. Requires either an `X-Tenant-ID` header or a JWT with an `activeTenantId` claim. ```bash curl -s http://localhost:3000/api/tenants/current \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # { "id": "tenant-uuid", "name": "Acme Corp", "slug": "acme-corp", # "plan": "FREE", "status": "ACTIVE", "createdAt": "..." } ``` -------------------------------- ### Get Authenticated User Profile - GET /api/users/me Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns the authenticated user's profile, including all tenant memberships and roles. Useful for bootstrapping the frontend after login. ```bash curl -s http://localhost:3000/api/users/me \ -H "Authorization: Bearer $ACCESS_TOKEN" # { # "id": "user-uuid", # "email": "alice@example.com", # "displayName": "Alice", # "memberships": [ # { "tenantId": "tenant-uuid", "role": "OWNER", # "tenant": { "id": "...", "name": "Acme Corp", "slug": "acme-corp" } } # ] # } ``` -------------------------------- ### List Current Tenant Members - GET /api/tenants/current/members Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Lists all memberships with embedded user details (id, email, displayName) for the current tenant. ```bash curl -s http://localhost:3000/api/tenants/current/members \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # [ # { "id": "mem-uuid", "role": "OWNER", # "user": { "id": "...", "email": "alice@example.com", "displayName": "Alice" } }, # { "id": "mem-uuid2", "role": "MEMBER", # "user": { "id": "...", "email": "bob@example.com", "displayName": "Bob" } } # ] ``` -------------------------------- ### Billing - GET /api/billing/limits Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns the hard limits for the tenant's current plan. ```APIDOC ## GET /api/billing/limits ### Description Fetches the hard limits for various resources associated with the tenant's current subscription plan. ### Method GET ### Endpoint /api/billing/limits ### Parameters #### Query Parameters None #### Request Body None ### Response #### Success Response (200) An object containing the limits for different resources. - **members** (number) - The maximum number of members allowed. - **projects** (number) - The maximum number of projects allowed. - **apiCalls** (number) - The maximum number of API calls allowed. - **storage** (number) - The maximum storage space allowed (in bytes or other units). #### Response Example # FREE plan: { "members": 3, "projects": 5, "apiCalls": 1000, "storage": 100 } # PRO plan: { "members": 25, "projects": 100, "apiCalls": 50000, "storage": 10000 } ``` -------------------------------- ### Get All Feature Flags for Tenant Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Retrieve a map of all feature flags for the active tenant, merging global defaults with tenant-specific overrides. Results are cached in Redis for 60 seconds. ```bash curl -s http://localhost:3000/api/feature-flags \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # { # "new_dashboard": true, # "ai_suggestions": false, # "csv_export": true # } ``` -------------------------------- ### Audit Logging - GET /api/audit Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns paginated audit logs for the current tenant. Requires `audit.read` permission (OWNER or ADMIN). ```APIDOC ## GET /api/audit ### Description Retrieves a paginated list of audit logs for the current tenant. Access is restricted to users with 'audit.read' permission. ### Method GET ### Endpoint /api/audit ### Parameters #### Query Parameters - **skip** (number) - Optional - The number of audit logs to skip from the beginning. - **take** (number) - Optional - The maximum number of audit logs to return. #### Request Body None ### Response #### Success Response (200) A paginated array of audit log objects. - **id** (string) - Unique identifier for the audit log entry. - **action** (string) - The action that was performed (e.g., "user.invited"). - **entity** (string) - The type of entity the action was performed on (e.g., "Membership"). - **entityId** (string) - The ID of the specific entity. - **metadata** (object) - Additional data related to the audit event. - **tenantId** (string) - The ID of the tenant. - **userId** (string) - The ID of the user who performed the action. - **user** (object) - Information about the user who performed the action. - **id** (string) - User's ID. - **email** (string) - User's email address. - **createdAt** (string) - The timestamp when the audit log was created. #### Response Example [ { "id": "...", "action": "user.invited", "entity": "Membership", "entityId": "mem-uuid", "metadata": { "email": "bob@example.com" }, "tenantId": "tenant-uuid", "userId": "alice-uuid", "user": { "id": "...", "email": "alice@example.com" }, "createdAt": "2026-01-27T12:00:00.000Z" } ] ``` -------------------------------- ### Get Current Tenant Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns details for the tenant identified by the active request context. Requires `X-Tenant-ID` header or a JWT with `activeTenantId`. ```APIDOC ## GET /api/tenants/current ### Description Returns details for the tenant identified by the active request context. Requires `X-Tenant-ID` header or a JWT with `activeTenantId`. ### Method GET ### Endpoint /api/tenants/current ### Request Example ```bash curl -s http://localhost:3000/api/tenants/current \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" ``` ### Response #### Success Response (200) - **id** (string) - The unique identifier for the tenant. - **name** (string) - The name of the tenant. - **slug** (string) - A URL-safe identifier for the tenant. - **plan** (string) - The subscription plan for the tenant. - **status** (string) - The status of the tenant. - **createdAt** (string) - The timestamp when the tenant was created. #### Response Example ```json { "id": "tenant-uuid", "name": "Acme Corp", "slug": "acme-corp", "plan": "FREE", "status": "ACTIVE", "createdAt": "..." } ``` ``` -------------------------------- ### GET /api/feature-flags/:key/check Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Checks the status of a single feature flag. Tenant-specific overrides take precedence over global flag settings. ```APIDOC ## Feature Flags — `GET /api/feature-flags/:key/check` Checks a single flag. Tenant-specific overrides take priority over global flags. ### Method GET ### Endpoint `/api/feature-flags/:key/check` ### Parameters #### Path Parameters - **key** (string) - Required - The key of the feature flag to check. ### Headers - **Authorization**: `Bearer ` - **X-Tenant-ID**: `` ### Response Example (200 OK) ```json { "enabled": true } ``` ``` -------------------------------- ### Health Check Endpoints with NestJS Terminus Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Provides examples of using NestJS Terminus for health checks, including full checks, liveness probes, and readiness probes. Demonstrates `curl` commands and expected JSON responses for each endpoint. ```bash # Full check: database ping + memory heap/RSS + disk usage curl -s http://localhost:3000/api/health # { # "status": "ok", # "info": { # "database": { "status": "up" }, # "memory_heap": { "status": "up" }, # "memory_rss": { "status": "up" }, # "disk": { "status": "up" } # } # } # Liveness — just "is the process running?" curl -s http://localhost:3000/api/health/liveness # { "status": "ok", "info": {} } # Readiness — can the app serve traffic? (requires DB) curl -s http://localhost:3000/api/health/readiness # { "status": "ok", "info": { "database": { "status": "up" } } } # Thresholds: memory_heap < 150 MB, memory_rss < 300 MB, disk < 90% usage ``` -------------------------------- ### Check Resource Quota with GET /api/billing/quota/:resource Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Checks if the tenant is within quota for a specific resource. Returns `allowed: false` and a message if over the limit. Requires `billing.read` permission. ```bash curl -s http://localhost:3000/api/billing/quota/members \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # Under limit → { "allowed": true, "current": 2, "limit": 3 } # Over limit → { "allowed": false, "current": 3, "limit": 3, # "message": "members limit reached for your plan" } ``` -------------------------------- ### Start Active Span for Tracing Payments Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Initiates and manages an active span for payment processing using TracingService. Attaches tenant context and payment details to the span, and records errors if they occur. ```typescript // src/tracing/tracing.service.ts @Injectable() export class PaymentsService { constructor(private readonly tracing: TracingService) {} async processPayment(tenantId: string, amount: number) { return this.tracing.startActiveSpan('payments.process', async (span) => { try { // Attach tenant context to this span this.tracing.addTenantContext(tenantId); span.setAttributes({ 'payment.amount': amount, 'payment.currency': 'USD' }); const result = await this.stripeAdapter.charge(amount); span.setAttributes({ 'payment.id': result.id }); return result; } catch (err) { this.tracing.recordError(err as Error); // marks span as error throw err; } }); } // Propagate context to downstream HTTP calls async callExternalService(url: string) { const headers: Record = {}; this.tracing.inject(headers); // adds traceparent / tracestate headers return fetch(url, { headers }); } } ``` -------------------------------- ### GET /api/feature-flags Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Retrieves a map of all feature flags for the active tenant, merging global defaults with tenant-specific overrides. Results are cached in Redis for 60 seconds. ```APIDOC ## Feature Flags — `GET /api/feature-flags` Returns a key→boolean map of all feature flags for the active tenant, merging global defaults with any tenant-specific overrides. Results are Redis-cached for 60 seconds. ### Method GET ### Endpoint `/api/feature-flags` ### Headers - **Authorization**: `Bearer ` - **X-Tenant-ID**: `` ### Response Example (200 OK) ```json { "new_dashboard": true, "ai_suggestions": false, "csv_export": true } ``` ``` -------------------------------- ### Run Database Migrations and Seed Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Apply database migrations to set up the schema and optionally seed the database with initial data. ```bash npx prisma migrate dev ``` ```bash npx prisma db seed ``` -------------------------------- ### Git Workflow for Contributions Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/README.md Follow these steps to contribute to the project: fork, create a feature branch, commit changes, and open a pull request. ```bash git checkout -b feature/amazing-feature ``` ```bash git commit -m 'feat: add amazing feature' ``` ```bash git push origin feature/amazing-feature ``` -------------------------------- ### Log Tenant Creation with LoggerService Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Logs the process of creating a tenant using LoggerService, including initial parameters and the final tenant details upon successful creation. Handles and logs errors during the creation process. ```typescript // src/logger/logger.service.ts @Injectable() export class TenantService { constructor(private readonly logger: LoggerService) {} async createTenant(name: string, ownerId: string) { this.logger.info('Creating tenant', { name, ownerId }); try { const tenant = await this.prisma.tenant.create({ data: { name, slug: toSlug(name) } }); this.logger.info('Tenant created', { tenantId: tenant.id, slug: tenant.slug }); return tenant; } catch (err) { this.logger.error('Tenant creation failed', { name, error: (err as Error).message }); throw err; } } } ``` -------------------------------- ### Get Audit History for a Specific Resource with GET /api/audit/entity Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Fetches the complete audit history for a specific resource (entity type + ID). Useful for detailed change-log views. Requires `audit.read` permission. ```bash curl -s "http://localhost:3000/api/audit/entity?entity=Project&entityId=project-uuid" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" # [ # { "action": "project.created", "entity": "Project", "entityId": "project-uuid", ... }, # { "action": "project.updated", "entity": "Project", "entityId": "project-uuid", ... }, ``` -------------------------------- ### Switch Tenant - POST /api/auth/switch-tenant Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Issues a new token pair scoped to a different tenant. Fails with 401 if the user has no membership in the requested tenant. ```bash ACCESS_TOKEN="eyJhbGci..." curl -s -X POST http://localhost:3000/api/auth/switch-tenant \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"tenantId": "tenant-uuid-..."}' # 200 OK # { "accessToken": "eyJ... (new, activeTenantId updated)", "refreshToken": "..." } # Not a member → 401 # { "statusCode": 401, "message": "Not a member of this tenant" } ``` -------------------------------- ### Invite User to Tenant - POST /api/memberships/invite Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Invites an existing user to the current tenant. Requires `users.invite` permission (OWNER or ADMIN). The invited user must already have an account. Handles cases where the user is not found, already a member, or lacks permission. ```bash curl -s -X POST http://localhost:3000/api/memberships/invite \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "X-Tenant-ID: tenant-uuid" \ -H "Content-Type: application/json" \ -d '{"email": "bob@example.com", "role": "ADMIN"}' # 201 Created # { # "id": "mem-uuid", # "role": "ADMIN", # "tenantId": "tenant-uuid", # "userId": "bob-uuid", # "user": { "id": "bob-uuid", "email": "bob@example.com", "displayName": "Bob" } # } # User not found → 404; Already a member → 400; No permission → 403 ``` -------------------------------- ### Run Tests Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/CONTRIBUTING.md Execute unit tests, end-to-end tests, or generate test coverage reports. ```bash # Unit tests npm run test # E2E tests npm run test:e2e # Test coverage npm run test:cov ``` -------------------------------- ### NestJS Project Structure Overview Source: https://github.com/owalishawon/multi-tenant-saas-starter-nestjs/blob/main/README.md Illustrates the modular structure of the NestJS application, highlighting key modules like authentication, tenants, RBAC, and common utilities. ```tree src/ ├── auth/ # JWT authentication │ ├── strategies/ # Passport JWT/Local strategies │ └── dto/ # Login, Register, Refresh DTOs ├── tenants/ # Organization management ├── users/ # User profiles ├── memberships/ # User-Tenant relationships ├── rbac/ # Role-based access control │ ├── guards/ # Permissions guard │ └── decorators/ # @RequirePermissions() ├── feature-flags/ # Feature toggle system ├── cache/ # Redis caching layer ├── audit/ # Audit logging ├── billing/ # Payment integration │ ├── ports/ # BillingPort interface │ └── adapters/ # Stripe, Paddle, Mock ├── notifications/ # Email notifications │ ├── ports/ # NotificationPort interface │ └── adapters/ # SMTP, Resend, Console ├── common/ │ ├── guards/ # JwtAuthGuard, TenantGuard │ ├── interceptors/ # TenantContext, Audit │ ├── decorators/ # @CurrentUser, @CurrentTenant │ └── tenant-context/ # AsyncLocalStorage context └── main.ts ``` -------------------------------- ### Create Tenant - POST /api/tenants Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Creates a new organization with the authenticated user assigned the OWNER role. A URL-safe slug is generated from the organization's name. ```bash ACCESS_TOKEN="eyJhbGci..." curl -s -X POST http://localhost:3000/api/tenants \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Corp"}' # 201 Created # { # "id": "tenant-uuid", # "name": "Acme Corp", # "slug": "acme-corp", # "plan": "FREE", # "status": "ACTIVE", # "memberships": [{ "id": "...", "role": "OWNER", "userId": "..." }] # } # Slug conflict → 400 # { "statusCode": 400, "message": "Tenant slug already exists" } ``` -------------------------------- ### Invite User to Tenant Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Invites an existing user to the current tenant. Requires `users.invite` permission (OWNER or ADMIN). The invited user must already have an account. ```APIDOC ## POST /api/memberships/invite ### Description Invites an existing user to the current tenant. Requires `users.invite` permission (OWNER or ADMIN). The invited user must already have an account. ### Method POST ### Endpoint /api/memberships/invite ### Request Body - **email** (string) - Required - The email address of the user to invite. - **role** (string) - Required - The role to assign to the invited user (e.g., ADMIN, MEMBER). ### Request Example ```json { "email": "bob@example.com", "role": "ADMIN" } ``` ### Response #### Success Response (201 Created) - **id** (string) - The membership's unique identifier. - **role** (string) - The assigned role. - **tenantId** (string) - The tenant's unique identifier. - **userId** (string) - The invited user's unique identifier. - **user** (object) - Embedded user details. - **id** (string) - The user's unique identifier. - **email** (string) - The user's email address. - **displayName** (string) - The user's display name. ### Response Example ```json { "id": "mem-uuid", "role": "ADMIN", "tenantId": "tenant-uuid", "userId": "bob-uuid", "user": { "id": "bob-uuid", "email": "bob@example.com", "displayName": "Bob" } } ``` #### Error Response (404) - **message** (string) - Description of the error (e.g., "User not found"). #### Error Response (400) - **message** (string) - Description of the error (e.g., "User already a member"). #### Error Response (403) - **message** (string) - Description of the error (e.g., "Permission denied"). ``` -------------------------------- ### NestJS Application Bootstrap Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt This code sets up the NestJS application, including global prefix, validation pipe, and Swagger documentation. It configures CORS and sets a body size limit. ```typescript // src/main.ts (abridged for clarity) import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule, { cors: true }); app.setGlobalPrefix('api'); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidUnknownValues: true, transform: true }), ); const config = new DocumentBuilder() .setTitle('Multi-Tenant SaaS API') .setVersion('1.0') .addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'JWT-auth') .addApiKey({ type: 'apiKey', name: 'X-Tenant-ID', in: 'header' }, 'X-Tenant-ID') .build(); SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config)); await app.listen(3000); // → API: http://localhost:3000/api // → Docs: http://localhost:3000/docs } bootstrap(); ``` -------------------------------- ### Audit Logging - GET /api/audit/entity Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Fetches the complete audit history for a specific resource (entity type + ID). Useful for detailed change-log views. ```APIDOC ## GET /api/audit/entity ### Description Retrieves the full audit history for a specific resource, identified by its entity type and ID. This is useful for tracking changes to a particular item. ### Method GET ### Endpoint /api/audit/entity ### Parameters #### Query Parameters - **entity** (string) - Required - The type of the entity (e.g., "Project"). - **entityId** (string) - Required - The unique identifier of the entity. #### Request Body None ### Response #### Success Response (200) A list of audit log objects pertaining to the specified entity. - **action** (string) - The action that was performed (e.g., "project.created"). - **entity** (string) - The type of the entity. - **entityId** (string) - The ID of the entity. - ... (other fields as applicable to audit logs) #### Response Example [ { "action": "project.created", "entity": "Project", "entityId": "project-uuid", ... }, { "action": "project.updated", "entity": "Project", "entityId": "project-uuid", ... } ] ``` -------------------------------- ### Get Authenticated User Profile Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Returns the authenticated user's profile including all tenant memberships and roles. Useful for bootstrapping the frontend after login. ```APIDOC ## GET /api/users/me ### Description Returns the authenticated user's profile including all tenant memberships and roles. Useful for bootstrapping the frontend after login. ### Method GET ### Endpoint /api/users/me ### Response #### Success Response (200) - **id** (string) - The user's unique identifier. - **email** (string) - The user's email address. - **displayName** (string) - The user's display name. - **memberships** (array) - An array of the user's tenant memberships. - **tenantId** (string) - The tenant's unique identifier. - **role** (string) - The user's role within the tenant. - **tenant** (object) - Embedded tenant details. - **id** (string) - The tenant's unique identifier. - **name** (string) - The tenant's name. - **slug** (string) - The tenant's slug. ### Response Example ```json { "id": "user-uuid", "email": "alice@example.com", "displayName": "Alice", "memberships": [ { "tenantId": "tenant-uuid", "role": "OWNER", "tenant": { "id": "...", "name": "Acme Corp", "slug": "acme-corp" } } ] } ``` ``` -------------------------------- ### Sending Emails with NotificationsService Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Illustrates how to use the NotificationsService to send various types of emails, including welcome, invite, subscription confirmation, password reset, and custom HTML emails. Requires user, tenant, and plan details for specific templates. ```typescript // src/notifications/notifications.service.ts @Injectable() export class OnboardingService { constructor(private readonly notifications: NotificationsService) {} async onUserRegistered(user: User) { await this.notifications.sendWelcomeEmail(user.email, { userName: user.displayName ?? user.email, loginUrl: `${process.env.FRONTEND_URL}/login`, }); } async onMemberInvited(toEmail: string, inviter: User, tenant: Tenant) { await this.notifications.sendInviteEmail(toEmail, { inviterName: inviter.displayName ?? inviter.email, tenantName: tenant.name, inviteUrl: `${process.env.FRONTEND_URL}/accept-invite?tenant=${tenant.slug}`, }); } async onSubscriptionUpgraded(to: string, plan: string) { await this.notifications.sendSubscriptionConfirmation(to, { planName: plan, amount: '$49/month', nextBillingDate: '2026-03-01', }); } async onPasswordReset(to: string, token: string) { await this.notifications.sendPasswordResetEmail(to, { resetUrl: `${process.env.FRONTEND_URL}/reset-password?token=${token}`, expiresIn: '1 hour', }); } // Raw email for custom templates async sendCustomEmail() { await this.notifications.sendEmail({ to: 'user@example.com', subject: 'Your invoice is ready', html: '

Download your invoice here.

', text: 'Download your invoice at: ...', }); } } ``` -------------------------------- ### Create Tenant Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Creates a new organization. The authenticated user is automatically assigned the OWNER role. A URL-safe slug is generated from the name. ```APIDOC ## POST /api/tenants ### Description Creates a new organization. The authenticated user is automatically assigned the `OWNER` role. A URL-safe `slug` is generated from the name. ### Method POST ### Endpoint /api/tenants ### Request Body - **name** (string) - Required - The name of the tenant to create. ### Request Example ```bash ACCESS_TOKEN="eyJhbGci..." curl -s -X POST http://localhost:3000/api/tenants \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Corp"}' ``` ### Response #### Success Response (201) - **id** (string) - The unique identifier for the tenant. - **name** (string) - The name of the tenant. - **slug** (string) - A URL-safe identifier for the tenant. - **plan** (string) - The subscription plan for the tenant. - **status** (string) - The status of the tenant (e.g., ACTIVE). - **memberships** (array) - A list of memberships for the tenant. - **id** (string) - The membership ID. - **role** (string) - The role of the user in the tenant. - **userId** (string) - The ID of the user. #### Response Example ```json { "id": "tenant-uuid", "name": "Acme Corp", "slug": "acme-corp", "plan": "FREE", "status": "ACTIVE", "memberships": [{ "id": "...", "role": "OWNER", "userId": "..." }] } ``` #### Error Response (400) - **statusCode** (number) - 400 - **message** (string) - Tenant slug already exists #### Error Response Example ```json { "statusCode": 400, "message": "Tenant slug already exists" } ``` ``` -------------------------------- ### Get Current Trace Context Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Retrieves the current tracing context, including trace ID, span ID, and trace flags, which can be useful for correlating logs. ```typescript // Read current trace ID (e.g., for logging correlation): const ctx = this.tracing.getCurrentContext(); // { traceId: "abc123...", spanId: "def456...", traceFlags: 1 } ``` -------------------------------- ### Switch Tenant Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Issues a new token pair scoped to a different tenant. Fails with 401 if the user has no membership in the requested tenant. ```APIDOC ## POST /api/auth/switch-tenant ### Description Issues a new token pair scoped to a different tenant. Fails with 401 if the user has no membership in the requested tenant. ### Method POST ### Endpoint /api/auth/switch-tenant ### Request Body - **tenantId** (string) - Required - The ID of the tenant to switch to. ### Request Example ```bash ACCESS_TOKEN="eyJhbGci..." curl -s -X POST http://localhost:3000/api/auth/switch-tenant \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"tenantId": "tenant-uuid-..."}' ``` ### Response #### Success Response (200) - **accessToken** (string) - The new access token. - **refreshToken** (string) - The new refresh token. #### Response Example ```json { "accessToken": "eyJ... (new, activeTenantId updated)", "refreshToken": "..." } ``` #### Error Response (401) - **statusCode** (number) - 401 - **message** (string) - Not a member of this tenant #### Error Response Example ```json { "statusCode": 401, "message": "Not a member of this tenant" } ``` ``` -------------------------------- ### Billing - GET /api/billing/quota/:resource Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Checks whether the tenant is within quota for a specific resource. Returns `allowed: false` plus a human-readable `message` when over the limit. ```APIDOC ## GET /api/billing/quota/:resource ### Description Checks if the tenant has exceeded the quota for a specified resource. Provides a boolean indicating allowance and a message if the limit is reached. ### Method GET ### Endpoint /api/billing/quota/:resource ### Parameters #### Path Parameters - **resource** (string) - Required - The name of the resource to check (e.g., "members", "projects"). #### Query Parameters None #### Request Body None ### Response #### Success Response (200) - **allowed** (boolean) - True if the tenant is within the quota for the resource, false otherwise. - **current** (number) - The current usage count for the resource. - **limit** (number) - The quota limit for the resource. - **message** (string) - A human-readable message explaining the status, especially when over the limit. #### Response Example # Under limit { "allowed": true, "current": 2, "limit": 3 } # Over limit { "allowed": false, "current": 3, "limit": 3, "message": "members limit reached for your plan" } ``` -------------------------------- ### Register New User API Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Registers a new user by hashing the password with bcrypt. It returns only the user's ID and email, and does not issue tokens. Subsequent token issuance requires calling the login endpoint. ```bash curl -s -X POST http://localhost:3000/api/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "password": "supersecret123", "displayName": "Alice" }' # 201 Created # { "id": "uuid- மரு", "email": "alice@example.com" } # Duplicate email → 400 # { "statusCode": 400, "message": "Email already in use" } ``` -------------------------------- ### Custom Metrics Recording with MetricsService Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Demonstrates how to use MetricsService to increment counters, set gauge values, and observe histogram durations with custom labels. ```typescript // Custom metrics: this.metrics.incrementCounter('payments_processed_total', { currency: 'USD', plan: 'PRO' }); this.metrics.setGauge('queue_depth', 42, { queue: 'email' }); this.metrics.observeHistogram('external_api_duration_seconds', 0.234, { service: 'stripe' }); ``` -------------------------------- ### Tenant Context Service - AsyncLocalStorage Source: https://context7.com/owalishawon/multi-tenant-saas-starter-nestjs/llms.txt Provides request-scoped tenant, user, and role data using Node's AsyncLocalStorage. This avoids manual context passing and circular dependencies. ```typescript // src/common/tenant-context/tenant-context.service.ts import { AsyncLocalStorage } from 'async_hooks'; import { Injectable } from '@nestjs/common'; @Injectable() export class TenantContextService { private readonly storage = new AsyncLocalStorage(); runWithContext(context: TenantRequestContext, cb: () => T): T { return this.storage.run(context, cb); } getTenantId(): string | undefined { return this.storage.getStore()?.tenantId; } getUserId(): string | undefined { return this.storage.getStore()?.userId; } getRoles(): string[] { return this.storage.getStore()?.roles ?? []; } } // Consuming in a service: @Injectable() export class MyService { constructor(private readonly ctx: TenantContextService) {} doSomething() { const tenantId = this.ctx.getTenantId(); // auto-resolved from current request // use tenantId for scoped DB queries } } ```