CentercodePorygon
DittoPorygonAPI

Scope Model

User, program, and project scope isolation

SafeReliable

Overview

Implement scope-based data isolation. User scope aggregates across programs, program scope is fully isolated, project scope exists within programs.

What it is

Three-level scope hierarchy (User → Program → Project) with strict data isolation between programs.

Why we use it

Data security, multi-tenant isolation, and clear boundaries between testing programs.

When to use

All data queries must include scope filters. Programs NEVER share data.

Key Features

  • User scope for cross-program aggregation
  • Program scope for full isolation
  • Project scope within programs
  • Scope enforcement in all queries

Quick Start

Scope-Aware Query

Always include scope in database queries.

// Scope hierarchy
User Scope     → Aggregated view across all programs for a user
  └── Program Scope  → Independent testing program (isolated)
        └── Project Scope → Validation effort within a program

// CRITICAL: Data NEVER crosses program boundaries
// Each program is fully isolated

// Example: Scope-aware query
import { prisma } from '@/lib/db';

// Get items for a specific program (scoped query)
const items = await prisma.item.findMany({
  where: {
    programId: currentProgramId, // Always filter by program
  },
});

Patterns

User Scope

Aggregate data across programs for a user.

// User Scope - Cross-program aggregation for dashboard

// User scope shows aggregated data across all programs
// the user has access to

export async function getUserDashboard(userId: string) {
  // Get all programs user belongs to
  const memberships = await prisma.programMember.findMany({
    where: { userId },
    include: { program: true },
  });

  // Aggregate stats across programs
  const stats = await Promise.all(
    memberships.map(async ({ program }) => ({
      programId: program.id,
      programName: program.name,
      itemCount: await prisma.item.count({
        where: { programId: program.id },
      }),
      activeProjects: await prisma.project.count({
        where: { programId: program.id, status: 'active' },
      }),
    }))
  );

  return { userId, programs: stats };
}

Program Scope

Fully isolated program data.

// Program Scope - Fully isolated data

// Each program is a complete silo
// NO data sharing between programs

export async function getProgramData(programId: string, userId: string) {
  // First verify user has access to this program
  const membership = await prisma.programMember.findUnique({
    where: {
      userId_programId: { userId, programId },
    },
  });

  if (!membership) {
    throw new AuthorizationError('Access denied to this program');
  }

  // All queries MUST filter by programId
  const items = await prisma.item.findMany({
    where: { programId }, // REQUIRED
  });

  const projects = await prisma.project.findMany({
    where: { programId }, // REQUIRED
  });

  return { items, projects };
}

Project Scope

Project data within program context.

// Project Scope - Within program context

// Projects exist within a program
// Project data inherits program isolation

export async function getProjectDetails(
  projectId: string,
  programId: string,
  userId: string
) {
  // Verify program access first
  await verifyProgramAccess(userId, programId);

  // Project must belong to the program
  const project = await prisma.project.findFirst({
    where: {
      id: projectId,
      programId, // Enforce program boundary
    },
    include: {
      items: true,
      feedback: true,
    },
  });

  if (!project) {
    throw new NotFoundError('Project not found in this program');
  }

  return project;
}

Watch Out

Cross-program data leakage

Don't

// Cross-program data leakage - DANGEROUS
export async function getItem(itemId: string) {
  // Missing program filter - can access any program's data!
  const item = await prisma.item.findUnique({
    where: { id: itemId },
  });

  return item; // Could be from a different program
}

Do

// Properly scoped query
export async function getItem(itemId: string, programId: string) {
  // Always include program in where clause
  const item = await prisma.item.findFirst({
    where: {
      id: itemId,
      programId, // Enforce program boundary
    },
  });

  if (!item) {
    throw new NotFoundError('Item not found');
  }

  return item;
}

Missing scope filters on queries

Don't

// Missing scope filter on list query
export async function getAllFeedback() {
  // Returns ALL feedback across ALL programs!
  const feedback = await prisma.feedback.findMany({
    orderBy: { createdAt: 'desc' },
  });

  return feedback;
}

Do

// Properly scoped list query
export async function getProgramFeedback(programId: string) {
  // Only returns feedback for the specified program
  const feedback = await prisma.feedback.findMany({
    where: { programId },
    orderBy: { createdAt: 'desc' },
  });

  return feedback;
}
  • Incorrect scope inheritance
  • Aggregation queries leaking data

Related

Authorization

Permission checks

Prisma ORM

Database queries

Feature Structure

Feature module structure