Firestore Security Rules Advanced for ChatGPT Apps

Building a ChatGPT app requires more than just functional code—it demands enterprise-grade security that protects user data, prevents unauthorized access, and enforces business logic at the database level. Firestore security rules are your first line of defense, executing server-side validation before any data is read or written.

While basic security rules handle simple authentication checks, production ChatGPT apps require advanced patterns: role-based access control (RBAC), field-level permissions, multi-tenant isolation, and dynamic validation. These patterns prevent common vulnerabilities like privilege escalation, data leakage, and unauthorized tenant access.

This guide covers advanced Firestore security rules specifically for ChatGPT apps built on the OpenAI Apps SDK. You'll learn how to implement RBAC with custom claims, enforce field-level security, isolate multi-tenant data, and test rules comprehensively. Every example is production-ready and battle-tested at scale.

For a complete security overview, see our ChatGPT App Security Hardening Guide. Let's dive into advanced security rules that protect your ChatGPT app.

Understanding Authentication Context in Firestore Rules

Firestore security rules operate in an authentication context provided by Firebase Auth. Every request includes a request.auth object containing the authenticated user's UID, email, and custom claims.

Authentication Context Structure:

// Available in all Firestore security rules
request.auth = {
  uid: "abc123",           // User ID (always available when authenticated)
  token: {
    email: "user@example.com",
    email_verified: true,
    firebase: {
      sign_in_provider: "google.com"
    },
    // Custom claims (set via Admin SDK)
    role: "admin",
    tenantId: "tenant_123",
    permissions: ["read", "write", "delete"]
  }
}

JWT Validation and Custom Claims:

Custom claims extend the authentication token with application-specific metadata. Set custom claims via the Firebase Admin SDK:

// functions/src/services/auth-manager.ts
import * as admin from 'firebase-admin';

export class AuthManager {
  /**
   * Set custom claims for role-based access control
   */
  static async setUserRole(uid: string, role: string, tenantId: string): Promise<void> {
    const customClaims = {
      role: role,                          // 'admin', 'editor', 'viewer'
      tenantId: tenantId,                  // Tenant isolation
      permissions: this.getRolePermissions(role),
      assignedAt: Date.now()
    };

    await admin.auth().setCustomUserClaims(uid, customClaims);

    // Force token refresh by updating user metadata
    await admin.firestore().collection('users').doc(uid).update({
      role: role,
      tenantId: tenantId,
      roleUpdatedAt: admin.firestore.FieldValue.serverTimestamp()
    });
  }

  /**
   * Get permissions array for a role
   */
  private static getRolePermissions(role: string): string[] {
    const rolePermissions: Record<string, string[]> = {
      'admin': ['read', 'write', 'delete', 'manage_users', 'manage_settings'],
      'editor': ['read', 'write'],
      'viewer': ['read'],
      'guest': []
    };
    return rolePermissions[role] || [];
  }

  /**
   * Verify custom claims on client-side token refresh
   */
  static async refreshUserClaims(user: admin.auth.UserRecord): Promise<void> {
    // Force token refresh (client-side calls this after role change)
    const currentToken = await user.getIdToken(true);
    return currentToken;
  }
}

Basic Authentication Check:

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Helper function: Check if user is authenticated
    function isAuthenticated() {
      return request.auth != null;
    }

    // Helper function: Check if user owns resource
    function isOwner(userId) {
      return isAuthenticated() && request.auth.uid == userId;
    }

    // Example: Users can only read/write their own documents
    match /users/{userId} {
      allow read, write: if isOwner(userId);
    }
  }
}

Custom claims eliminate the need for database lookups during security rule evaluation, improving performance and reducing costs. Always validate custom claims server-side before setting them.

Role-Based Access Control (RBAC) Implementation

RBAC enforces permissions based on user roles defined in custom claims. This pattern scales from simple admin/user distinctions to complex hierarchical roles with granular permissions.

