Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
Firestore Security Rules Reference
Documents every Firestore and Storage security rule in firestore.rules and storage.rules.
These rules enforce tenant isolation, RBAC, and data integrity at the database level.
Overview
| Property | Value |
|---|
| Rules version | 2 |
| Firestore rules file | firestore.rules (~3,751 lines) |
| Storage rules file | storage.rules (~116 lines) |
| Multi-tenancy model | organizationId field on every document + custom claims |
| Auth mechanism | Firebase Auth custom claims (role, organizationId, isSuperAdmin) |
| Role hierarchy | SuperAdmin > Admin > Manager > User |
All rules follow a strict tenant-isolation pattern: every read/write must validate the user’s organizationId against the document’s organizationId. Cross-tenant access is never allowed except for SuperAdmin.
Helper Functions
Authentication & Identity
| Function | Purpose | Used By |
|---|
isAuthenticated() | Returns true if request.auth != null | Nearly all rules |
userId() | Returns request.auth.uid | Owner checks, creator validation |
getUserOrganizationId() | Resolves org ID from auth claims, then falls back to employees or people document lookup | Org membership checks |
employeeDocPath() | Returns Firestore path /employees/{uid} | Role/org lookups |
personDocPath() | Returns Firestore path /people/{uid} | Role/org lookups (modern collection) |
Organization Membership
| Function | Purpose | Used By |
|---|
hasOrgClaim(orgId) | Checks organizationId or organizationIds (array) in auth claims | belongsToOrg() |
belongsToOrg(orgId) | Comprehensive org membership check: impersonation, claims, employee/people doc, or SuperAdmin | Most collection rules |
hasEmployeeDoc(orgId) | Checks if employees/{uid} exists and matches org | belongsToOrg() |
hasPeopleDoc(orgId) | Checks if people/{uid} exists and matches org | belongsToOrg() |
isOrgActive(orgId) | Validates organizations/{orgId}.status == 'active' | Create rules (prevents writes to suspended orgs) |
Role Checks
| Function | Purpose | Used By |
|---|
hasAnyRoleClaim(roles) | Checks if auth claim role is in the given array | Role hierarchy functions |
employeeSystemRoleIn(roles) | Checks employees/{uid}.systemRole against role array | Stored role fallback |
personSystemRoleIn(roles) | Checks people/{uid}.systemRole against role array | Stored role fallback |
hasStoredSystemRole(roles) | Combines employeeSystemRoleIn + personSystemRoleIn | Role hierarchy functions |
isSuperAdmin() | Checks isSuperAdmin claim, or role claim/stored role matches superadmin/Super Admin/SuperAdmin | Global admin access |
isAdmin() | SuperAdmin OR admin/Admin claim/stored role | Org-level admin access |
isManagerOrHigher() | SuperAdmin OR admin/manager claim/stored role | Approval workflows |
Impersonation
| Function | Purpose | Used By |
|---|
isImpersonating() | Checks request.auth.token.impersonating == true AND isSuperAdmin() | belongsToOrg() |
impersonatingOrgId() | Returns request.auth.token.impersonatingOrgId | Impersonation org context |
Auditor Access
| Function | Purpose | Used By |
|---|
hasAuditorAccess(orgId) | Checks auditorAccessGrants/{uid}_{orgId} for active, non-expired grant | isAuditor() |
isAuditor(orgId) | SuperAdmin OR (auditor role + active access grant) | Compliance/audit collections |
Resource Checks
| Function | Purpose | Used By |
|---|
isOwner(resource) | resource.data.userId == userId() | Owner-only operations |
isLocked(resource) | resource.data.isLocked == true | Journal/timesheet approval lock |
canApproveJournals() | Alias for isManagerOrHigher() | Journal approval rules |
Permission Checks
| Function | Purpose | Used By |
|---|
hasUserPermission(orgId, uid, permission) | Looks up employee’s roleId, then checks roles/{roleId}.permissions array for the given permission string | API keys, contacts, interactions |
Collection-scoped Helpers
These are defined inside specific match blocks:
| Function | Scope | Purpose |
|---|
hasApiKeyPermission(orgId) | apiKeys/{keyId} | Checks SuperAdmin, Admin, or MANAGE_API_KEYS/MANAGE_PLATFORM permission |
hasContactPermission(permission) | contacts/{contactId} | Checks SuperAdmin, Admin, or specific permission |
hasInteractionPermission(permission) | interactions/{interactionId} | Checks SuperAdmin, Admin, or specific permission |
hasSegmentPermission(permission) | contactSegments/{segmentId} | Checks SuperAdmin, Admin, or specific permission |
Collection Rules
Organizations
organizations/{orgId}
- read:
belongsToOrg(orgId)
- create:
isSuperAdmin() (tenant provisioning only)
- update: Admin or SuperAdmin or impersonating; validates
partnerOrgId reference if present
- delete:
isSuperAdmin() (soft delete via status change)
organizations/{orgId}/roles/{roleId}
- read:
belongsToOrg(orgId)
- write: SuperAdmin or Admin
organizations/{orgId}/config/{configId}
- read:
belongsToOrg(orgId)
- write: SuperAdmin or Admin
organizations/{orgId}/integration_exports/{exportId}
- read:
belongsToOrg(orgId)
- write: Admin or SuperAdmin
organizations/{orgId}/api_keys/{keyId}
- read: Admin only
- write: Admin or SuperAdmin
organizations/{orgId}/webhooks/{webhookId}
- read: Admin only
- write: Admin or SuperAdmin
- read:
belongsToOrg(orgId)
- create/update: Manager+
- delete: Admin only
organizations/{orgId}/active_timers/{employeeId}
- read: Org member; only timer owner or Manager+
- create/update: Timer owner only
- delete: Timer owner or Admin
organizations/{orgId}/agent_runs/{runId}
- read:
belongsToOrg(orgId)
- create/update: Admin or SuperAdmin
- delete: SuperAdmin only
organizations/{orgId}/credit_reservations/{reservationId}
- read:
belongsToOrg(orgId)
- create/update: Admin or SuperAdmin
- delete: SuperAdmin only
organizations/{orgId}/moduleInstallations/{moduleId}
- read:
belongsToOrg(orgId)
- create/update: Admin or SuperAdmin
- delete: SuperAdmin only
organizations/{orgId}/modules/{moduleId}/{document=**}
- read:
belongsToOrg(orgId)
- write:
belongsToOrg(orgId) (module-specific permissions enforced at app layer)
organizations/{orgId}/complianceSummaries/{summaryId}
- read:
belongsToOrg(orgId)
- create/update:
belongsToOrg(orgId)
- delete: Admin or SuperAdmin
organizations/{orgId}/submissionMetrics/{metricId}
- read:
belongsToOrg(orgId)
- create/update:
belongsToOrg(orgId)
- delete: Admin or SuperAdmin
organizations/{orgId}/complianceAlerts/{alertId}
- read:
belongsToOrg(orgId)
- create/update:
belongsToOrg(orgId)
- delete: Admin or SuperAdmin
organizations/{orgId}/reportTemplates/{templateId}
- read/list:
belongsToOrg(orgId)
- create/update/delete: Manager+ and
belongsToOrg(orgId)
organizations/{orgId}/intelligenceCache/{docId}
- read:
belongsToOrg(orgId)
- write: Denied (Cloud Functions only via Admin SDK)
People & Employees
{peopleCollection}/{employeeId} (where peopleCollection in ['people', 'employees'])
- read: Own document, SuperAdmin, or org member
- create: SuperAdmin, Admin, or authenticated user accepting invitation (validated via
invitations/{invitationId})
- update: SuperAdmin, Admin (full access), owner (cannot change
organizationId, systemRole, bsn, bankAccount), or Manager (cannot change bsn, bankAccount)
- delete: SuperAdmin or Admin
- Notes: BSN and bank account are encrypted at storage level;
systemRole and organizationId are immutable for self-service updates
Projects
projects/{projectId}
- read: Authenticated +
belongsToOrg(resource.data.organizationId)
- create: SuperAdmin or Manager+ (org must be active)
- update: SuperAdmin or Manager+ (cannot change
organizationId)
- delete: SuperAdmin or Admin
projects/{projectId}/phase_history/{transitionId}
- read: Authenticated + belongs to project’s org
- create: Manager+ in project’s org; requires
fromPhase, toPhase, transitionedAt, transitionedBy fields
- update: Never (immutable)
- delete: Never (immutable audit trail)
Journals / Timesheets
journals/{journalId} (two match blocks exist — one with userId, one with employeeId)
- read: Authenticated + org member
- create: SuperAdmin or owner creating own entry (org must be active)
- update: SuperAdmin, owner (if not locked, cannot change org), or Manager+ for approval
- delete: SuperAdmin, owner (if not locked), or Admin
timesheets/{journalId} (legacy alias)
- Same pattern as journals; supports both
userId and employeeId fields for backward compatibility
journalSubmissions/{retroId} (Monthly Journal Submissions)
- read: Owner or Manager+
- create: Employee creating own submission in org
- update: Owner (if status is
Draft) or Manager+ for approval
- delete: Owner (if
Draft) or Admin
journalSubmissions/{retroId}/history/{historyId}
- read: Submission owner or Manager+
- create: Org member who is the submission owner or Manager+
journalGenerationJobs/{jobId}
- read: Owner or Manager+
- create: Owner in org
- update: Owner (cannot change
employeeId/organizationId) or Manager+
- delete: Owner or Admin
Leave Management
leave-balances/{balanceId} (appears twice — merged rules)
- get: Authenticated; own balance or org member (handles
resource == null for non-existent docs)
- list: Authenticated + belongs to own org
- create/update: Manager+ or system; cannot change
employeeId
- delete: Admin only
Expenses
expenses/{expenseId}
- read: Authenticated + org member
- create: SuperAdmin or owner creating own expense (org must be active)
- update: SuperAdmin, owner (if status !=
approved, cannot change org), or Manager+ for approval
- delete: SuperAdmin, owner (if not approved), or Admin
expenseAllocations/{allocationId}
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Admin only
Deadlines / Milestones
{milestoneCollection}/{deadlineId} (where milestoneCollection in ['milestones', 'deadlines'])
- read: Authenticated + org member or Auditor
- create: SuperAdmin, Manager+ (org active), or system-generated (
createdBy == 'system')
- update: SuperAdmin, Manager+, assignee (if in
assignedTo array), or system
- delete: SuperAdmin, Admin, or creator of custom deadlines
Compliance & Rule Engine
compliance_rules/{ruleId}
- read: Authenticated; platform defaults readable by all, org-scoped rules by org members or auditors
- create: SuperAdmin or Manager+ in org (org active)
- update: SuperAdmin or Manager+ (cannot change
organizationId)
- delete: SuperAdmin or Admin
rule_templates/{templateId}
- read: Authenticated; public templates by all, org-scoped by org members
- create: SuperAdmin or Manager+
- update: SuperAdmin or Manager+ (org templates only, cannot change org)
- delete: SuperAdmin (public) or Admin (org templates)
grant_rules/{junctionId}
- read: Authenticated + org member
- create: SuperAdmin or Manager+ (org active)
- update: SuperAdmin or Manager+ (cannot change org)
- delete: SuperAdmin or Admin
rule_violations/{violationId}
- read: Manager+ or Auditor
- create: SuperAdmin, Manager+ in org, or system
- update: SuperAdmin or Manager+ (cannot change org)
- delete: SuperAdmin or Admin
rule_analytics/{metricsId}
- read: Authenticated + Manager+
- create/update: SuperAdmin or system (
createdBy == 'system')
- delete: SuperAdmin only
- read: Any authenticated user
- create/update/delete: SuperAdmin only
- Notes: Platform-wide resource, no
organizationId
tenant_donor_configs/{orgId}
- read: Org member or Auditor
- create: SuperAdmin or Manager+ (doc ID must match org ID)
- update: SuperAdmin or Manager+ (cannot change org)
- delete: SuperAdmin only
complianceSummaries/{summaryId} (top-level)
- read: Authenticated + org member
- create/update: SuperAdmin or authenticated
- delete: SuperAdmin only
compliance_summaries/{summaryId} (snake_case variant)
compliance_alerts/{alertId} / compliance-alerts/{alertId} / complianceAlerts/{alertId}
- read: Authenticated + org member
- create/update: Authenticated + org member (for acknowledgment)
- delete: Admin or SuperAdmin
- Notes: Three naming variants exist for backward compatibility
complianceRequirements/{requirementId}
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Admin in org
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Admin in org
compliance-tasks/{taskId}
- read: Org member; task assignee or Manager+
- create: Manager+ or system
- update: Org member; assignee or Manager+
- delete: Admin only
- read: Any authenticated user
- create/update/delete: SuperAdmin only
Invitations
invitations/{invitationId}
- read: Invitee (by email match) or Admin
- create: Admin (org must be active)
- update: Invitee (accepting) or Admin
- delete: Admin
Audit & Security
auditLogs/{logId}
- read: SuperAdmin, Admin, or Auditor
- create: SuperAdmin or any authenticated user
- update/delete: Never (immutable)
audit_logs/{logId} (snake_case variant)
- read: SuperAdmin or Admin in org
- create: Any authenticated user
- update/delete: Never (immutable)
securityEvents/{eventId}
- read: SuperAdmin, Admin of org, or event owner
- create:
true (allows pre-auth logging)
- update/delete: Never (immutable)
securityAlertConfigs/{configId}
- read:
true (needed for login threshold checks)
- create/update: SuperAdmin only
- delete: SuperAdmin only
loginAttempts/{attemptId}
- read: SuperAdmin or own attempts
- create:
true (pre-auth)
- update/delete: Never (immutable)
userSessions/{sessionId}
- read: SuperAdmin or own sessions
- create: Authenticated, own session
- update: SuperAdmin or own session
- delete: SuperAdmin or own session
imuserationSessions/{sessionId} (Impersonation Sessions)
- read: SuperAdmin who created the session
- create: SuperAdmin, must set own
superAdminId
- update: SuperAdmin who owns the session
- delete: Never (audit trail)
auditorAccessGrants/{grantId}
- read: Admin, active auditor, or SuperAdmin
- create/update: Admin only
- delete: Never (audit trail)
configurationSnapshots/{snapshotId}
- read: Auditor, Admin, or SuperAdmin
- create: Auditor or Admin, must set
createdBy to own UID
- update/delete: Never (immutable)
auditExportJobs/{jobId}
- read: Requester, Auditor, Admin, or SuperAdmin
- create: Auditor or Admin, must set
requestedBy to own UID
- update: Never (server-side only)
- delete: Never (audit trail)
securityPolicies/{policyId}
- read: Admin (for
org_* policies) or SuperAdmin
- create/update: Admin (for
org_*) or SuperAdmin (for global)
- delete: SuperAdmin only
Onboarding
onboardingChecklists/{checklistId}
- read: User (for
user_* checklists) or Admin (for org_*)
- create: SuperAdmin (for
org_*) or user (for user_*)
- update: User (own
user_* checklist) or Admin (org_*)
- delete: Never
Team & Role Management
teamMemberRequests/{requestId}
- list: Any authenticated user
- get: Request creator or Admin
- create: Manager+; must set
requestedBy to own UID, status = pending, org must be active
- update: Admin only; preserves immutable fields (
requestedBy, proposedName, proposedEmail, proposedRole)
- delete: Never
roleChangeRecommendations/{recommendationId}
- list: Any authenticated user
- get: Recommender, affected employee, or Admin
- create: Manager+; validates employee exists, status =
pending
- update: Admin only; preserves immutable fields
- delete: Never
Referrals & Gamification
referrals/{referralId}
- get: SuperAdmin or org member
- list: Any authenticated user
- create: SuperAdmin or authenticated org member
- update/delete: SuperAdmin or Admin
missionCredits/{userId}
- get: Own wallet or SuperAdmin
- list: Any authenticated user (leaderboard)
- create/update: SuperAdmin only
- delete: Never
creditTransactions/{transactionId}
- read/list: Own transactions or SuperAdmin / any authenticated
- create: SuperAdmin only
- update/delete: Never (immutable)
creditRedemptions/{redemptionId}
- read/list: Own redemptions, Admin of org, or SuperAdmin
- create: Authenticated user; own
userId, status = pending
- update/delete: SuperAdmin only
badges/{badgeId}
- read/list: Own badges, Admin of org, or SuperAdmin
- create/update: Never (server-side only)
- delete: Never
gamificationPreferences/{userId}
- read: Own or Admin/SuperAdmin
- create/update: Own only
- delete: Own or SuperAdmin
campaigns/{campaignId}
- read/list: Any authenticated user
- create/update: Admin or SuperAdmin
- delete: SuperAdmin only
Notifications
notifications/{notificationId}
- read/list: SuperAdmin (all), recipient, or platform-wide (
recipientId == 'all')
- create: SuperAdmin or authenticated user (must include
organizationId)
- update: SuperAdmin or recipient (marking as read)
- delete: SuperAdmin, recipient, or Admin of org
fcmTokens/{tokenId}
- read/list: SuperAdmin, token owner, or authenticated
- create: Authenticated; own
employeeId, must include organizationId
- update: Own token or SuperAdmin; cannot change
employeeId
- delete: Own token or SuperAdmin
notificationDeliveries/{deliveryId}
- read/list: SuperAdmin, recipient, or authenticated
- create: Authenticated; must include
organizationId and recipientId
- update/delete: SuperAdmin only
Configuration
config/{configId}
- read: Any authenticated user
- create/update/delete: SuperAdmin only
- read:
true (unauthenticated — needed for login page branding)
- create/update: SuperAdmin or Admin
- delete: Never
Donors / Foundations
foundations/{foundationId}
- read: Any authenticated user
- create: SuperAdmin or authenticated user (sets
createdBy)
- update: SuperAdmin or any authenticated user
- delete: SuperAdmin only
Grant Management
grantOpportunities/{opportunityId}
- read: Any authenticated user
- create/update/delete: Admin or SuperAdmin
{trackerCollection}/{pipelineId} (where trackerCollection in ['grantTracker', 'grantPipeline'])
- read: Authenticated + org member
- create: Authenticated + org member
- update: Assigned team member (in
assignedTo) or Manager+
- delete: Admin + org member
Subcollections: tasks/{taskId}, documents/{documentId}
- read/write: Authenticated + belongs to parent pipeline’s org
Subcollection: activities/{activityId}
- read: Authenticated + belongs to parent pipeline’s org
- create: Same
- update/delete: Never (immutable)
grantApplications/{applicationId}
- read: Authenticated + org member
- create: Authenticated + org member
- update: Creator or Manager+
- delete: Admin + org member
activeGrants/{grantId}
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Admin in org
Risk Module
risks/{riskId}, riskMitigations/{mitigationId}, riskEscalations/{escalationId}
- read: Authenticated + org member
- create: Authenticated + org member (org active) + schema validation function
- update: Authenticated + org member (cannot change org) + schema validation
- delete: Admin + org member
- Notes: Reference validation functions (
isValidRisksDoc, etc.) that enforce schema at the rules level
Marketing Module
marketingPages/{pageId}, mediaAssets/{assetId}, publishingSchedules/{scheduleId}
- Standard org-scoped pattern: read by org members, create/update by org members with validation, delete by Admin
Referral Module (Program/Tracking/Rewards)
referralPrograms/{programId}, referralTracking/{trackingId}, referralRewards/{rewardId}
- Standard org-scoped pattern with validation functions
Support Module
tutorials/{tutorialId}, kbArticles/{articleId}, supportVideos/{videoId}
- Standard org-scoped pattern with validation functions
Pending Signups
pendingSignups/{email}
- read: Never
- create: Anyone (requires
referralCode + timestamp fields)
- update/delete: Never (Cloud Functions process and clean up)
API Keys & Activity Feeds
apiKeys/{keyId}
- read: Org member + API key permission
- list: SuperAdmin, Admin, or
MANAGE_API_KEYS permission
- create: Org member + API key permission
- update: Same + cannot change
organizationId
- delete: Org member + API key permission
activityFeeds/{employeeId}/monthly/{monthId}
- read: SuperAdmin, own data, or Admin in org
- create/update: SuperAdmin, own data, or authenticated with org
- delete: SuperAdmin, own data, or Admin in org
HR Administration
cao-rules/{ruleId}
- read: Authenticated + org member
- create/update/delete: Admin only
payroll-exports/{exportId}
- read: Admin + org member (sensitive salary data)
- create/update/delete: Admin only (update preserves org)
- read/create/update/delete:
true (open for development/seeding)
- Notes: These are intentionally permissive for emulator development
- read: Any authenticated user
- create/update/delete: SuperAdmin only
- read: Any authenticated user (marketplace browsing)
- create/update/delete: SuperAdmin only
RAG System
document-chunks/{chunkId}
- read: Authenticated + org member
- write: Never (Cloud Functions only via Admin SDK)
processed-documents/{docId}
- read: Authenticated + org member
- create: Authenticated + org member
- update: Authenticated + org member (cannot change org)
- delete: Manager+ in org
ai-query-logs/{logId}
- read: Authenticated + org member; own logs or Manager+
- write: Never (Cloud Functions only)
query-cache/{cacheId}
- read: Authenticated + org member
- write: Never (Cloud Functions only)
activity-rules/{ruleId}, project-deadlines/{deadlineId}
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Admin in org
Partner Member Benefit Program
partnerOrganizations/{partnerId}
- read: SuperAdmin only
- create: SuperAdmin; must set
createdBy to own UID
- update: SuperAdmin; cannot change
createdBy
- delete: Never (soft delete via status)
memberInvitations/{invitationId}
- read: SuperAdmin or any authenticated user (for invitation code validation)
- list: SuperAdmin only (prevents enumeration)
- create: SuperAdmin; validates partner org exists
- update: SuperAdmin or authenticated user (limited to status/timestamp updates, core fields immutable)
- delete: Never (audit trail)
partnershipMetrics/{metricId}
- read: SuperAdmin only
- write: Never (Cloud Functions only)
Reporting
schedules/{scheduleId}
- read: Authenticated + org member
- create/update/delete: Manager+ in org
reportTemplates/{templateId} (top-level)
- read: Authenticated + org member
- list: Any authenticated user
- create/update/delete: Manager+ in org
NGO HR & Staff Allocation
volunteers/{volunteerId}
- read: Org members
- create/update: Manager+ (own org or SuperAdmin)
- delete: Admin only
staffAllocations/{allocationId}
- read: Org member; Manager+ or own allocation
- write: Never (computed server-side)
payrollExports/{exportId} (camelCase variant)
- read: Admin or SuperAdmin in org
- create: Admin in org or SuperAdmin
- update: Admin (cannot change org)
- delete: Admin (draft exports only)
accrualBalances/{balanceId}
- read: Admin or own balance in org
- write: Never (computed server-side)
Relations Module (CRM)
- read: Org member + Manager+ or
VIEW_RELATIONS permission
- create:
CREATE_CONTACT permission; validates required fields, createdBy/updatedBy must match caller
- update:
EDIT_CONTACT permission; cannot change org, createdAt, createdBy; must set updatedBy
- delete:
DELETE_CONTACT permission + org member
interactions/{interactionId}
- read: Org member + Manager+ or
VIEW_RELATIONS permission
- create: Manager+ or
CREATE_CONTACT permission; validates required fields
- update: Manager+ or
EDIT_CONTACT permission; cannot change org, contact, or immutable audit fields
- delete: Manager+ or
DELETE_CONTACT permission + org member
- read: Org member +
VIEW_RELATIONS permission; own views or shared views
- create:
CREATE_CONTACT permission; enforces owner
- update: Own views +
EDIT_CONTACT permission
- delete: Own views or Admin
- read: Org member + Manager+ or
VIEW_RELATIONS
- create/update/delete: Manager+ or
MANAGE_CONTACT_SEGMENTS or relevant CRUD permission
HubSpot Integration
hubspotConfigs/{organizationId}
- read: Admin of org or SuperAdmin
- create: Admin; doc ID must match org ID; validates required fields (tokens, portal ID, sync settings)
- update: Admin; cannot change org
- delete: Admin of org or SuperAdmin
hubspotSyncLogs/{logId}
- read: Admin or
VIEW_RELATIONS permission + org member
- create: Admin; validates extensive required fields
- update: Never (immutable audit log)
- delete: Admin + org member
All under platform/intelligence/ path:
- read/create/update: Admin of the org
- delete: Never
- read: SuperAdmin only
- write: Never (Cloud Functions only)
- read: Authenticated user whose org has opted in (
consent/{orgId}.optedIn == true)
- write: Never (Cloud Functions only)
- read: SuperAdmin only
- write: Never (Cloud Functions only)
Document Library
document-library/{documentId}
- read: Authenticated + org member
- create: Authenticated; own org, org must be active
- update: Org member; creator or Admin; cannot change org
- delete: Admin + org member
document-folders/{folderId}
- read: Authenticated + org member
- create: Manager+ in org
- update: Creator or Admin; cannot change org
- delete: Admin only
document-templates/{templateId}
- read: Authenticated; platform-scoped or org member
- create: Admin; scope must be
organization
- update/delete: Admin + org member
Stakeholder Portal
portalTokens/{tokenId}
- read: SuperAdmin or Manager+ in org
- create: Manager+ in org; sets
createdBy, isActive = true
- update: Manager+ in org; cannot change
organizationId or projectId
- delete: Never (soft delete via
isActive = false)
portalSessions/{sessionId}
- read: SuperAdmin or Admin in org
- create/update: Never (Cloud Function only)
- delete: Never (audit trail)
- read: SuperAdmin or org member
- create: Authenticated org member;
authorType must be organization, authorId must be caller
- update: Author (editing) or org member marking stakeholder comments as read
- delete: Admin + org member
portalReportAcknowledgments/{ackId}
- read: Org member
- create: Never (Cloud Function only)
- update: Manager+ in org
- delete: Never
portalSubmissions/{submissionId}
- read: Org member
- create: Never (Cloud Function only)
- update: Manager+ in org (review)
- delete: Never (audit trail)
Grantor Relationship Management
grantorInteractions/{interactionId}
- read: Org member or Auditor
- create: Authenticated + org member
- update: Creator or Manager+ in org
- delete: Admin + org member
reportingDeadlines/{deadlineId}
- read: Org member or Auditor
- create: Authenticated + org member
- update: Creator, assignee, or Manager+
- delete: Admin, or creator (if not completed)
M&E (Monitoring & Evaluation)
meIndicators/{indicatorId}
- read: Authenticated + org member
- create: SuperAdmin or authenticated org member (org active)
- update: SuperAdmin or creator/Manager+ (cannot change org)
- delete: Admin + org member
meIndicatorTargets/{targetId}, meDataPoints/{dataPointId}
- read: Authenticated + org member
- create: SuperAdmin or authenticated org member (org active)
- update: SuperAdmin or creator/Manager+ (cannot change org)
- delete: Creator or Manager+
meDashboardConfigs/{configId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
meKoboConnections/{connectionId}
- read: Authenticated + org member
- create/update/delete: Admin only (stores API tokens)
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Creator or Manager+
meIndicatorSummaries/{summaryId}
- read: Authenticated + org member
- write: Never (Cloud Functions only)
meGrantIndicatorConfigs/{configId}
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Creator or Manager+
meAISuggestions/{suggestionId}
- read: Authenticated + org member
- create/update: Manager+ in org
- delete: Never (audit trail)
meWorkflowRules/{ruleId}
- read: Authenticated + org member
- create/update/delete: Admin in org
meWorkflowExecutionLogs/{logId}
- read: Authenticated + org member
- write: Never (Cloud Functions only)
Grant Calendar Extension
grantCalendarEvents/{eventId}
- read: Authenticated + org member
- create: SuperAdmin or authenticated org member (org active)
- update: SuperAdmin or creator/Manager+ (cannot change org)
- delete: Admin + org member
grantCalendarSettings/{settingsId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
Donor Wallet Extension
donorWallets/{walletId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
walletTransactions/{txId}
- read: Authenticated + org member
- create: SuperAdmin or authenticated org member (org active)
- update: SuperAdmin or Manager+ (cannot change org)
- delete: Admin + org member
walletAllocations/{allocId}
- read: Authenticated + org member
- create: SuperAdmin or authenticated org member (org active)
- update: SuperAdmin or creator/Manager+
- delete: Admin + org member
ngoDirectory/{ngoId}, donorWalletSettings/{settingsId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
matchingPrograms/{programId}
- read: Authenticated + org member
- create/update: SuperAdmin or Admin in org
- delete: Admin + org member
Virtual Giving Card Extension
virtualCards/{cardId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
cardTransactions/{txId}
- read: Authenticated + org member
- create: SuperAdmin or authenticated org member (org active)
- update: SuperAdmin or Manager+ (cannot change org)
- delete: Admin + org member
cardSettings/{settingsId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
Corporate CSR Hub Extension
corporateAccounts/{accountId}
- read: Authenticated + org member
- create/update: SuperAdmin or Admin in org
- delete: SuperAdmin only
employeeDonations/{donationId}
- read: Authenticated + org member
- create: Authenticated org member (org active)
- update: SuperAdmin or Manager+ (cannot change org)
- delete: Admin + org member
matchingTransactions/{txId}, matchingRules/{ruleId}
- read: Authenticated + org member
- create: SuperAdmin or Admin/Manager+ in org
- update: SuperAdmin or Admin (cannot change org)
- delete: Admin + org member
csrCampaigns/{campaignId}, csrImpactReports/{reportId}
- read: Authenticated + org member
- create/update: SuperAdmin or Manager+ in org
- delete: Admin + org member
ergGroups/{groupId}
- read: Authenticated + org member
- create: Authenticated org member (org active)
- update: SuperAdmin, Manager+, or group leader (
leaderId)
- delete: Admin + org member
csrSettings/{settingsId}
- read: Authenticated + org member
- create/update: SuperAdmin or Admin in org
- delete: SuperAdmin only
Workflow / Orchestration
approvalRoutingConfigs/{configId}, delegationSettings/{settingId}, notificationTemplateOverrides/{templateId}
- read: Authenticated + Admin
- create/update: SuperAdmin or Admin
- delete: SuperAdmin only
- read: Authenticated + Admin
- create/update/delete: SuperAdmin or Admin
- read: Authenticated + Admin
- create: SuperAdmin or Admin
- update/delete: SuperAdmin only
Durable Eventing
systemEvents/{eventId}, extensionEvents/{eventId}
- read: Authenticated + org member
- create: Authenticated + org member;
userId must match caller
- update: Authenticated + org member + event owner; cannot change
organizationId, userId, or type
- delete: Never (append-only)
eventOutbox/{eventId}
- read: Authenticated; Admin or org member
- create: Authenticated + org member;
userId must match caller
- update: Same as system events (owner only, immutable core fields)
- delete: Never
eventConsumptions/{consumptionId}, eventDlq/{eventId}
- read: Authenticated + Admin
- write: Never (backend managed via Admin SDK)
Default Deny
Any collection not explicitly matched above is denied by default (Firestore’s implicit deny behavior). No catch-all allow rule exists.
Storage Rules (storage.rules)
Helper Functions
| Function | Purpose |
|---|
isAuthenticated() | request.auth != null |
hasPeopleProfile() | Checks if /people/{uid} exists in Firestore |
getEmployee() | Returns people/{uid} doc (preferred) or falls back to employees/{uid} |
belongsToOrg(orgId) | Authenticated + employee doc’s organizationId matches |
isAdmin() | systemRole is Admin or Super Admin |
isSuperAdmin() | systemRole is Super Admin |
Storage Path Rules
| Path | Read | Write | Delete |
|---|
avatars/{userId}/{fileName} | Any authenticated | Own avatar only | Own avatar only |
logos/{orgId}/{fileName} | Org member or SuperAdmin | SuperAdmin or Admin in org | SuperAdmin or Admin in org |
contacts/{orgId}/{fileName} | Org member | Org member | Org member |
platform/{fileName} | Any authenticated | SuperAdmin only | SuperAdmin only |
organizations/{orgId}/projects/{projectId}/{**} | Org member | Admin in org | Admin in org |
expenses/{orgId}/{expenseId}/{fileName} | Org member | Org member | Org member |
receipts/{orgId}/{projectId}/{expenseId}/{fileName} | Org member (legacy) | Org member | Org member |
project-documents/{orgId}/{projectId}/{fileName} | Org member | Org member | Admin in org |
portal-submissions/{orgId}/{projectId}/{submissionId}/{fileName} | Org member | Never (signed URLs only) | Admin in org |
{allPaths=**} (default) | Never | Never | Never |
Tenant Isolation Pattern
Every rule enforces tenant isolation through one of three patterns:
Pattern 1: Direct organizationId Field
Most top-level collections store organizationId directly on each document. Rules validate:
resource.data.organizationId == getUserOrganizationId()
via the belongsToOrg() helper. This is the most common pattern.
Pattern 2: Subcollection Under Organization
Collections nested under /organizations/{orgId}/ use the path parameter:
This covers config, widget overrides, timers, agent runs, credit reservations, module installations, and intelligence cache.
Pattern 3: User-Scoped Data with Org Check
Some collections (expenses, journals) validate both ownership and org membership:
isOwner(resource) && belongsToOrg(resource.data.organizationId)
Immutability of organizationId
All update rules that allow modifications include a check preventing organizationId changes:
request.resource.data.organizationId == resource.data.organizationId
This prevents documents from being moved between tenants.
Security Patterns
Role-Based Access Summary
| Operation | User | Manager | Admin | SuperAdmin |
|---|
| Read own data | Yes | Yes | Yes | Yes |
| Read org data | Yes | Yes | Yes | Yes |
| Create entries | Own only | Yes | Yes | Yes |
| Approve/reject | No | Yes | Yes | Yes |
| Delete records | Own (limited) | Some | Most | All |
| Manage config | No | No | Yes | Yes |
| Cross-org access | No | No | No | Yes |
Immutable Audit Collections
The following collections deny all updates and deletes to maintain audit integrity:
auditLogs, audit_logs
securityEvents
loginAttempts
creditTransactions
badges
imuserationSessions
auditorAccessGrants
configurationSnapshots
auditExportJobs
systemEvents, extensionEvents, eventOutbox
eventConsumptions, eventDlq
Cloud Function-Only Collections
These collections deny all client writes; data is managed exclusively through Admin SDK:
document-chunks
ai-query-logs
query-cache
partnershipMetrics
meIndicatorSummaries
meWorkflowExecutionLogs
platform/intelligence/snapshots
platform/intelligence/benchmarks
platform/intelligence/funderProfiles
platform/intelligence/predictions
platform/intelligence/trends
portalSessions
portalReportAcknowledgments (create only)
portalSubmissions (create only)
staffAllocations
accrualBalances
Time-Boxed Auditor Access
Auditor access is temporary and scoped:
- An Admin creates an
auditorAccessGrants/{uid}_{orgId} document with isActive and expiresAt
isAuditor() checks that the grant is active AND expiresAt > request.time
- Auditors get read-only access to compliance, audit, and financial collections
- Grants cannot be deleted (audit trail), only deactivated
Owner-Only with Lock Pattern
Used for journals and timesheets:
- Owners can update/delete their own entries only if
isLocked == false
- Once a manager approves (locks) an entry, the owner can no longer modify it
- Managers can always update for approval purposes
Pre-Authentication Rules
Some security monitoring collections allow unauthenticated writes:
securityEvents (create: true) — needed for login failure logging
loginAttempts (create: true) — needed before auth is established
securityAlertConfigs (read: true) — needed for threshold checks during login
platformConfig (read: true) — needed for branding on login page
pendingSignups (create with field validation) — referral code storage during signup
Maintenance
Update this document whenever firestore.rules or storage.rules is modified. Key areas to check:
- New collections require a new match block (default deny means they are inaccessible without one)
- New roles or permissions should be reflected in the helper functions section
- Subcollections under existing paths may need separate documentation
- Backward-compatible collection name variants (e.g.,
compliance_alerts vs complianceAlerts) should be noted