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:
- Authentication Context: Use custom claims for RBAC instead of database lookups
- Role Hierarchies: Implement permission matrices with helper functions
- Field-Level Security: Validate both structure and content, enforce immutability
- Multi-Tenancy: Strict tenant isolation prevents cross-tenant data leakage
- 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
- Firestore Security Rules Documentation - Official Firebase guide
- Firebase Auth Custom Claims - Custom JWT claims documentation
- Security Rules Testing Guide - Firebase Emulator testing
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"
}
}