RBAC Security Rules (Production-Ready):

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // =====================================
    // RBAC HELPER FUNCTIONS
    // =====================================

    function isAuthenticated() {
      return request.auth != null;
    }

    function getUserRole() {
      return isAuthenticated() ? request.auth.token.role : null;
    }

    function getTenantId() {
      return isAuthenticated() ? request.auth.token.tenantId : null;
    }

    function hasRole(role) {
      return getUserRole() == role;
    }

    function hasAnyRole(roles) {
      return getUserRole() in roles;
    }

    function hasPermission(permission) {
      return isAuthenticated()
        && 'permissions' in request.auth.token
        && permission in request.auth.token.permissions;
    }

    // Role hierarchy check (admin > editor > viewer)
    function roleLevel(role) {
      return role == 'admin' ? 3
           : role == 'editor' ? 2
           : role == 'viewer' ? 1
           : 0;
    }

    function hasMinimumRole(minRole) {
      return roleLevel(getUserRole()) >= roleLevel(minRole);
    }

    // =====================================
    // CHATGPT APPS COLLECTION
    // =====================================

    match /apps/{appId} {
      // Read: Owner OR tenant member with 'read' permission
      allow read: if isAuthenticated() && (
        resource.data.userId == request.auth.uid ||
        (resource.data.tenantId == getTenantId() && hasPermission('read'))
      );

      // Create: Authenticated users with 'write' permission
      allow create: if isAuthenticated()
        && hasPermission('write')
        && request.resource.data.userId == request.auth.uid
        && request.resource.data.tenantId == getTenantId();

      // Update: Owner OR tenant admin/editor
      allow update: if isAuthenticated() && (
        resource.data.userId == request.auth.uid ||
        (resource.data.tenantId == getTenantId() && hasAnyRole(['admin', 'editor']))
      );

      // Delete: Owner OR tenant admin
      allow delete: if isAuthenticated() && (
        resource.data.userId == request.auth.uid ||
        (resource.data.tenantId == getTenantId() && hasRole('admin'))
      );
    }

    // =====================================
    // USER MANAGEMENT (ADMIN ONLY)
    // =====================================

    match /users/{userId} {
      // Users can read their own profile
      allow read: if isAuthenticated() && (
        request.auth.uid == userId ||
        hasRole('admin')
      );

      // Users can update their own profile (except role/tenantId)
      allow update: if isAuthenticated() && request.auth.uid == userId
        && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role', 'tenantId', 'permissions']);

      // Only admins can create/delete users
      allow create, delete: if hasRole('admin');
    }

    // =====================================
    // TENANT SETTINGS (HIERARCHICAL PERMISSIONS)
    // =====================================

    match /tenants/{tenantId}/settings/{settingId} {
      // Read: Any tenant member
      allow read: if getTenantId() == tenantId;

      // Update: Editor or above
      allow update: if getTenantId() == tenantId
        && hasMinimumRole('editor');

      // Delete: Admin only
      allow delete: if getTenantId() == tenantId
        && hasRole('admin');
    }

    // =====================================
    // AUDIT LOGS (READ-ONLY FOR NON-ADMINS)
    // =====================================

    match /audit_logs/{logId} {
      // Read: Admins see all logs, users see their own
      allow read: if isAuthenticated() && (
        hasRole('admin') ||
        resource.data.userId == request.auth.uid
      );

      // Write: Server-side only (no client writes)
      allow write: if false;
    }
  }
}

Permission Matrix Testing:

// test/security-rules.test.ts
import * as firebase from '@firebase/rules-unit-testing';
import { readFileSync } from 'fs';

