# Firestore Security Rules

> Write and validate Firestore Security Rules following the project's multi-tenancy Blueprint pattern. Use this skill when implementing collection-level security, Blueprint membership validation, role-based permissions, and data access controls. Ensures rules validate BlueprintMember status, check permissions array, enforce data isolation, and integrate with the three-layer architecture where Security Rules are the first line of defense.

- Skill: `majiayu000/firestore-security-rules` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/firestore-security-rules`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/firestore-security-rules/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- License: MIT
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/firestore-security-rules

---


# Firestore Security Rules Skill

This skill helps create secure Firestore Security Rules following the project's multi-tenancy architecture.

## Core Principles

### Security-First Architecture
- **Security Rules are First Defense**: Never trust client-side checks alone
- **Multi-Tenancy**: Blueprint-based data isolation
- **Permission System**: Role + permissions array validation
- **Server-Side Validation**: All access validated at database level

### Blueprint Multi-Tenancy Model

```
User → Organization → Blueprint → Resources
           ↓
      Team / Partner
```

- Blueprint defines permission boundaries (not data boundaries)
- BlueprintMember is dedicated collection for membership
- All resources belong to a Blueprint
- Access controlled via BlueprintMember role + permissions

## Core Helper Functions

### Global Authentication

```javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    /**
     * Check if user is authenticated
     */
    function isAuthenticated() {
      return request.auth != null;
    }
    
    /**
     * Get current user ID
     */
    function getCurrentUserId() {
      return request.auth.uid;
    }
    
    /**
     * Check if user is system admin
     */
    function isSystemAdmin() {
      return isAuthenticated() && 
             get(/databases/$(database)/documents/users/$(getCurrentUserId())).data.role == 'admin';
    }
  }
}
```

### Blueprint Membership Functions

```javascript
/**
 * Check if user is Blueprint member
 */
function isBlueprintMember(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  return exists(/databases/$(database)/documents/blueprintMembers/$(memberId));
}

/**
 * Get user's role in Blueprint
 */
function getBlueprintMemberRole(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  return member.data.role;
}

/**
 * Check if user is Owner or Admin
 */
function isBlueprintOwnerOrAdmin(blueprintId) {
  let role = getBlueprintMemberRole(blueprintId);
  return role in ['owner', 'admin'];
}

/**
 * Check if user has specific permission
 */
function hasPermission(blueprintId, permission) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  return permission in member.data.permissions;
}

/**
 * Check if member is active
 */
function isMemberActive(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  return member.data.status == 'active';
}
```

### Data Validation Functions

```javascript
/**
 * Validate task data structure
 */
function validateTaskData(data) {
  return data.keys().hasAll(['blueprintId', 'title', 'status']) &&
         data.title is string &&
         data.title.size() > 0 &&
         data.title.size() <= 200 &&
         data.status in ['pending', 'in-progress', 'completed', 'archived'];
}
```

## Collection Rules Pattern

### Resource Collection (with Blueprint)

```javascript
match /tasks/{taskId} {
  // Blueprint members can read tasks
  allow read: if isAuthenticated() && 
                 isBlueprintMember(resource.data.blueprintId) &&
                 isMemberActive(resource.data.blueprintId);
  
  // Members with task:create permission can create
  allow create: if isAuthenticated() && 
                   isBlueprintMember(request.resource.data.blueprintId) &&
                   isMemberActive(request.resource.data.blueprintId) &&
                   hasPermission(request.resource.data.blueprintId, 'task:create') &&
                   validateTaskData(request.resource.data);
  
  // Members with task:update permission or assignee can update
  allow update: if isAuthenticated() && 
                   isBlueprintMember(resource.data.blueprintId) &&
                   isMemberActive(resource.data.blueprintId) &&
                   (hasPermission(resource.data.blueprintId, 'task:update') || 
                    resource.data.assignedTo == getCurrentUserId()) &&
                   validateTaskData(request.resource.data);
  
  // Only members with task:delete permission can delete
  allow delete: if isAuthenticated() && 
                   isBlueprintMember(resource.data.blueprintId) &&
                   isMemberActive(resource.data.blueprintId) &&
                   hasPermission(resource.data.blueprintId, 'task:delete');
}
```

### Nested Subcollections

```javascript
match /blueprints/{blueprintId} {
  allow read: if isAuthenticated() && isBlueprintMember(blueprintId);
  
  // Nested tasks subcollection
  match /tasks/{taskId} {
    allow read: if isAuthenticated() && 
                   isBlueprintMember(blueprintId) &&
                   isMemberActive(blueprintId);
    
    allow create: if isAuthenticated() && 
                     isBlueprintMember(blueprintId) &&
                     isMemberActive(blueprintId) &&
                     hasPermission(blueprintId, 'task:create') &&
                     validateTaskData(request.resource.data) &&
                     request.resource.data.blueprintId == blueprintId;
  }
}
```

### Membership Collection

```javascript
match /blueprintMembers/{memberId} {
  // Blueprint members can read member list
  allow read: if isAuthenticated() && 
                 isBlueprintMember(resource.data.blueprintId);
  
  // Only Owner/Admin can add/modify/remove members
  allow create, update: if isAuthenticated() && 
                           isBlueprintOwnerOrAdmin(resource.data.blueprintId);
  
  // Only Owner can delete members (soft delete recommended)
  allow delete: if isAuthenticated() && 
                   isBlueprintOwnerOrAdmin(resource.data.blueprintId);
}
```

### User Collection

```javascript
match /users/{userId} {
  // Users can only read their own data
  allow read: if isAuthenticated() && getCurrentUserId() == userId;
  
  // Users can update their own data (but not role)
  allow update: if isAuthenticated() && 
                   getCurrentUserId() == userId &&
                   !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']);
  
  // Only system admin can create/delete users
  allow create, delete: if isSystemAdmin();
}
```

### Audit Logs (Write-Only)

```javascript
match /auditLogs/{logId} {
  // Only system admin can read logs
  allow read: if isSystemAdmin();
  
  // Any authenticated user can write logs
  allow create: if isAuthenticated();
  
  // Logs cannot be modified or deleted
  allow update, delete: if false;
}
```

## Performance Optimization

### Minimize get() Calls

```javascript
// ❌ BAD: Multiple get() calls
function hasMultiplePermissions(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  let canCreate = 'task:create' in member.data.permissions;
  let member2 = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); // Duplicate!
  let canUpdate = 'task:update' in member2.data.permissions;
  return canCreate && canUpdate;
}

