| 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;
blueprintId: string;
userId: string;
memberType: 'user' | 'team' | 'partner';
role: 'owner' | 'admin' | 'member' | 'viewer';
permissions: string[];
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;
title: string;
description: string;
status: 'pending' | 'in-progress' | 'completed';
assignedTo?: string;
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';
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);
});
}
async create(blueprintId: string, task: Omit<, >): <> {
.( () => {
taskWithBlueprint = {
...task,
blueprintId,
: (),
: (),
:
};
.(taskWithBlueprint);
});
}
}
3. Service with Blueprint Context
@Injectable({ providedIn: 'root' })
export class TaskService {
private taskRepository = inject(TaskRepository);
private blueprintMemberRepository = inject(BlueprintMemberRepository);
private eventBus = inject(BlueprintEventBus);
async getTasks(blueprintId: string): Promise<Task[]> {
await this.validateBlueprintAccess(blueprintId);
return await this.taskRepository.findByBlueprintId(blueprintId);
}
async createTask(
blueprintId: string,
task: Omit<Task, 'id' | 'blueprintId'>
): Promise<Task> {
await this.validatePermission(blueprintId, );
created = ..(blueprintId, task);
..({
: ,
blueprintId,
: (),
: .(),
: created
});
created;
}
(: ): <> {
userId = .();
member = ..(
userId,
blueprintId
);
(!member || member. !== ) {
();
}
}
(
: ,
:
): <> {
userId = .();
member = ..(
userId,
blueprintId
);
(!member || member. !== ) {
();
}
(!member..(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);
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 = ..(blueprintId);
..(blueprint.);
}
(: ): <> {
..();
{
tasks = ..(blueprintId);
..(tasks);
} {
..();
}
}
}
Ownership Patterns
User-Owned Blueprint
interface Blueprint {
ownerType: 'user';
ownerId: string;
}
interface BlueprintMember {
memberType: 'user';
userId: string;
}
Organization-Owned Blueprint
interface Blueprint {
ownerType: 'organization';
ownerId: string;
}
interface BlueprintMember {
memberType: 'user' | 'team' | 'partner';
userId?: string;
teamId?: string;
partnerId?: string;
}
Permission System
Role + Permissions Model
type Role = 'owner' | 'admin' | 'member' | 'viewer';
type Permission =
| 'task:create'
| 'task:read'
| 'task:update'
| 'task:delete'
| 'file:upload'
| 'file:download'
| 'member:invite'
| 'member:remove';
interface BlueprintMember {
role: Role;
permissions: Permission[];
}
Checking Permissions
async checkPermission(
blueprintId: string,
permission: Permission
): Promise<boolean> {
const member = await this.getMember(blueprintId);
return member?.permissions.includes(permission) ?? false;
}
canCreateTask = computed(() => {
const member = this.currentMember();
return member?.permissions.includes('task:create') ?? false;
});
Security Rules Integration
Blueprint-Aware Rules
match /tasks/{taskId} {
allow read: if isAuthenticated() &&
isBlueprintMember(resource.data.blueprint_id);
allow create: if isAuthenticated() &&
isBlueprintMember(request.resource.data.blueprint_id) &&
hasPermission(request.resource.data.blueprint_id, 'task:create');
}
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.. == ;
}
Event-Driven Integration
Publishing Blueprint Events
this.eventBus.publish({
type: 'task.created',
blueprintId: task.blueprintId,
timestamp: new Date(),
actor: this.getCurrentUserId(),
data: task
});
Subscribing to Blueprint Events
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 () => {
await expectAsync(
service.getTasks('blueprint3')
).toBeRejectedWithError('Access denied to Blueprint');
});
it('should reject action without permission', async () => {
await expectAsync(
service.createTask(, { : })
).();
});
});
Checklist
When integrating Blueprint pattern:
References