describe('RBAC Security Rules', () => {
  let testEnv: firebase.RulesTestEnvironment;

  beforeAll(async () => {
    testEnv = await firebase.initializeTestEnvironment({
      projectId: 'test-project',
      firestore: {
        rules: readFileSync('firestore.rules', 'utf8'),
        host: 'localhost',
        port: 8080
      }
    });
  });

  afterAll(async () => {
    await testEnv.cleanup();
  });

  describe('App Collection RBAC', () => {
    it('should allow admin to delete any app in tenant', async () => {
      const adminContext = testEnv.authenticatedContext('admin_user', {
        role: 'admin',
        tenantId: 'tenant_1',
        permissions: ['read', 'write', 'delete']
      });

      const appRef = adminContext.firestore().collection('apps').doc('app_123');

      await firebase.assertSucceeds(
        appRef.set({
          userId: 'other_user',
          tenantId: 'tenant_1',
          name: 'Test App',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );

      await firebase.assertSucceeds(appRef.delete());
    });

    it('should prevent editor from deleting apps', async () => {
      const editorContext = testEnv.authenticatedContext('editor_user', {
        role: 'editor',
        tenantId: 'tenant_1',
        permissions: ['read', 'write']
      });

      const appRef = editorContext.firestore().collection('apps').doc('app_456');

      await firebase.assertFails(appRef.delete());
    });

    it('should prevent cross-tenant access', async () => {
      const tenant1Context = testEnv.authenticatedContext('user_1', {
        role: 'editor',
        tenantId: 'tenant_1',
        permissions: ['read', 'write']
      });

      const appRef = tenant1Context.firestore().collection('apps').doc('app_tenant2');

      await firebase.assertFails(
        appRef.set({
          userId: 'user_2',
          tenantId: 'tenant_2',  // Different tenant!
          name: 'Unauthorized App',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );
    });
  });
});

RBAC patterns scale to thousands of users while maintaining sub-millisecond rule evaluation. Always test permission matrices comprehensively.

Field-Level Security and Validation

Field-level security restricts which fields users can read or write, preventing privilege escalation and data corruption. Combine this with validation rules to enforce business logic at the database level.

Field-Level Security Rules:

// firestore.rules (continued)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // =====================================
    // FIELD-LEVEL VALIDATION HELPERS
    // =====================================

    function isValidEmail(email) {
      return email.matches('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');
    }

    function isValidURL(url) {
      return url.matches('^https?://.*');
    }

    function isWithinSize(text, maxLength) {
      return text.size() <= maxLength;
    }

    function hasOnlyAllowedFields(allowedFields) {
      return request.resource.data.keys().hasOnly(allowedFields);
    }

    function isNotChanging(field) {
      return !(field in request.resource.data)
        || request.resource.data[field] == resource.data[field];
    }

    // =====================================
    // CHATGPT APP FIELD VALIDATION
    // =====================================

    match /apps/{appId} {
      // Field validation on create
      allow create: if isAuthenticated()
        && hasPermission('write')
        && request.resource.data.keys().hasAll(['name', 'userId', 'tenantId', 'status'])
        && request.resource.data.name is string
        && isWithinSize(request.resource.data.name, 100)
        && request.resource.data.userId == request.auth.uid
        && request.resource.data.tenantId == getTenantId()
        && request.resource.data.status in ['draft', 'active', 'archived']
        && (!('description' in request.resource.data) || isWithinSize(request.resource.data.description, 500))
        && (!('webhookUrl' in request.resource.data) || isValidURL(request.resource.data.webhookUrl));

      // Field validation on update (selective field updates)
      allow update: if isAuthenticated()
        && (resource.data.userId == request.auth.uid || hasRole('admin'))
        && isNotChanging('userId')          // Immutable field
        && isNotChanging('tenantId')         // Immutable field
        && isNotChanging('createdAt')        // Immutable field
        && (
          // Regular users can update name, description, status
          (!hasRole('admin') && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'description', 'status', 'updatedAt']))
          ||
          // Admins can update any field except immutables
          (hasRole('admin') && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['userId', 'tenantId', 'createdAt']))
        );
    }

    // =====================================
    // USER PROFILE FIELD SECURITY
    // =====================================

    match /users/{userId} {
      // Read filtering: Hide sensitive fields from non-admins
      allow read: if isAuthenticated() && (
        request.auth.uid == userId ||
        hasRole('admin')
      );

      // Update: Users can update profile but not role/permissions
      allow update: if isAuthenticated()
        && request.auth.uid == userId
        && isNotChanging('role')
        && isNotChanging('tenantId')
        && isNotChanging('permissions')
        && isNotChanging('createdAt')
        && (!('email' in request.resource.data) || isValidEmail(request.resource.data.email))
        && (!('displayName' in request.resource.data) || isWithinSize(request.resource.data.displayName, 100));

      // Admin update: Can change role/permissions
      allow update: if hasRole('admin')
        && request.resource.data.tenantId == getTenantId()  // Prevent cross-tenant privilege escalation
        && isNotChanging('createdAt');
    }

    // =====================================
    // SUBSCRIPTION FIELD VALIDATION
    // =====================================

    match /subscriptions/{subscriptionId} {
      // Read-only for users (Stripe webhooks write via Admin SDK)
      allow read: if isAuthenticated()
        && resource.data.userId == request.auth.uid;

      // No client writes (Stripe webhook only)
      allow write: if false;
    }

    // =====================================
    // MCP SERVER CONFIG VALIDATION
    // =====================================

    match /apps/{appId}/mcp_servers/{serverId} {
      allow create, update: if isAuthenticated()
        && get(/databases/$(database)/documents/apps/$(appId)).data.userId == request.auth.uid
        && request.resource.data.keys().hasAll(['name', 'transport', 'tools'])
        && request.resource.data.transport in ['streamable-http', 'sse']
        && request.resource.data.tools is list
        && request.resource.data.tools.size() <= 50  // Max 50 tools per server
        && (!('authType' in request.resource.data) || request.resource.data.authType in ['none', 'oauth', 'api_key']);
    }
  }
}

