> ## Documentation Index
> Fetch the complete documentation index at: https://system.muzemus.online/llms.txt
> Use this file to discover all available pages before exploring further.

# Data scoping

> How the ScopeGuard and service-layer filtering enforce data boundaries

MUZE enforces data boundaries at two levels: `ScopeGuard` at the URL level, and explicit scope filters in each service method at the query level. This page shows both patterns.

## ScopeGuard (URL level)

The `ScopeGuard` runs after `PermissionsGuard` and before the controller handler. It reads `@RequireScope(...)` metadata from the route.

### Configuration

```typescript theme={null}
@RequireScope({ storeIdParam: 'storeId' })
@Get('stores/:storeId/employees')
async getStoreEmployees(
  @Param('storeId') storeId: string,
) { ... }
```

### Scope resolution logic

```mermaid theme={null}
flowchart TD
    A[ScopeGuard activated] --> B{Route has @RequireScope?}
    B -->|No| C[Pass through]
    B -->|Yes| D{Is admin role?}
    D -->|Yes| C
    D -->|No| E{Store Manager?}
    E -->|Yes| F{storeId in URL matches assigned store?}
    F -->|Yes| C
    F -->|No| G[403 Forbidden]
    E -->|No| H{HR?}
    H -->|Yes| I{Store belongs to assigned client?}
    I -->|Yes| C
    I -->|No| G
    H -->|No| G
```

## Service-layer scoping (query level)

Every service method that reads data applies explicit scope filters. This is not automated; it is a convention enforced through code review.

### Store-scoped queries

```typescript theme={null}
async getStoreEmployees(request: RequestWithPermissions, storeId: string) {
  if (!request.isAdmin) {
    const store = await this.prisma.store.findUnique({ where: { id: storeId } });
    const canAccess = request.userRoles.some(r =>
      (r.role === 'STORE_MANAGER' && r.storeId === storeId) ||
      (r.role === 'HR' && r.clientId === store?.clientId)
    );
    if (!canAccess) throw new ForbiddenException('Access denied');
  }

  return this.prisma.employee.findMany({ where: { storeId } });
}
```

### Client-scoped queries

```typescript theme={null}
async getOrders(request: RequestWithPermissions) {
  const where: Prisma.OrderWhereInput = {};

  if (!request.isAdmin) {
    const hrClientIds = request.userRoles
      .filter(r => r.role === 'HR')
      .map(r => r.clientId)
      .filter(Boolean);

    if (hrClientIds.length > 0) {
      where.employee = { store: { clientId: { in: hrClientIds } } };
    } else {
      const storeIds = request.userRoles
        .filter(r => r.role === 'STORE_MANAGER')
        .map(r => r.storeId)
        .filter(Boolean);
      where.employee = { storeId: { in: storeIds } };
    }
  }

  return this.prisma.order.findMany({ where });
}
```

## Scope propagation through relations

When querying across relations, scope must be maintained. A store-scoped order query follows the chain `Order → Employee → Store → clientId`. The service resolves this chain so cross-client data cannot leak through joined queries.

## Client narrowing

Client-scoped endpoints accept an optional `clientId` query parameter, resolved by `resolveRequestedClientId` in `backend/src/auth/user-scope.ts`. The parameter can only narrow a query:

* Admin: filters to the requested client
* HR: honoured only when inside the assigned client; otherwise ignored (role scope is authoritative)
* Store Manager: ignored entirely; store scope is the only filter

The resolved filter is always combined with the existing scope filter, so a request parameter can never weaken role or store scope.