// ✅ GOOD: Single get() call
function hasMultiplePermissions(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  return 'task:create' in member.data.permissions && 
         'task:update' in member.data.permissions;
}
```

### Use exists() When Possible

```javascript
// ✅ GOOD: exists() is faster than get()
function isBlueprintMember(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  return exists(/databases/$(database)/documents/blueprintMembers/$(memberId));
}

// Only use get() when you need data
function getBlueprintMemberRole(blueprintId) {
  let memberId = getCurrentUserId() + '_' + blueprintId;
  let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId));
  return member.data.role;
}
```

## Testing Security Rules

### Using Firebase Emulator

```bash
# Start emulator
firebase emulators:start

# Run security rules tests
npm run test:rules
```

### Test Template

```javascript
describe('Task Collection Security Rules', () => {
  it('should allow authenticated blueprint member to read tasks', async () => {
    const db = testEnv.authenticatedContext('user1').firestore();
    
    // Setup: Create member
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await context.firestore().doc('blueprintMembers/user1_blueprint1').set({
        blueprintId: 'blueprint1',
        userId: 'user1',
        role: 'member',
        status: 'active',
        permissions: ['task:read']
      });
    });
    
    // Test: Read should succeed
    await assertSucceeds(db.doc('tasks/task1').get());
  });
  
  it('should deny unauthenticated access', async () => {
    const db = testEnv.unauthenticatedContext().firestore();
    await assertFails(db.doc('tasks/task1').get());
  });
});
```

## Checklist

When writing Security Rules:

- [ ] All collections have security rules
- [ ] Multi-tenancy data isolation implemented
- [ ] BlueprintMember membership checked
- [ ] Permissions array validated
- [ ] Member status checked (active/suspended)
- [ ] Data validation functions complete
- [ ] Minimize get() calls for performance
- [ ] Use exists() when only checking existence
- [ ] Audit logs are write-only
- [ ] Cross-blueprint access prevented
- [ ] Tests cover all scenarios
- [ ] Both positive and negative tests

## Integration with Architecture

### Three-Layer Security

```
1. UI Layer: Permission checks for UX (can hide buttons)
2. Service Layer: Business logic validation
3. Security Rules: Server-side enforcement (MUST)
```

### Repository + Security Rules

```typescript
// Repository - No permission checks
async deleteTask(taskId: string): Promise<void> {
  await this.taskRepository.delete(taskId);
  // Security Rules will validate permissions
}
```

```javascript
// Security Rules - Permission enforcement
match /tasks/{taskId} {
  allow delete: if isAuthenticated() 
                && isBlueprintMember(resource.data.blueprintId)
                && hasPermission(resource.data.blueprintId, 'task:delete');
}
```

## References

- [Security Rules Instructions](.github/instructions/ng-gighub-security-rules.instructions.md)
- [Architecture Guide](.github/instructions/ng-gighub-architecture.instructions.md)
- [Firebase Documentation](https://firebase.google.com/docs/firestore/security/get-started)

