# Multi-Tenancy Guide

## Overview

ERP Suite implementa **multi-tenancy a nivel de schema** usando PostgreSQL Row-Level Security (RLS) para garantizar aislamiento completo de datos entre tenants.

**Modelo:** Shared database, shared schema, **isolated by tenant_id**

## Architecture

### Tenant Isolation Strategy

```
┌─────────────────────────────────────────┐
│         Single PostgreSQL Database      │
│                                          │
│  ┌────────────────────────────────────┐ │
│  │  Table: products.products          │ │
│  │  ┌──────────────────────────────┐  │ │
│  │  │ Tenant A rows (tenant_id=A)  │  │ │
│  │  ├──────────────────────────────┤  │ │
│  │  │ Tenant B rows (tenant_id=B)  │  │ │
│  │  ├──────────────────────────────┤  │ │
│  │  │ Tenant C rows (tenant_id=C)  │  │ │
│  │  └──────────────────────────────┘  │ │
│  │                                    │ │
│  │  RLS Policy: WHERE tenant_id =    │ │
│  │    current_setting('app.current_   │ │
│  │    tenant_id')::uuid               │ │
│  └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
```

### Why RLS (Row-Level Security)?

**Advantages:**
- **Security at database level** - Even if application has bugs, data is isolated
- **Transparent to application** - No manual filtering in every query
- **Works with BI tools** - Reports automatically scoped to tenant
- **Audit trail** - PostgreSQL logs enforce tenant context

**Disadvantages:**
- PostgreSQL specific (not portable to MySQL/MongoDB)
- Slight performance overhead (minimal)
- Requires SET LOCAL on each connection

## Implementation

### 1. Database Schema Design

#### Tenant Table

```sql
-- Schema: auth
CREATE TABLE auth.tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    plan VARCHAR(50) NOT NULL DEFAULT 'free',
    settings JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at TIMESTAMPTZ
);

-- Index
CREATE INDEX idx_tenants_slug ON auth.tenants(slug) WHERE deleted_at IS NULL;
CREATE INDEX idx_tenants_status ON auth.tenants(status);
```

#### User-Tenant Relationship

```sql
CREATE TABLE auth.users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    -- ...
    CONSTRAINT uq_user_email_tenant UNIQUE (email, tenant_id)
);

CREATE INDEX idx_users_tenant_id ON auth.users(tenant_id);
```

#### Standard Table Structure

**Every table** (except auth.tenants) must have:

```sql
CREATE TABLE {schema}.{table} (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),

    -- Business columns
    -- ...

    -- Audit columns
    created_by UUID REFERENCES auth.users(id),
    updated_by UUID REFERENCES auth.users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at TIMESTAMPTZ  -- Soft delete
);

-- Standard indexes
CREATE INDEX idx_{table}_tenant_id ON {schema}.{table}(tenant_id);
CREATE INDEX idx_{table}_deleted_at ON {schema}.{table}(deleted_at);
```

### 2. Row-Level Security Policies

#### Enable RLS

```sql
ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY;
```

#### Standard Policy

```sql
-- Policy: tenant_isolation
-- Applies to: SELECT, INSERT, UPDATE, DELETE
CREATE POLICY tenant_isolation ON {schema}.{table}
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

**What this does:**
- `USING` clause filters SELECT queries
- Also applies to UPDATE and DELETE
- INSERT requires tenant_id to match

#### Super Admin Bypass (Optional)

```sql
-- Allow super admins to see all data
CREATE POLICY admin_bypass ON {schema}.{table}
    USING (
        current_setting('app.current_tenant_id')::uuid = tenant_id
        OR
        current_setting('app.is_super_admin', true)::boolean = true
    );
```

#### Example: Full RLS Setup

```sql
-- Create table
CREATE TABLE products.products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
    name VARCHAR(255) NOT NULL,
    sku VARCHAR(100),
    price NUMERIC(12, 2),
    created_by UUID REFERENCES auth.users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at TIMESTAMPTZ
);

-- Indexes
CREATE INDEX idx_products_tenant_id ON products.products(tenant_id);
CREATE INDEX idx_products_sku ON products.products(tenant_id, sku) WHERE deleted_at IS NULL;

-- Enable RLS
ALTER TABLE products.products ENABLE ROW LEVEL SECURITY;

-- Create policy
CREATE POLICY tenant_isolation ON products.products
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Grant permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON products.products TO erp_app_user;
```

### 3. Backend Implementation

#### Middleware: Set Tenant Context

```typescript
// middleware/tenant-context.middleware.ts
import { Request, Response, NextFunction } from 'express';
import pool from '../config/database';

