@helix/shared/middleware
Shared NestJS middleware for the Helix platform.
Overview
This library provides four essential middleware components for all Helix microservices:
- Authentication Middleware - JWT validation with WorkOS
- Tenant Context Middleware - Tenant resolution and caching
- Request Logging Middleware - Request ID generation and timing
- Rate Limiting Middleware - Tenant-based throttling
Installation
This is an internal library in the Nx monorepo. Import using the TypeScript path alias:
import {
AuthenticationMiddleware,
TenantContextMiddleware,
RequestLoggingMiddleware,
RateLimitMiddleware,
} from '@helix/shared/middleware';
Middleware Order
CRITICAL: Middleware must be applied in this exact order:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
RequestLoggingMiddleware, // 1. Generate request ID
AuthenticationMiddleware, // 2. Validate JWT, extract user
TenantContextMiddleware, // 3. Resolve tenant context
RateLimitMiddleware, // 4. Check rate limits
)
.forRoutes('*'); // Apply to all routes except health checks
}
}
Why This Order?
- Logging first - Generates request ID for tracing
- Auth second - Validates user, extracts role from JWT
- Tenant third - Resolves tenant (needs user context)
- Rate limit last - Throttles based on tenant+user+role
1. Authentication Middleware
Validates JWT tokens from WorkOS and extracts user context.
Features
- JWT validation against WorkOS
- Token expiry checking
- User ID and role extraction from JWT
- Error handling for invalid tokens
WorkOS RBAC Integration
IMPORTANT: Roles and scopes are extracted from WorkOS JWT tokens, NOT from our databases.
// JWT payload from WorkOS contains:
{
sub: 'user_01HXYZ',
tenantId: 'tenant_abc',
role: 'tenant_admin', // From WorkOS RBAC
scopes: ['tenant:write', ...], // From WorkOS RBAC
email: 'user@example.com',
name: 'John Doe'
}
// Middleware extracts and attaches to request:
req.user = {
userId: 'user_01HXYZ',
tenantId: 'tenant_abc',
role: 'tenant_admin', // NOT from database
scopes: ['tenant:write', ...], // NOT from database
email: 'user@example.com',
displayName: 'John Doe'
};
Usage
import { AuthenticationMiddleware } from '@helix/shared/middleware';
@Module({
providers: [
{
provide: 'WORKOS_CLIENT',
useFactory: () => {
return new WorkOS(process.env.WORKOS_API_KEY);
},
},
AuthenticationMiddleware,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthenticationMiddleware)
.forRoutes('*');
}
}
Error Responses
// Missing token
{
statusCode: 401,
message: 'No authorization token provided',
error: 'Unauthorized'
}
// Invalid/expired token
{
statusCode: 401,
message: 'Invalid or expired token',
error: 'Unauthorized'
}
2. Tenant Context Middleware
Resolves tenant information and caches it using Redis.
Features
- Verifies user has access to tenant
- Loads tenant display information
- Redis caching (30-minute TTL)
- Multi-tenant support
WorkOS RBAC Alignment
CRITICAL ARCHITECTURAL CHANGE: This middleware does NOT query for user roles.
// OLD (incorrect): Query role from database
const membership = await prisma.tenantMember.findFirst({
where: { workosUserId, tenantId },
select: { role: true } // ❌ NO! Role not in database
});
// NEW (correct): Role already in req.user from JWT
req.user.role; // ✅ From WorkOS JWT token
// Middleware only loads tenant information
req.tenant = {
tenantId: 'tenant_abc',
tenantName: 'Acme Corp',
tenantDomain: 'acme.com',
tenantStatus: 'active',
tenantSettings: { ... }
// NO role - comes from JWT
};
Usage
import { TenantContextMiddleware } from '@helix/shared/middleware';
@Module({
providers: [
TenantContextMiddleware,
CentralPrismaService,
RedisService,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
AuthenticationMiddleware,
TenantContextMiddleware, // After auth
)
.forRoutes('*');
}
}
Cache Management
// Clear cache for a user
await tenantContextMiddleware.clearCache(userId, tenantId);
// Clear all caches for a tenant (when tenant info changes)
await tenantContextMiddleware.clearTenantCache(tenantId);
Error Responses
// No user context (auth middleware not run)
{
statusCode: 403,
message: 'User context not found',
error: 'Forbidden'
}
// User doesn't have access to tenant
{
statusCode: 403,
message: 'User does not have access to this tenant',
error: 'Forbidden'
}
3. Request Logging Middleware
Generates unique request IDs and logs request/response details.
Features
- UUID generation for each request
- Request/response timing
- User and tenant context logging
- Sanitized query parameters
- X-Request-ID header in response
Usage
import { RequestLoggingMiddleware } from '@helix/shared/middleware';
@Module({
providers: [RequestLoggingMiddleware],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestLoggingMiddleware) // First middleware
.forRoutes('*');
}
}
Log Output
// Incoming request
{
"type": "request",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"path": "/api/users/123",
"query": {},
"userId": "user_01HXYZ",
"tenantId": "tenant_abc",
"ip": "192.168.1.1",
"userAgent": "Mozilla/5.0..."
}
// Outgoing response
{
"type": "response",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"path": "/api/users/123",
"statusCode": 200,
"duration": 45,
"userId": "user_01HXYZ",
"tenantId": "tenant_abc"
}
Accessing Request ID
// In a controller
@Get()
async getData(@Req() req: Request) {
const requestId = (req as any).requestId;
this.logger.log(`Processing request ${requestId}`);
}
// Request ID is also in response headers
// X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
4. Rate Limiting Middleware
Enforces tenant-based rate limits using Redis.
Features
- Tenant+user rate limiting
- Configurable limits by role (from JWT)
- Sliding window algorithm
- Standard rate limit headers
- Administrative controls
Default Limits
| Role | Requests/Minute |
|---|---|
| tenant_admin | 1000 |
| product_admin | 500 |
| user | 100 |
| read_only | 50 |
Usage
import { RateLimitMiddleware } from '@helix/shared/middleware';
// With default limits
@Module({
providers: [
{
provide: RateLimitMiddleware,
useFactory: (redis: Redis) => {
return new RateLimitMiddleware(redis);
},
inject: [RedisService],
},
],
})
export class AppModule {}
// With custom limits
@Module({
providers: [
{
provide: RateLimitMiddleware,
useFactory: (redis: Redis) => {
return new RateLimitMiddleware(redis, {
tenant_admin: { limit: 2000, window: 60 },
user: { limit: 200, window: 60 },
});
},
inject: [RedisService],
},
],
})
export class AppModule {}
Response Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705583640
Error Response
// Rate limit exceeded
{
statusCode: 429,
message: 'Rate limit exceeded. Limit: 100 requests per 60 seconds',
error: 'Too Many Requests',
retryAfter: 1705583640
}
Administrative Controls
// Get current rate limit status
const status = await rateLimitMiddleware.getRateLimitStatus(
tenantId,
userId,
role
);
// Returns: { count: 45, limit: 100, remaining: 55, resetTime: 1705583640 }
// Reset rate limit for a user
await rateLimitMiddleware.resetRateLimit(tenantId, userId);
Complete Example
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import {
AuthenticationMiddleware,
TenantContextMiddleware,
RequestLoggingMiddleware,
RateLimitMiddleware,
} from '@helix/shared/middleware';
import { WorkOS } from '@workos-inc/node';
import { PrismaService } from '@helix/prisma-central';
import { RedisService } from './redis.service';
@Module({
providers: [
// WorkOS client
{
provide: 'WORKOS_CLIENT',
useFactory: () => new WorkOS(process.env.WORKOS_API_KEY),
},
// Prisma
PrismaService,
// Redis
RedisService,
// Middleware
{
provide: AuthenticationMiddleware,
useFactory: (workos: WorkOS) => new AuthenticationMiddleware(workos),
inject: ['WORKOS_CLIENT'],
},
{
provide: TenantContextMiddleware,
useFactory: (prisma: PrismaService, redis: RedisService) =>
new TenantContextMiddleware(prisma, redis),
inject: [PrismaService, RedisService],
},
RequestLoggingMiddleware,
{
provide: RateLimitMiddleware,
useFactory: (redis: RedisService) =>
new RateLimitMiddleware(redis, {
tenant_admin: { limit: 2000, window: 60 },
user: { limit: 200, window: 60 },
}),
inject: [RedisService],
},
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
RequestLoggingMiddleware, // 1. Logging
AuthenticationMiddleware, // 2. Auth
TenantContextMiddleware, // 3. Tenant
RateLimitMiddleware, // 4. Rate limit
)
.exclude(
{ path: 'health', method: RequestMethod.GET }, // Skip health checks
)
.forRoutes('*');
}
}
Request Context Shape
After all middleware runs, requests have this structure:
interface EnhancedRequest extends Request {
// From RequestLoggingMiddleware
requestId: string;
// From AuthenticationMiddleware
user: {
userId: string; // WorkOS user ID
tenantId: string; // From JWT
role: UserRole; // From JWT (NOT database)
scopes: PermissionScope[]; // From JWT (NOT database)
email: string;
displayName: string;
};
// From TenantContextMiddleware
tenant: {
tenantId: string;
tenantName: string;
tenantDomain: string;
tenantStatus: TenantStatus;
tenantSettings: Record<string, any>;
// NO role - comes from JWT
};
}
WorkOS RBAC Architecture
Key Principles
- Roles managed by WorkOS - Never stored in our databases
- JWT contains everything - Role, scopes, tenant ID
- No role synchronization - WorkOS is single source of truth
- Faster authorization - No database queries for roles
- Better security - Roles in signed tokens
Authorization Flow
// 1. User authenticates with WorkOS
// 2. WorkOS returns JWT with role and scopes
// 3. AuthenticationMiddleware validates JWT
// 4. Role and scopes attached to req.user
// In your guards
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
// Role comes from JWT, not database!
return user.role === 'tenant_admin';
}
}
// Product access is SEPARATE (stored in Tenant DB)
const hasKiraAccess = await tenantDb.user_product_access.findFirst({
where: {
team_member: { workosUserId: user.userId },
product_type: 'kira',
revoked_at: null
}
});
Testing
describe('AuthenticationMiddleware', () => {
it('should validate JWT and attach user context', async () => {
// Test JWT validation
});
it('should reject invalid tokens', async () => {
// Test error handling
});
});
describe('TenantContextMiddleware', () => {
it('should resolve tenant from user ID', async () => {
// Test tenant resolution
});
it('should use Redis cache', async () => {
// Test caching behavior
});
});
describe('RequestLoggingMiddleware', () => {
it('should generate unique request IDs', () => {
// Test UUID generation
});
it('should log request timing', () => {
// Test timing logs
});
});
describe('RateLimitMiddleware', () => {
it('should enforce rate limits', async () => {
// Test rate limiting
});
it('should set rate limit headers', async () => {
// Test headers
});
});
Related Libraries
@helix/shared/types- Type definitions@helix/shared/interfaces- Interface definitions- Central DB client (see Database & Prisma)
- Tenant DB client (see Database & Prisma)
Development
# Build the library
nx build shared-middleware
# Lint the library
nx lint shared-middleware
# Test the library
nx test shared-middleware
License
Proprietary - CleverChain Limited