Field Validation Service (TypeScript):

// functions/src/services/field-validator.ts
import { DocumentData } from 'firebase-admin/firestore';

export class FieldValidator {
  /**
   * Validate app creation payload
   */
  static validateAppCreate(data: DocumentData): { valid: boolean; errors: string[] } {
    const errors: string[] = [];

    // Required fields
    if (!data.name || typeof data.name !== 'string') {
      errors.push('name is required and must be a string');
    } else if (data.name.length > 100) {
      errors.push('name must be 100 characters or less');
    }

    if (!data.userId || typeof data.userId !== 'string') {
      errors.push('userId is required');
    }

    if (!data.tenantId || typeof data.tenantId !== 'string') {
      errors.push('tenantId is required');
    }

    if (!data.status || !['draft', 'active', 'archived'].includes(data.status)) {
      errors.push('status must be draft, active, or archived');
    }

    // Optional fields
    if (data.description && data.description.length > 500) {
      errors.push('description must be 500 characters or less');
    }

    if (data.webhookUrl && !this.isValidURL(data.webhookUrl)) {
      errors.push('webhookUrl must be a valid HTTPS URL');
    }

    if (data.tags && (!Array.isArray(data.tags) || data.tags.length > 10)) {
      errors.push('tags must be an array with max 10 items');
    }

    return { valid: errors.length === 0, errors };
  }

  /**
   * Validate app update payload (only allowed fields)
   */
  static validateAppUpdate(
    currentData: DocumentData,
    updateData: DocumentData,
    userRole: string
  ): { valid: boolean; errors: string[] } {
    const errors: string[] = [];
    const allowedFields = userRole === 'admin'
      ? ['name', 'description', 'status', 'webhookUrl', 'tags', 'settings']
      : ['name', 'description', 'status'];

    const immutableFields = ['userId', 'tenantId', 'createdAt'];

    // Check for immutable field changes
    for (const field of immutableFields) {
      if (field in updateData && updateData[field] !== currentData[field]) {
        errors.push(`${field} is immutable and cannot be changed`);
      }
    }

    // Check for unauthorized field updates
    for (const field of Object.keys(updateData)) {
      if (!allowedFields.includes(field) && !immutableFields.includes(field) && field !== 'updatedAt') {
        errors.push(`${field} is not allowed for ${userRole} role`);
      }
    }

    return { valid: errors.length === 0, errors };
  }

  private static isValidURL(url: string): boolean {
    try {
      const parsed = new URL(url);
      return parsed.protocol === 'https:';
    } catch {
      return false;
    }
  }
}

Field-level security prevents clients from bypassing validation by sending unauthorized fields. Always validate both structure and content.

Multi-Tenancy and Tenant Isolation

Multi-tenant ChatGPT apps require strict data isolation between tenants. Firestore security rules enforce tenant boundaries, preventing users from accessing or modifying other tenants' data.

Multi-Tenant Security Rules:

