# RLS Context Usage Guide This guide explains how to use the RLS (Row-Level Security) context system in services. ## Overview The RLS context system automatically sets PostgreSQL session variables for authenticated requests: - `app.current_user_id` - Set from the authenticated user - `app.current_workspace_id` - Set from the workspace context (if present) These session variables enable PostgreSQL RLS policies to automatically filter queries based on user permissions. ## How It Works 1. **RlsContextInterceptor** runs after AuthGuard and WorkspaceGuard 2. It wraps the request in a Prisma transaction (30s timeout, 10s max wait for connection) 3. Inside the transaction, it executes `SET LOCAL` to set session variables 4. The transaction client is propagated via AsyncLocalStorage 5. Services access it using `getRlsClient()` ### Transaction Timeout The interceptor configures a 30-second transaction timeout and 10-second max wait for connection acquisition. This supports: - File uploads - Complex queries with joins - Bulk operations - Report generation If you need longer-running operations, consider moving them to background jobs instead of synchronous HTTP requests. ## Usage in Services ### Basic Pattern ```typescript import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { getRlsClient } from "../prisma/rls-context.provider"; @Injectable() export class TasksService { constructor(private readonly prisma: PrismaService) {} async findAll(workspaceId: string) { // Use RLS client if available, otherwise fall back to standard client const client = getRlsClient() ?? this.prisma; // This query is automatically filtered by RLS policies return client.task.findMany({ where: { workspaceId }, }); } } ``` ### Why Use This Pattern? **With RLS context:** - Queries are automatically filtered by user/workspace permissions - Defense in depth: Even if application logic fails, database RLS enforces security - No need to manually add `where` clauses for user/workspace filtering **Fallback to standard client:** - Supports unauthenticated routes (public endpoints) - Supports system operations that need full database access - Graceful degradation if RLS context isn't set ### Advanced: Explicit Transaction Control For operations that need multiple queries in a single transaction: ```typescript async createWithRelations(workspaceId: string, data: CreateTaskDto) { const client = getRlsClient() ?? this.prisma; // If using RLS client, we're already in a transaction // If not, we need to create one if (getRlsClient()) { // Already in a transaction with RLS context return this.performCreate(client, data); } else { // Need to manually wrap in transaction return this.prisma.$transaction(async (tx) => { return this.performCreate(tx, data); }); } } private async performCreate(client: PrismaClient, data: CreateTaskDto) { const task = await client.task.create({ data }); await client.activity.create({ data: { type: "TASK_CREATED", taskId: task.id, }, }); return task; } ``` ## Unauthenticated Routes For public endpoints (no AuthGuard), `getRlsClient()` returns `undefined`: ```typescript @Get("public/stats") async getPublicStats() { // No RLS context - uses standard Prisma client const client = getRlsClient() ?? this.prisma; // This query has NO RLS filtering (public data) return client.task.count(); } ``` ## Testing When testing services, you can mock the RLS context: ```typescript import { vi } from "vitest"; import * as rlsContext from "../prisma/rls-context.provider"; describe("TasksService", () => { it("should use RLS client when available", () => { const mockClient = {} as PrismaClient; vi.spyOn(rlsContext, "getRlsClient").mockReturnValue(mockClient); // Service will use mockClient instead of prisma }); }); ``` ## Security Considerations 1. **Always use the pattern**: `getRlsClient() ?? this.prisma` 2. **Don't bypass RLS** unless absolutely necessary (e.g., system operations) 3. **Trust the interceptor**: It sets context automatically - no manual setup needed 4. **Test with and without RLS**: Ensure services work in both contexts ## Architecture ``` Request → AuthGuard → WorkspaceGuard → RlsContextInterceptor → Service ↓ Prisma.$transaction ↓ SET LOCAL app.current_user_id SET LOCAL app.current_workspace_id ↓ AsyncLocalStorage ↓ Service (getRlsClient()) ``` ## Related Files - `/apps/api/src/common/interceptors/rls-context.interceptor.ts` - Main interceptor - `/apps/api/src/prisma/rls-context.provider.ts` - AsyncLocalStorage provider - `/apps/api/src/lib/db-context.ts` - Legacy RLS utilities (reference only) - `/apps/api/src/prisma/prisma.service.ts` - Prisma service with RLS helpers ## Migration from Legacy Pattern If you're migrating from the legacy `withUserContext()` pattern: **Before:** ```typescript return withUserContext(userId, async (tx) => { return tx.task.findMany({ where: { workspaceId } }); }); ``` **After:** ```typescript const client = getRlsClient() ?? this.prisma; return client.task.findMany({ where: { workspaceId } }); ``` The interceptor handles transaction management automatically, so you no longer need to wrap every query.