# Multi-Tenant API Platform Example

A production-ready microservices architecture demonstrating service mesh patterns, distributed caching, async processing, and multi-tenancy.

## What This Example Demonstrates

- **Microservices Architecture**: 6 independent services with clear boundaries
- **API Gateway Pattern**: Single entry point with routing and rate limiting
- **Service Mesh**: Inter-service communication with shared infrastructure
- **Multi-Database**: Separate databases per service domain
- **Async Processing**: Background workers for notifications and analytics
- **Multi-Tenancy**: Isolation and rate limiting per tenant/API key

## Architecture Overview

```
                                ┌─────────────┐
                          ┌────►│Auth Service │
                          │     └─────────────┘
                          │
┌────────┐      ┌─────────┴──┐  ┌─────────────┐
│ Client │─────►│   Gateway  │─►│User Service │
└────────┘      └─────────┬──┘  └─────────────┘
                          │
                          │     ┌──────────────────┐
                          └────►│Payment Service   │
                                └──────────────────┘

                ┌──────────┐
                │  Redis   │◄─── (All Services)
                └──────────┘

                ┌──────────┐     ┌───────────────────┐
                │  Queue   │────►│Notification Service│
                └──────────┘     └───────────────────┘
                     │
                     │           ┌──────────────────┐
                     └──────────►│Analytics Worker  │
                                 └──────────────────┘
```

## Quick Start

### 1. Generate the Platform

```bash
cd examples/api-platform
bang generate api-platform.workflow.yaml
```

Generates:
- 6 microservices with independent codebases
- Shared infrastructure configuration
- API Gateway with routing
- Docker Compose for local development
- Kubernetes manifests for production

### 2. Start All Services

```bash
# Start infrastructure
docker-compose up -d

# Install dependencies for all services
npm run install:all

# Run migrations for each service
npm run migrate:all

# Start all services
npm run dev:all
```

### 3. Access Services

- **API Gateway**: http://localhost:3000
- **Auth Service**: http://localhost:3001
- **User Service**: http://localhost:3002
- **Payment Service**: http://localhost:3003
- **API Docs**: http://localhost:3000/docs

## Microservices Patterns

### Pattern 1: API Gateway with Rate Limiting

```typescript
// gateway/src/middleware/rate-limit.ts
import { redisClient } from '../services/redis-client';

export async function rateLimitByApiKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  const tenant = await getTenantByApiKey(apiKey);

  const key = `rate-limit:${tenant.id}:${req.path}`;
  const limit = tenant.rateLimit || 1000; // requests per hour
  const window = 3600; // 1 hour in seconds

  const current = await redisClient.incr(key);

  if (current === 1) {
    await redisClient.expire(key, window);
  }

  if (current > limit) {
    const ttl = await redisClient.ttl(key);
    return res.status(429).json({
      error: 'Rate limit exceeded',
      limit,
      retryAfter: ttl,
      tenant: tenant.name,
    });
  }

  res.setHeader('X-RateLimit-Limit', limit);
  res.setHeader('X-RateLimit-Remaining', limit - current);

  next();
}
```

### Pattern 2: Service-to-Service Authentication

```typescript
// gateway/src/services/service-auth.ts
import jwt from 'jsonwebtoken';

export function generateServiceToken(serviceName: string) {
  return jwt.sign(
    { service: serviceName },
    process.env.SERVICE_SECRET,
    { expiresIn: '5m' }
  );
}

// user-service/src/middleware/verify-service.ts
export function verifyServiceToken(req, res, next) {
  const token = req.headers['x-service-token'];

  try {
    const payload = jwt.verify(token, process.env.SERVICE_SECRET);
    req.serviceContext = payload;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid service token' });
  }
}
```

### Pattern 3: Distributed Caching

```typescript
// user-service/src/services/user-cache.ts
import { redisClient } from './redis-client';

export class UserCache {
  async get(userId: string) {
    const cached = await redisClient.get(`user:${userId}`);
    if (cached) {
      return JSON.parse(cached);
    }

    const user = await User.findByPk(userId);
    await this.set(userId, user);
    return user;
  }

  async set(userId: string, user: any) {
    await redisClient.setex(
      `user:${userId}`,
      300, // 5 minute TTL
      JSON.stringify(user)
    );
  }

  async invalidate(userId: string) {
    await redisClient.del(`user:${userId}`);
  }
}
```

### Pattern 4: Event-Driven Communication