export const setTenantContext = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const tenantId = req.user?.tenantId; // From JWT

  if (!tenantId) {
    return res.status(401).json({ error: 'Tenant not found' });
  }

  try {
    // Set tenant context for this connection
    await pool.query(
      `SET LOCAL app.current_tenant_id = $1`,
      [tenantId]
    );

    // Optional: Set super admin flag
    if (req.user?.role === 'super_admin') {
      await pool.query(`SET LOCAL app.is_super_admin = true`);
    }

    next();
  } catch (error) {
    console.error('Error setting tenant context:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
};
```

**Apply to all routes:**

```typescript
// app.ts
import express from 'express';
import { authenticateJWT } from './middleware/auth.middleware';
import { setTenantContext } from './middleware/tenant-context.middleware';

const app = express();

// Apply to all routes after authentication
app.use(authenticateJWT);
app.use(setTenantContext);

// Now all routes are tenant-scoped
app.use('/api/products', productRoutes);
```

#### BaseService with Tenant Enforcement

```typescript
// shared/services/base.service.ts
export abstract class BaseService<T, CreateDto, UpdateDto> {
  constructor(
    protected tableName: string,
    protected schema: string
  ) {}

  async findAll(
    tenantId: string,
    filters?: Filters,
    pagination?: Pagination
  ): Promise<PaginatedResult<T>> {
    const { page = 1, limit = 20 } = pagination || {};
    const offset = (page - 1) * limit;

    // RLS automatically filters by tenant_id
    // No need to add WHERE tenant_id = $1
    const query = `
      SELECT *
      FROM ${this.schema}.${this.tableName}
      WHERE deleted_at IS NULL
      ORDER BY created_at DESC
      LIMIT $1 OFFSET $2
    `;

    const result = await pool.query(query, [limit, offset]);

    return {
      data: result.rows,
      total: result.rowCount,
      page,
      limit
    };
  }

  async create(
    data: CreateDto,
    tenantId: string,
    userId: string
  ): Promise<T> {
    const columns = Object.keys(data);
    const values = Object.values(data);

    // Explicitly add tenant_id
    columns.push('tenant_id', 'created_by');
    values.push(tenantId, userId);

    const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');

    const query = `
      INSERT INTO ${this.schema}.${this.tableName} (${columns.join(', ')})
      VALUES (${placeholders})
      RETURNING *
    `;

    const result = await pool.query(query, values);
    return result.rows[0];
  }

  // Other CRUD methods...
}
```

### 4. Frontend Implementation

#### Store Tenant Info in Auth State

```typescript
// stores/auth.store.ts
import create from 'zustand';

interface User {
  id: string;
  email: string;
  tenantId: string;
  tenantName: string;
  role: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: null,

  login: async (email, password) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });

    const { user, token } = await response.json();

    set({ user, token });
    localStorage.setItem('token', token);
  },

  logout: () => {
    set({ user: null, token: null });
    localStorage.removeItem('token');
  }
}));
```

#### Display Tenant Context

```tsx
// components/TenantIndicator.tsx
import { useAuthStore } from '../stores/auth.store';

