Claude Code Plugins

Community-maintained marketplace

Feedback

Blueprint Multi-Tenancy Integration

@tw-lin/ng-lin
0
0

Integrate the Blueprint multi-tenancy pattern into new features and modules. Use this skill when adding Blueprint-aware functionality, implementing BlueprintMember access control, handling Blueprint ownership (User vs Organization), enforcing resource isolation, and integrating with BlueprintEventBus. Ensures proper multi-tenant architecture where Blueprint defines permission boundaries and all resources respect Blueprint context.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Blueprint Multi-Tenancy Integration
description Integrate the Blueprint multi-tenancy pattern into new features and modules. Use this skill when adding Blueprint-aware functionality, implementing BlueprintMember access control, handling Blueprint ownership (User vs Organization), enforcing resource isolation, and integrating with BlueprintEventBus. Ensures proper multi-tenant architecture where Blueprint defines permission boundaries and all resources respect Blueprint context.
license MIT

Blueprint Multi-Tenancy Integration Skill

This skill helps integrate Blueprint multi-tenancy patterns into features and modules.

Blueprint System Overview

Core Concept

Blueprint is a Permission Boundary, NOT a Data Boundary

  • Blueprint defines WHO can access WHAT resources
  • Resources belong to Blueprints
  • Users access resources via BlueprintMember role + permissions
  • Owner can be User or Organization

Entity Hierarchy

User ─┐
      ├─→ Blueprint ─→ Resources (Tasks, Files, etc.)
Organization ─┘
      ├─→ Team
      └─→ Partner

Key Entities

interface Blueprint {
  id: string;
  name: string;
  ownerType: 'user' | 'organization';
  ownerId: string;
  createdAt: Date;
  updatedAt: Date;
}

interface BlueprintMember {
  id: string; // {userId}_{blueprintId}
  blueprintId: string;
  userId: string;
  memberType: 'user' | 'team' | 'partner';
  role: 'owner' | 'admin' | 'member' | 'viewer';
  permissions: string[]; // ['task:create', 'task:update', ...]
  status: 'active' | 'suspended' | 'revoked';
  createdAt: Date;
  updatedAt: Date;
}

Integration Patterns

1. Resource with Blueprint Context

All resources MUST include Blueprint reference:

interface Task {
  id: string;
  blueprintId: string; // ✅ REQUIRED
  title: string;
  description: string;
  status: 'pending' | 'in-progress' | 'completed';
  assignedTo?: string; // User, Team, or Partner ID
  assignedToType?: 'user' | 'team' | 'partner';
  createdAt: Date;
  updatedAt: Date;
  deletedAt: Date | null;
}

2. Repository Queries with Blueprint Filter

@Injectable({ providedIn: 'root' })
export class TaskRepository extends FirestoreBaseRepository<Task> {
  protected collectionName = 'tasks';
  
  /**
   * ✅ CORRECT: Always filter by blueprintId
   */
  async findByBlueprintId(blueprintId: string): Promise<Task[]> {
    return this.executeWithRetry(async () => {
      const q = query(
        collection(this.firestore, this.collectionName),
        where('blueprint_id', '==', blueprintId),
        where('deleted_at', '==', null),
        orderBy('created_at', 'desc')
      );
      return this.queryDocuments(q);
    });
  }
  
  /**
   * ✅ CORRECT: Include blueprintId in creation
   */
  async create(blueprintId: string, task: Omit<Task, 'id'>): Promise<Task> {
    return this.executeWithRetry(async () => {
      const taskWithBlueprint = {
        ...task,
        blueprintId,
        createdAt: new Date(),
        updatedAt: new Date(),
        deletedAt: null
      };
      return this.createDocument(taskWithBlueprint);
    });
  }
}

3. Service with Blueprint Context

@Injectable({ providedIn: 'root' })
export class TaskService {
  private taskRepository = inject(TaskRepository);
  private blueprintMemberRepository = inject(BlueprintMemberRepository);
  private eventBus = inject(BlueprintEventBus);
  