// firestore.rules (continued)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // =====================================
    // MULTI-TENANT ISOLATION HELPERS
    // =====================================

    function getTenantId() {
      return request.auth != null ? request.auth.token.tenantId : null;
    }

    function isTenantMember(tenantId) {
      return getTenantId() == tenantId;
    }

    function isTenantAdmin(tenantId) {
      return isTenantMember(tenantId) && hasRole('admin');
    }

    function getTenantResourceQuota(tenantId, resourceType) {
      return get(/databases/$(database)/documents/tenants/$(tenantId)).data.quotas[resourceType];
    }

    function getCurrentResourceCount(tenantId, resourceType) {
      // Note: This is expensive - use server-side counters instead
      return get(/databases/$(database)/documents/tenants/$(tenantId)).data.usage[resourceType];
    }

    // =====================================
    // TENANT COLLECTION
    // =====================================

    match /tenants/{tenantId} {
      // Read: Any authenticated user (for tenant discovery)
      allow read: if isAuthenticated();

      // Create: Server-side only (Admin SDK)
      allow create: if false;

      // Update: Tenant admins only
      allow update: if isTenantAdmin(tenantId)
        && isNotChanging('id')
        && isNotChanging('createdAt')
        && isNotChanging('subscriptionTier');  // Updated via Stripe webhook

      // Delete: Server-side only
      allow delete: if false;
    }

    // =====================================
    // TENANT-SCOPED APPS
    // =====================================

    match /apps/{appId} {
      // Strict tenant isolation on read
      allow read: if isAuthenticated()
        && resource.data.tenantId == getTenantId();

      // Create with tenant quota enforcement
      allow create: if isAuthenticated()
        && request.resource.data.tenantId == getTenantId()
        && request.resource.data.userId == request.auth.uid
        // Note: Quota checks should be server-side for accuracy
        && hasPermission('write');

      // Update: Tenant member with edit permission
      allow update: if isAuthenticated()
        && resource.data.tenantId == getTenantId()
        && isNotChanging('tenantId')
        && (resource.data.userId == request.auth.uid || hasRole('admin'));

      // Delete: Owner or tenant admin
      allow delete: if isAuthenticated()
        && resource.data.tenantId == getTenantId()
        && (resource.data.userId == request.auth.uid || hasRole('admin'));
    }

    // =====================================
    // TENANT USAGE TRACKING
    // =====================================

    match /tenants/{tenantId}/usage/{month} {
      // Read: Tenant members
      allow read: if isTenantMember(tenantId);

      // Write: Server-side only (Cloud Functions)
      allow write: if false;
    }

    // =====================================
    // CROSS-TENANT PREVENTION
    // =====================================

    match /apps/{appId}/collaborators/{userId} {
      // Collaborators must belong to same tenant
      allow create: if isAuthenticated()
        && get(/databases/$(database)/documents/apps/$(appId)).data.tenantId == getTenantId()
        && get(/databases/$(database)/documents/users/$(userId)).data.tenantId == getTenantId();

      allow read, update, delete: if isAuthenticated()
        && get(/databases/$(database)/documents/apps/$(appId)).data.tenantId == getTenantId();
    }
  }
}

Tenant Isolation Service:

// functions/src/services/tenant-manager.ts
import * as admin from 'firebase-admin';

export class TenantManager {
  /**
   * Create new tenant with default quotas
   */
  static async createTenant(params: {
    name: string;
    ownerId: string;
    subscriptionTier: 'free' | 'starter' | 'professional' | 'business';
  }): Promise<string> {
    const tenantId = admin.firestore().collection('tenants').doc().id;

    const quotas = this.getQuotasForTier(params.subscriptionTier);

    await admin.firestore().collection('tenants').doc(tenantId).set({
      id: tenantId,
      name: params.name,
      ownerId: params.ownerId,
      subscriptionTier: params.subscriptionTier,
      quotas: quotas,
      usage: {
        apps: 0,
        toolCalls: 0,
        storage: 0
      },
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
      updatedAt: admin.firestore.FieldValue.serverTimestamp()
    });

    // Assign tenant to owner
    await admin.auth().setCustomUserClaims(params.ownerId, {
      tenantId: tenantId,
      role: 'admin',
      permissions: ['read', 'write', 'delete', 'manage_users']
    });

    return tenantId;
  }