export const TenantIndicator = () => {
  const user = useAuthStore((state) => state.user);

  if (!user) return null;

  return (
    <div className="bg-blue-100 px-4 py-2 rounded">
      <span className="text-sm text-gray-700">
        Tenant: <strong>{user.tenantName}</strong>
      </span>
    </div>
  );
};
```

## Multi-Tenant SaaS Features

### Onboarding New Tenant

```typescript
// services/tenant.service.ts
export class TenantService {
  async createTenant(data: CreateTenantDto): Promise<Tenant> {
    const client = await pool.connect();

    try {
      await client.query('BEGIN');

      // 1. Create tenant
      const tenantQuery = `
        INSERT INTO auth.tenants (name, slug, plan)
        VALUES ($1, $2, $3)
        RETURNING *
      `;
      const tenantResult = await client.query(tenantQuery, [
        data.name,
        data.slug,
        data.plan || 'free'
      ]);
      const tenant = tenantResult.rows[0];

      // 2. Create admin user for tenant
      const passwordHash = await bcrypt.hash(data.adminPassword, 10);
      const userQuery = `
        INSERT INTO auth.users (tenant_id, email, password_hash, role)
        VALUES ($1, $2, $3, $4)
        RETURNING *
      `;
      await client.query(userQuery, [
        tenant.id,
        data.adminEmail,
        passwordHash,
        'admin'
      ]);

      // 3. Initialize default data (catalogs, etc.)
      await this.initializeTenantData(client, tenant.id);

      await client.query('COMMIT');
      return tenant;

    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  private async initializeTenantData(client: PoolClient, tenantId: string) {
    // Create default categories
    await client.query(`
      INSERT INTO products.categories (tenant_id, name)
      VALUES
        ($1, 'General'),
        ($1, 'Services')
    `, [tenantId]);

    // Create default warehouse
    await client.query(`
      INSERT INTO inventory.warehouses (tenant_id, name, code)
      VALUES ($1, 'Main Warehouse', 'WH01')
    `, [tenantId]);

    // More defaults...
  }
}
```

### Tenant Switching (for super admins)

```typescript
// middleware/switch-tenant.middleware.ts
export const switchTenant = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const targetTenantId = req.headers['x-tenant-id'] as string;

  if (req.user?.role !== 'super_admin') {
    return res.status(403).json({ error: 'Only super admins can switch tenants' });
  }

  if (!targetTenantId) {
    return res.status(400).json({ error: 'Target tenant ID required' });
  }

  // Override tenant context
  await pool.query(`SET LOCAL app.current_tenant_id = $1`, [targetTenantId]);
  next();
};
```

## Data Isolation Testing

### Test Suite

```typescript
// __tests__/multi-tenancy.test.ts
import { pool } from '../config/database';
import { TenantService } from '../services/tenant.service';
import { ProductService } from '../modules/products/product.service';

describe('Multi-Tenancy Data Isolation', () => {
  let tenantA: Tenant;
  let tenantB: Tenant;

  beforeAll(async () => {
    tenantA = await TenantService.createTenant({ name: 'Tenant A', ... });
    tenantB = await TenantService.createTenant({ name: 'Tenant B', ... });
  });

  it('should isolate data between tenants', async () => {
    // Set context to Tenant A
    await pool.query(`SET LOCAL app.current_tenant_id = $1`, [tenantA.id]);

    // Create product for Tenant A
    const productA = await ProductService.create({
      name: 'Product A',
      price: 100
    }, tenantA.id, 'user-a');

    // Switch context to Tenant B
    await pool.query(`SET LOCAL app.current_tenant_id = $1`, [tenantB.id]);

    // Try to fetch products (should only see Tenant B's data)
    const products = await ProductService.findAll(tenantB.id);

    expect(products.data).toHaveLength(0); // Tenant B has no products
    expect(products.data).not.toContainEqual(productA);
  });

  it('should prevent cross-tenant access', async () => {
    // Set context to Tenant A
    await pool.query(`SET LOCAL app.current_tenant_id = $1`, [tenantA.id]);

    // Create product for Tenant A
    const productA = await ProductService.create({
      name: 'Product A'
    }, tenantA.id, 'user-a');

    // Switch to Tenant B
    await pool.query(`SET LOCAL app.current_tenant_id = $1`, [tenantB.id]);

    // Try to access Tenant A's product
    const result = await ProductService.findById(productA.id, tenantB.id);

    expect(result).toBeNull(); // RLS blocks access
  });
});
```

## Performance Considerations

### Indexing Strategy

**Always index tenant_id:**

```sql
CREATE INDEX idx_{table}_tenant_id ON {schema}.{table}(tenant_id);
```

**Composite indexes for common queries:**

```sql
-- Query: Find product by SKU for tenant
CREATE INDEX idx_products_tenant_sku
ON products.products(tenant_id, sku)
WHERE deleted_at IS NULL;

-- Query: List recent orders for tenant
CREATE INDEX idx_orders_tenant_created
ON sales.orders(tenant_id, created_at DESC);
```

### Query Performance

**With RLS:**
```sql
EXPLAIN ANALYZE
SELECT * FROM products.products WHERE sku = 'ABC123';

-- Plan:
-- Index Scan using idx_products_tenant_sku
-- Filter: (tenant_id = '...'::uuid)  ← Automatic
```

**RLS overhead:** ~5-10% (minimal with proper indexing)

### Connection Pooling

```typescript
// config/database.ts
import { Pool } from 'pg';

export const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20,  // Max connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// Set tenant context on each query
pool.on('connect', async (client) => {
  // This is set per-request, not per-connection
  // Use middleware instead
});
```

## Security Best Practices

### 1. Always Validate Tenant Access

```typescript
// Even with RLS, validate user belongs to tenant
if (req.user.tenantId !== req.params.tenantId) {
  return res.status(403).json({ error: 'Forbidden' });
}
```

### 2. Never Disable RLS

```sql
-- ❌ NEVER DO THIS
ALTER TABLE products.products NO FORCE ROW LEVEL SECURITY;
```

### 3. Audit Tenant Changes

```sql
CREATE TABLE audit.tenant_changes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    user_id UUID NOT NULL,
    action VARCHAR(50) NOT NULL,
    table_name VARCHAR(100) NOT NULL,
    record_id UUID,
    old_data JSONB,
    new_data JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 4. Monitor Cross-Tenant Attempts

```typescript
// Log suspicious activity
if (attemptedCrossTenantAccess) {
  logger.warn('Cross-tenant access attempt', {
    userId: req.user.id,
    userTenant: req.user.tenantId,
    targetTenant: req.params.tenantId,
    ip: req.ip
  });
}
```

## Troubleshooting

### RLS Not Working

**Check if RLS is enabled:**
```sql
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'products';
```

**Check policies:**
```sql
SELECT * FROM pg_policies WHERE tablename = 'products';
```

**Verify tenant context is set:**
```sql
SHOW app.current_tenant_id;
```

### Performance Issues

**Check query plan:**
```sql
EXPLAIN ANALYZE
SELECT * FROM products.products WHERE name LIKE '%widget%';
```

**Add missing indexes:**
```sql
CREATE INDEX idx_products_name ON products.products(tenant_id, name);
```

## References

- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- [Architecture Documentation](./ARCHITECTURE.md)
- [Database Schema](../apps/erp-core/database/ddl/)