  /**
   * Get tasks for specific Blueprint
   */
  async getTasks(blueprintId: string): Promise<Task[]> {
    // Validate user has access to Blueprint
    await this.validateBlueprintAccess(blueprintId);
    
    return await this.taskRepository.findByBlueprintId(blueprintId);
  }
  
  /**
   * Create task in Blueprint context
   */
  async createTask(
    blueprintId: string,
    task: Omit<Task, 'id' | 'blueprintId'>
  ): Promise<Task> {
    // Validate user has permission
    await this.validatePermission(blueprintId, 'task:create');
    
    // Create task with Blueprint context
    const created = await this.taskRepository.create(blueprintId, task);
    
    // Publish Blueprint event
    this.eventBus.publish({
      type: 'task.created',
      blueprintId,
      timestamp: new Date(),
      actor: this.getCurrentUserId(),
      data: created
    });
    
    return created;
  }
  
  private async validateBlueprintAccess(blueprintId: string): Promise<void> {
    const userId = this.getCurrentUserId();
    const member = await this.blueprintMemberRepository.findByUserAndBlueprint(
      userId,
      blueprintId
    );
    
    if (!member || member.status !== 'active') {
      throw new Error('Access denied to Blueprint');
    }
  }
  
  private async validatePermission(
    blueprintId: string,
    permission: string
  ): Promise<void> {
    const userId = this.getCurrentUserId();
    const member = await this.blueprintMemberRepository.findByUserAndBlueprint(
      userId,
      blueprintId
    );
    
    if (!member || member.status !== 'active') {
      throw new Error('Access denied to Blueprint');
    }
    
    if (!member.permissions.includes(permission)) {
      throw new Error(`Missing permission: ${permission}`);
    }
  }
}

4. Component with Blueprint Context

@Component({
  selector: 'app-task-list',
  standalone: true,
  imports: [SHARED_IMPORTS],
  template: `
    <div class="task-list">
      <h2>Tasks for {{ blueprintName() }}</h2>
      
      @if (loading()) {
        <nz-spin nzSimple />
      } @else {
        @for (task of tasks(); track task.id) {
          <app-task-item [task]="task" />
        } @empty {
          <nz-empty />
        }
      }
    </div>
  `
})
export class TaskListComponent {
  private taskService = inject(TaskService);
  private blueprintService = inject(BlueprintService);
  
  // ✅ Blueprint context from input or route
  blueprintId = input.required<string>();
  
  loading = signal(false);
  tasks = signal<Task[]>([]);
  blueprintName = signal<string>('');
  
  constructor() {
    effect(() => {
      const id = this.blueprintId();
      this.loadBlueprint(id);
      this.loadTasks(id);
    });
  }
  
  async loadBlueprint(blueprintId: string): Promise<void> {
    const blueprint = await this.blueprintService.getBlueprint(blueprintId);
    this.blueprintName.set(blueprint.name);
  }
  
  async loadTasks(blueprintId: string): Promise<void> {
    this.loading.set(true);
    try {
      const tasks = await this.taskService.getTasks(blueprintId);
      this.tasks.set(tasks);
    } finally {
      this.loading.set(false);
    }
  }
}

Ownership Patterns

User-Owned Blueprint

// When ownerType = 'user'
interface Blueprint {
  ownerType: 'user';
  ownerId: string; // User ID
}

// Members can only be:
// - Users (collaborators)
interface BlueprintMember {
  memberType: 'user';
  userId: string;
}

Organization-Owned Blueprint

// When ownerType = 'organization'
interface Blueprint {
  ownerType: 'organization';
  ownerId: string; // Organization ID
}

