# Migration Guide: 11.19.x → 11.20.0

## Overview

| Category | Details |
|----------|---------|
| **Breaking Changes** | None |
| **New Features** | Multi-Tenancy Plugin (opt-in) |
| **Bugfixes** | None |
| **Migration Effort** | 0 min (feature is opt-in, no changes required for existing projects) |

---

## Quick Migration (No Breaking Changes)

```bash
# Update package
pnpm add @lenne.tech/nest-server@11.20.0

# Verify build
pnpm run build

# Run tests
pnpm test
```

No code changes required. Multi-tenancy is disabled by default and has zero overhead when not configured.

---

## What's New in 11.20.0

### Multi-Tenancy: Tenant-Based Data Isolation

A global Mongoose plugin that automatically filters all database queries by `tenantId`, providing tenant isolation in a shared MongoDB database.

**Key features:**
- Auto-filters queries (find, update, delete, aggregate, count, distinct)
- Auto-sets `tenantId` on new documents (save, insertMany)
- Zero overhead when disabled (plugin not registered)
- Works with both Legacy Auth (JWT) and BetterAuth (IAM)
- Follows the "Presence Implies Enabled" configuration pattern

#### Step 1: Enable in config

```typescript
// config.env.ts
{
  // Enable with defaults (userField: 'tenantId')
  multiTenancy: {},

  // Or with options
  multiTenancy: {
    excludeSchemas: ['User', 'Session'], // Model names to exclude from filtering
    userField: 'tenantId',               // Field on req.user (default: 'tenantId')
  },

  // Pre-configured but disabled
  multiTenancy: {
    enabled: false,
    excludeSchemas: ['User'],
  },
}
```

#### Step 2: Add `tenantId` to your User model

```typescript
// In your project's User model (NOT in nest-server!)
export class User extends CoreUserModel {
  @UnifiedField({
    mongoose: { type: String, index: true },
    roles: RoleEnum.S_NO_ONE, // Users cannot modify their own tenantId
  })
  tenantId: string = undefined;
}
```

#### Step 3: Add `tenantId` to domain models

> **Security Warning:** You **MUST** use `@UnifiedField` with `roles: RoleEnum.S_NO_ONE` on the `tenantId` field of every domain model. Without this, API consumers can inject arbitrary tenant IDs via request body, bypassing tenant isolation on writes. The plugin auto-sets `tenantId` from the user context — manual input should be blocked.
>
> **This is a developer responsibility that the plugin cannot enforce.** The tenant plugin operates at the Mongoose middleware layer and has no knowledge of your GraphQL/REST input schema. If you omit the `roles: RoleEnum.S_NO_ONE` declaration, the field will be writable via API input, silently undermining tenant isolation on write paths. There is no runtime warning — the plugin will simply preserve whatever `tenantId` value was provided in the request body instead of auto-assigning from the user context.

```typescript
@MongooseSchema({ timestamps: true })
export class Order extends CorePersistenceModel {
  @UnifiedField({
    mongoose: { type: String, index: true },
    roles: RoleEnum.S_NO_ONE, // Prevent user from setting tenantId via API input
  })
  tenantId: string = undefined;

  // ... other fields
}
```

The plugin automatically activates on any schema that has a `tenantId` field. Schemas without `tenantId` are not affected.

#### Cross-Tenant Admin Operations

**Important:** ADMIN users are also filtered by their own `tenantId`. For admin dashboards or cross-tenant queries, use the explicit bypass:

```typescript
import { RequestContext } from '@lenne.tech/nest-server';

// Admin dashboard: view all tenants
const allOrders = await RequestContext.runWithBypassTenantGuard(async () => {
  return this.orderService.find();
});
```

#### Behavior Reference

| Situation | Behavior |
|-----------|----------|
| No `multiTenancy` config | Plugin not registered, zero overhead |
| No RequestContext (cron, migration) | No filter applied |
| `bypassTenantGuard: true` | No filter applied |
| Schema in `excludeSchemas` | No filter applied |
| User with `tenantId: "abc"` | Filter: `{ tenantId: "abc" }` |
| User without `tenantId` | Filter: `{ tenantId: null }` (sees only unassigned data) |
| User with `tenantId: ""` (empty string) | Same as without — empty string is falsy, filter: `{ tenantId: null }` |
| No user (public endpoint) | No filter applied |

#### Known Limitations

1. **`populate()`**: Referenced models with `tenantId` are also filtered. Shared models (User, Config) should be in `excludeSchemas`.
2. **`estimatedDocumentCount`**: Does not support query filters (MongoDB limitation).
3. **`$lookup` in aggregates**: Joined collections are not automatically filtered. Add `$match` in `$lookup.pipeline` manually.
4. **Legacy data**: Existing documents without `tenantId` are not auto-assigned. Backfill tenantIds via migration script.
5. **Write-path protection**: The plugin auto-sets `tenantId` on new documents but does not enforce it on explicit values. Use `@UnifiedField({ roles: RoleEnum.S_NO_ONE })` on domain model `tenantId` fields to prevent cross-tenant write injection via API input.
6. **Empty string tenantId**: If `req.user.tenantId` is an empty string (`''`), it is treated as falsy — the user sees only documents with `tenantId: null`. Ensure tenant IDs are never stored as empty strings.

---

## Compatibility Notes

### Pattern: Existing projects without multi-tenancy

**Status: Fully compatible, no action required**

If your project does not configure `multiTenancy`, the plugin is never registered. There is zero performance impact.

### Pattern: Projects with custom Mongoose plugins

**Status: Compatible**

The tenant plugin is registered after the audit fields plugin in `connectionFactory`. It does not interfere with other plugins.

---

## Module Documentation

| Module | Documentation |
|--------|--------------|
| Multi-Tenancy Plugin | `src/core/common/plugins/mongoose-tenant.plugin.ts` |
| RequestContext | `src/core/common/services/request-context.service.ts` |
| Configuration | `IMultiTenancy` in `src/core/common/interfaces/server-options.interface.ts` |

---

## Troubleshooting

| Issue | Solution |
|-------|----------|
| Admin can't see all tenants | Use `RequestContext.runWithBypassTenantGuard()` — ADMIN users are also filtered |
| `populate()` returns null for cross-tenant refs | Add referenced model to `excludeSchemas` |
| Legacy data invisible after enabling MT | Backfill `tenantId` on existing documents |
| Cron job sees all data | Expected — no RequestContext = no filter |