  /**
   * Check if tenant has quota available
   */
  static async checkQuota(tenantId: string, resourceType: string): Promise<boolean> {
    const tenantDoc = await admin.firestore().collection('tenants').doc(tenantId).get();
    if (!tenantDoc.exists) return false;

    const data = tenantDoc.data()!;
    const quota = data.quotas[resourceType];
    const usage = data.usage[resourceType] || 0;

    return usage < quota;
  }

  /**
   * Increment tenant usage (atomic)
   */
  static async incrementUsage(tenantId: string, resourceType: string, amount: number = 1): Promise<void> {
    await admin.firestore().collection('tenants').doc(tenantId).update({
      [`usage.${resourceType}`]: admin.firestore.FieldValue.increment(amount),
      updatedAt: admin.firestore.FieldValue.serverTimestamp()
    });
  }

  private static getQuotasForTier(tier: string): Record<string, number> {
    const quotas: Record<string, Record<string, number>> = {
      'free': { apps: 1, toolCalls: 1000, storage: 100 },      // 100MB
      'starter': { apps: 3, toolCalls: 10000, storage: 1000 },   // 1GB
      'professional': { apps: 10, toolCalls: 50000, storage: 10000 },  // 10GB
      'business': { apps: 50, toolCalls: 200000, storage: 100000 }     // 100GB
    };
    return quotas[tier] || quotas['free'];
  }
}

Multi-tenant isolation requires both security rules (client-side enforcement) and server-side validation (quota checks, usage tracking). Never trust client-provided tenant IDs.

Security Rules Testing and CI/CD Integration

Comprehensive testing prevents security regressions and validates permission matrices across all user roles. Use the Firebase Emulator Suite and rules unit testing framework for local development and CI/CD pipelines.

Complete Security Test Suite:

// test/firestore-security.test.ts
import * as firebase from '@firebase/rules-unit-testing';
import { readFileSync } from 'fs';
import { describe, it, beforeAll, afterAll, expect } from '@jest/globals';