// Members can be:
// - Organization members
// - Teams (sub-accounts)
// - Partners (external relations)
interface BlueprintMember {
  memberType: 'user' | 'team' | 'partner';
  userId?: string; // If memberType = 'user'
  teamId?: string; // If memberType = 'team'
  partnerId?: string; // If memberType = 'partner'
}

Permission System

Role + Permissions Model

// Roles provide base permissions
type Role = 'owner' | 'admin' | 'member' | 'viewer';

// Permissions are granular capabilities
type Permission = 
  | 'task:create' 
  | 'task:read' 
  | 'task:update' 
  | 'task:delete'
  | 'file:upload'
  | 'file:download'
  | 'member:invite'
  | 'member:remove';

// BlueprintMember combines both
interface BlueprintMember {
  role: Role; // High-level access
  permissions: Permission[]; // Granular capabilities
}

Checking Permissions

// In Service
async checkPermission(
  blueprintId: string,
  permission: Permission
): Promise<boolean> {
  const member = await this.getMember(blueprintId);
  return member?.permissions.includes(permission) ?? false;
}

// In Component (UI-level check)
canCreateTask = computed(() => {
  const member = this.currentMember();
  return member?.permissions.includes('task:create') ?? false;
});

Security Rules Integration

Blueprint-Aware Rules

// Firestore Security Rules
match /tasks/{taskId} {
  // Validate Blueprint membership
  allow read: if isAuthenticated() && 
                 isBlueprintMember(resource.data.blueprint_id);
  
  // Validate permissions
  allow create: if isAuthenticated() && 
                   isBlueprintMember(request.resource.data.blueprint_id) &&
                   hasPermission(request.resource.data.blueprint_id, 'task:create');
}

// Helper functions
function isBlueprintMember(blueprintId) {
  let memberId = request.auth.uid + '_' + blueprintId;
  return exists(/databases/$(database)/documents/blueprintMembers/$(memberId));
}

function hasPermission(blueprintId, permission) {
  let memberId = request.auth.uid + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  return permission in member.data.permissions &&
         member.data.status == 'active';
}

Event-Driven Integration

Publishing Blueprint Events

// When task changes in Blueprint context
this.eventBus.publish({
  type: 'task.created', // Domain event
  blueprintId: task.blueprintId, // ✅ Blueprint context
  timestamp: new Date(),
  actor: this.getCurrentUserId(),
  data: task
});

Subscribing to Blueprint Events

// Subscribe to events in specific Blueprint
this.eventBus.subscribe('task.created')
  .pipe(
    filter(event => event.blueprintId === this.blueprintId()),
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(event => {
    console.log('Task created in current Blueprint:', event.data);
    this.refreshTasks();
  });

Testing Blueprint Integration

describe('Task Service with Blueprint', () => {
  it('should filter tasks by blueprint', async () => {
    const blueprint1Tasks = await service.getTasks('blueprint1');
    const blueprint2Tasks = await service.getTasks('blueprint2');
    
    expect(blueprint1Tasks.every(t => t.blueprintId === 'blueprint1')).toBe(true);
    expect(blueprint2Tasks.every(t => t.blueprintId === 'blueprint2')).toBe(true);
  });
  
  it('should reject access without membership', async () => {
    // User not member of blueprint
    await expectAsync(
      service.getTasks('blueprint3')
    ).toBeRejectedWithError('Access denied to Blueprint');
  });
  
  it('should reject action without permission', async () => {
    // User has read but not create permission
    await expectAsync(
      service.createTask('blueprint1', { title: 'Test' })
    ).toBeRejectedWithError('Missing permission: task:create');
  });
});

Checklist

When integrating Blueprint pattern:

  • All resources include blueprintId
  • Repository queries filter by Blueprint
  • Service validates Blueprint access
  • Service checks granular permissions
  • Components receive Blueprint context
  • Events include blueprintId
  • Security Rules validate membership
  • Security Rules check permissions
  • Tests cover multi-tenancy isolation
  • Tests cover permission enforcement
  • No cross-Blueprint data leakage

References