```typescript
// payment-service/src/routes/payments.ts
import { queueClient } from '../services/queue-client';

router.post('/charge', async (req, res) => {
  const payment = await Payment.create(req.body);

  // Queue async tasks
  await queueClient.add('payment-processed', {
    paymentId: payment.id,
    userId: payment.userId,
    amount: payment.amount,
  });

  res.json(payment);
});

// notification-service/src/jobs/payment-notifications.ts
import { queueClient } from '../services/queue-client';
import { emailClient } from '../services/email-client';

queueClient.process('payment-processed', async (job) => {
  const { paymentId, userId, amount } = job.data;

  // Send receipt
  await emailClient.send({
    to: user.email,
    subject: 'Payment Receipt',
    template: 'payment-receipt',
    data: { paymentId, amount },
  });

  // Send SMS if enabled
  if (user.smsNotifications) {
    await smsClient.send({
      to: user.phone,
      body: `Payment of $${amount} processed successfully`,
    });
  }
});
```

### Pattern 5: Circuit Breaker

```typescript
// gateway/src/services/circuit-breaker.ts
import { redisClient } from './redis-client';

export class CircuitBreaker {
  constructor(private serviceName: string) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    const state = await this.getState();

    if (state === 'open') {
      throw new Error(`Circuit breaker open for ${this.serviceName}`);
    }

    try {
      const result = await fn();
      await this.recordSuccess();
      return result;
    } catch (error) {
      await this.recordFailure();
      throw error;
    }
  }

  private async getState(): Promise<'open' | 'closed'> {
    const failures = await redisClient.get(`cb:${this.serviceName}:failures`);
    return parseInt(failures || '0') >= 5 ? 'open' : 'closed';
  }

  private async recordFailure() {
    const key = `cb:${this.serviceName}:failures`;
    await redisClient.incr(key);
    await redisClient.expire(key, 60); // Reset after 1 minute
  }

  private async recordSuccess() {
    await redisClient.del(`cb:${this.serviceName}:failures`);
  }
}
```

## Real-World Scenarios

### Scenario 1: Adding a New Microservice

**Goal**: Add a "Billing Service" for invoice generation

**Step 1**: Update workflow

```yaml
# Add to stack:
billing-service:
  type: backend
  technology: typescript/express
  port: 3004
  expected_dependencies:
    - service: redis
      usage: Invoice caching
      required: true
    - service: queue
      usage: Async invoice generation
      required: true
    - service: email
      usage: Send invoices to customers
      required: true
```

**Step 2**: Generate service

```bash
bang generate api-platform.workflow.yaml
```

This creates:
- `billing-service/` directory with full service structure
- Redis, Queue, and Email clients pre-configured
- Docker configuration updated
- Kubernetes manifests updated

**Step 3**: Implement business logic

```typescript
// billing-service/src/routes/invoices.ts
import { queueClient } from '../services/queue-client';

router.post('/invoices', async (req, res) => {
  const invoice = await Invoice.create(req.body);

  await queueClient.add('generate-pdf', {
    invoiceId: invoice.id,
  });

  res.json(invoice);
});
```

**Step 4**: Update gateway routing

```typescript
// gateway/src/routes/index.ts
app.use('/billing', proxy('http://billing-service:3004'));
```

### Scenario 2: Multi-Tenant Isolation

**Goal**: Ensure tenant A cannot access tenant B's data

```typescript
// gateway/src/middleware/tenant-context.ts
import { redisClient } from '../services/redis-client';

export async function extractTenant(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  // Check cache first
  let tenant = await redisClient.get(`apikey:${apiKey}`);

  if (!tenant) {
    tenant = await Tenant.findOne({ where: { apiKey } });
    await redisClient.setex(`apikey:${apiKey}`, 3600, JSON.stringify(tenant));
  } else {
    tenant = JSON.parse(tenant);
  }

  if (!tenant) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  // Add tenant context to all downstream requests
  req.headers['x-tenant-id'] = tenant.id;
  next();
}

// user-service/src/middleware/tenant-filter.ts
export function tenantFilter(req, res, next) {
  const tenantId = req.headers['x-tenant-id'];

  // Add tenant filter to all queries
  req.queryOptions = {
    where: { tenantId },
  };

  next();
}

// user-service/src/routes/users.ts
router.get('/users', async (req, res) => {
  // Automatically filtered by tenant
  const users = await User.findAll(req.queryOptions);
  res.json(users);
});
```

### Scenario 3: Handling Service Failures

**Goal**: Graceful degradation when a service is down