describe('Firestore Security Rules', () => {
  let testEnv: firebase.RulesTestEnvironment;

  beforeAll(async () => {
    testEnv = await firebase.initializeTestEnvironment({
      projectId: 'test-chatgpt-app',
      firestore: {
        rules: readFileSync('firestore.rules', 'utf8'),
        host: 'localhost',
        port: 8080
      }
    });
  });

  afterAll(async () => {
    await testEnv.cleanup();
  });

  afterEach(async () => {
    await testEnv.clearFirestore();
  });

  describe('Multi-Tenant Isolation', () => {
    it('should prevent cross-tenant app access', async () => {
      const tenant1User = testEnv.authenticatedContext('user_tenant1', {
        tenantId: 'tenant_1',
        role: 'admin',
        permissions: ['read', 'write', 'delete']
      });

      const tenant2User = testEnv.authenticatedContext('user_tenant2', {
        tenantId: 'tenant_2',
        role: 'admin',
        permissions: ['read', 'write', 'delete']
      });

      // Tenant 1 creates app
      const appRef = tenant1User.firestore().collection('apps').doc('app_1');
      await firebase.assertSucceeds(
        appRef.set({
          userId: 'user_tenant1',
          tenantId: 'tenant_1',
          name: 'Tenant 1 App',
          status: 'active',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );

      // Tenant 2 cannot read Tenant 1's app
      const crossTenantRead = tenant2User.firestore().collection('apps').doc('app_1');
      await firebase.assertFails(crossTenantRead.get());

      // Tenant 2 cannot delete Tenant 1's app
      await firebase.assertFails(crossTenantRead.delete());
    });

    it('should enforce tenant-scoped queries', async () => {
      const tenant1User = testEnv.authenticatedContext('user_tenant1', {
        tenantId: 'tenant_1',
        role: 'viewer',
        permissions: ['read']
      });

      // Query without tenant filter should fail
      const unfiltered = tenant1User.firestore().collection('apps');
      await firebase.assertFails(unfiltered.get());

      // Query with correct tenant filter should succeed
      const filtered = tenant1User.firestore()
        .collection('apps')
        .where('tenantId', '==', 'tenant_1');
      await firebase.assertSucceeds(filtered.get());
    });
  });

  describe('Role-Based Access Control', () => {
    it('should allow admin to delete any tenant app', async () => {
      const adminUser = testEnv.authenticatedContext('admin_user', {
        tenantId: 'tenant_1',
        role: 'admin',
        permissions: ['read', 'write', 'delete']
      });

      const appRef = adminUser.firestore().collection('apps').doc('app_admin');
      await firebase.assertSucceeds(
        appRef.set({
          userId: 'other_user',
          tenantId: 'tenant_1',
          name: 'Test App',
          status: 'active',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );

      await firebase.assertSucceeds(appRef.delete());
    });

    it('should prevent viewer from creating apps', async () => {
      const viewerUser = testEnv.authenticatedContext('viewer_user', {
        tenantId: 'tenant_1',
        role: 'viewer',
        permissions: ['read']
      });

      const appRef = viewerUser.firestore().collection('apps').doc('app_viewer');
      await firebase.assertFails(
        appRef.set({
          userId: 'viewer_user',
          tenantId: 'tenant_1',
          name: 'Unauthorized App',
          status: 'draft',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );
    });

    it('should prevent editor from changing user roles', async () => {
      const editorUser = testEnv.authenticatedContext('editor_user', {
        tenantId: 'tenant_1',
        role: 'editor',
        permissions: ['read', 'write']
      });

      const userRef = editorUser.firestore().collection('users').doc('target_user');

      // Create user document first (as admin)
      await testEnv.withSecurityRulesDisabled(async (context) => {
        await context.firestore().collection('users').doc('target_user').set({
          tenantId: 'tenant_1',
          role: 'viewer',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        });
      });

      // Editor tries to promote user to admin
      await firebase.assertFails(
        userRef.update({ role: 'admin' })
      );
    });
  });

  describe('Field-Level Security', () => {
    it('should prevent changing immutable fields', async () => {
      const user = testEnv.authenticatedContext('user_1', {
        tenantId: 'tenant_1',
        role: 'editor',
        permissions: ['read', 'write']
      });

      const appRef = user.firestore().collection('apps').doc('app_immutable');

      // Create app
      await firebase.assertSucceeds(
        appRef.set({
          userId: 'user_1',
          tenantId: 'tenant_1',
          name: 'Original Name',
          status: 'draft',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );

      // Try to change tenantId (immutable)
      await firebase.assertFails(
        appRef.update({ tenantId: 'tenant_2' })
      );

      // Allowed field update
      await firebase.assertSucceeds(
        appRef.update({ name: 'Updated Name' })
      );
    });

    it('should validate field types and constraints', async () => {
      const user = testEnv.authenticatedContext('user_1', {
        tenantId: 'tenant_1',
        role: 'editor',
        permissions: ['read', 'write']
      });

      const appRef = user.firestore().collection('apps').doc('app_validation');

      // Invalid status enum
      await firebase.assertFails(
        appRef.set({
          userId: 'user_1',
          tenantId: 'tenant_1',
          name: 'Test',
          status: 'invalid_status',  // Not in ['draft', 'active', 'archived']
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );

      // Valid payload
      await firebase.assertSucceeds(
        appRef.set({
          userId: 'user_1',
          tenantId: 'tenant_1',
          name: 'Test',
          status: 'draft',
          createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
      );
    });
  });
});

CI/CD Integration (GitHub Actions):

# .github/workflows/security-tests.yml
name: Firestore Security Rules Tests

on:
  push:
    branches: [master, develop]
    paths:
      - 'firestore.rules'
      - 'test/**'
  pull_request:
    branches: [master]

jobs:
  test-security-rules:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Start Firebase Emulator
        run: |
          firebase emulators:start --only firestore --project test-project &
          sleep 10

      - name: Run security rules tests
        run: npm run test:security

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: security-test-results
          path: test-results/

Automated testing catches security regressions before deployment. Run tests locally with firebase emulators:exec --only firestore "npm run test:security".

Conclusion: Enterprise-Grade Security for ChatGPT Apps

Advanced Firestore security rules transform your database into a fortress, enforcing authentication, authorization, and validation at every access point. By implementing RBAC with custom claims, field-level permissions, multi-tenant isolation, and comprehensive testing, you create a security layer that scales from MVP to enterprise.

Key Takeaways:

  1. Authentication Context: Use custom claims for RBAC instead of database lookups
  2. Role Hierarchies: Implement permission matrices with helper functions
  3. Field-Level Security: Validate both structure and content, enforce immutability
  4. Multi-Tenancy: Strict tenant isolation prevents cross-tenant data leakage
  5. Testing: Automate security rule validation in CI/CD pipelines

Production Checklist:

  • RBAC roles defined with permission matrices
  • Custom claims set via Admin SDK on user creation/role change
  • Field validation enforces business logic (length, format, enums)
  • Immutable fields protected from client updates
  • Multi-tenant isolation tested across all collections
  • Security rules unit tests achieve 100% coverage
  • CI/CD pipeline runs emulator tests on every commit
  • Audit logs track sensitive operations (read-only for non-admins)

Ready to build ChatGPT apps with enterprise-grade security? MakeAIHQ generates production-ready Firestore security rules automatically, including RBAC, multi-tenancy, and field validation—all optimized for OpenAI Apps SDK compliance.

Start building secure ChatGPT apps →


Related Resources

  • ChatGPT App Security Hardening Guide - Complete security architecture
  • Firestore Query Optimization for ChatGPT Apps - Performance best practices
  • Multi-Tenant Architecture for ChatGPT Apps - Tenant isolation patterns
  • RBAC Implementation Guide for ChatGPT Apps - Role-based access control
  • Firebase Auth Custom Claims for ChatGPT Apps - JWT customization
  • ChatGPT App User Authentication Patterns - Auth strategies
  • Firestore Data Validation for ChatGPT Apps - Input validation

External References


Schema Markup (JSON-LD):

{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "How to Write Advanced Firestore Security Rules for ChatGPT Apps",
  "description": "Write advanced Firestore security rules for ChatGPT apps. Role-based access, field-level security, validation with production examples.",
  "image": "https://makeaihq.com/images/firestore-security-rules-advanced.png",
  "totalTime": "PT45M",
  "estimatedCost": {
    "@type": "MonetaryAmount",
    "currency": "USD",
    "value": "0"
  },
  "tool": [
    {
      "@type": "HowToTool",
      "name": "Firebase Firestore"
    },
    {
      "@type": "HowToTool",
      "name": "Firebase Auth"
    },
    {
      "@type": "HowToTool",
      "name": "Firebase Emulator Suite"
    }
  ],
  "step": [
    {
      "@type": "HowToStep",
      "name": "Understand Authentication Context",
      "text": "Learn how request.auth provides authentication context in Firestore security rules, including custom claims for RBAC.",
      "url": "https://makeaihq.com/guides/cluster/firestore-security-rules-advanced-chatgpt#understanding-authentication-context-in-firestore-rules"
    },
    {
      "@type": "HowToStep",
      "name": "Implement RBAC",
      "text": "Create role-based access control using custom claims and helper functions for permission matrices.",
      "url": "https://makeaihq.com/guides/cluster/firestore-security-rules-advanced-chatgpt#role-based-access-control-rbac-implementation"
    },
    {
      "@type": "HowToStep",
      "name": "Enforce Field-Level Security",
      "text": "Restrict which fields users can read or write, with validation rules for business logic enforcement.",
      "url": "https://makeaihq.com/guides/cluster/firestore-security-rules-advanced-chatgpt#field-level-security-and-validation"
    },
    {
      "@type": "HowToStep",
      "name": "Implement Multi-Tenancy",
      "text": "Enforce strict tenant isolation to prevent cross-tenant data access and quota enforcement.",
      "url": "https://makeaihq.com/guides/cluster/firestore-security-rules-advanced-chatgpt#multi-tenancy-and-tenant-isolation"
    },
    {
      "@type": "HowToStep",
      "name": "Test Security Rules",
      "text": "Use Firebase Emulator Suite and unit tests to validate permission matrices and prevent regressions.",
      "url": "https://makeaihq.com/guides/cluster/firestore-security-rules-advanced-chatgpt#security-rules-testing-and-cicd-integration"
    }
  ],
  "about": {
    "@type": "Thing",
    "name": "Firestore Security Rules"
  }
}