# 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.