```typescript
// gateway/src/routes/health.ts
import axios from 'axios';

const services = [
  { name: 'auth', url: 'http://auth-service:3001/health' },
  { name: 'user', url: 'http://user-service:3002/health' },
  { name: 'payment', url: 'http://payment-service:3003/health' },
];

router.get('/health', async (req, res) => {
  const health = await Promise.all(
    services.map(async (service) => {
      try {
        await axios.get(service.url, { timeout: 1000 });
        return { name: service.name, status: 'healthy' };
      } catch (error) {
        return { name: service.name, status: 'unhealthy' };
      }
    })
  );

  const allHealthy = health.every(s => s.status === 'healthy');
  res.status(allHealthy ? 200 : 503).json({ services: health });
});

// gateway/src/middleware/service-fallback.ts
export function fallbackHandler(serviceName: string) {
  return async (req, res, next) => {
    try {
      await next();
    } catch (error) {
      if (error.code === 'ECONNREFUSED') {
        // Service is down, return cached response if available
        const cached = await redisClient.get(`fallback:${req.path}`);
        if (cached) {
          return res.json({
            data: JSON.parse(cached),
            warning: `${serviceName} is temporarily unavailable`,
          });
        }
      }
      throw error;
    }
  };
}
```

## Version Management

### Managing Breaking Changes

```bash
# Current version: 1.0.0
# All services using Redis for sessions

# Change: Move sessions to dedicated service
# This is a breaking change

# 1. Create feature branch
git checkout -b feature/session-service

# 2. Add session-service to workflow
vim api-platform.workflow.yaml

# 3. Verify current state
bang verify api-platform.workflow.yaml
# Shows: All services currently using Redis for sessions

# 4. Generate new service
bang generate api-platform.workflow.yaml

# 5. Migrate services gradually
# - Update auth-service first
# - Test thoroughly
# - Update user-service
# - etc.

# 6. Create new major version
bang version create 2.0.0 -w api-platform.workflow.yaml

# 7. Compare versions
bang version diff 1.0.0 2.0.0
# Shows: Added session-service, modified all services
```

### Rolling Back Service Updates

```bash
# Version 2.1.0 introduced a bug in payment-service

# 1. Check recent versions
bang version list

# 2. Compare to stable version
bang version diff 2.0.0 2.1.0

# 3. Rollback
bang version rollback 2.0.0 --yes

# 4. Redeploy stable version
kubectl rollout restart deployment payment-service
```

## Deployment

### Development

```bash
docker-compose up -d
npm run dev:all
```

### Staging

```bash
# Deploy all services
kubectl apply -f k8s/staging/

# Or deploy specific service
kubectl apply -f k8s/staging/payment-service.yaml

# Check status
kubectl get pods -n api-platform
```

### Production

```bash
# Create production version
bang version create 3.0.0 -w api-platform.workflow.yaml

# Deploy with zero-downtime
kubectl set image deployment/payment-service \
  payment-service=myregistry/payment-service:3.0.0

# Monitor rollout
kubectl rollout status deployment/payment-service
```

## Monitoring

### Service Health

```bash
# Check all services
curl http://localhost:3000/health

# Check specific service
curl http://localhost:3001/health
```

### Rate Limit Monitoring

```bash
# Redis CLI
redis-cli
> KEYS rate-limit:*
> GET rate-limit:tenant-123:/api/users
```

### Queue Status

```bash
# View queue metrics
cd notification-service
npm run queue:status

# View failed jobs
npm run queue:failed
```

## Project Structure

```
api-platform/
├── api-platform.workflow.yaml
├── gateway/                      # API Gateway
│   ├── src/
│   │   ├── middleware/
│   │   │   ├── rate-limit.ts
│   │   │   └── tenant-context.ts
│   │   ├── services/
│   │   │   ├── redis-client.ts   # Generated
│   │   │   └── analytics-client.ts
│   │   └── index.ts
│   └── package.json
├── auth-service/                 # Authentication
│   ├── src/
│   │   ├── services/
│   │   │   ├── redis-client.ts   # Generated
│   │   │   └── email-client.ts   # Generated
│   │   └── index.ts
│   └── package.json
├── user-service/                 # User Management
├── payment-service/              # Payments
├── notification-service/         # Notifications (Worker)
├── analytics-worker/             # Analytics Processing
├── docker-compose.yml
├── k8s/                         # Kubernetes manifests
│   ├── staging/
│   └── production/
└── .bangstack/
    └── manifest.json
```

## Learn More

- [E-commerce Example](../ecommerce-api/)
- [SaaS Full-Stack Example](../saas-fullstack/)
- [Microservices Patterns](https://microservices.io/patterns/)
- [Version Management](../../README.md#version-management)

## Next Steps

1. Explore each microservice's generated code
2. Try adding a new service
3. Implement service-to-service communication
4. Set up monitoring and logging
5. Deploy to Kubernetes cluster
