Recently, I worked on deploying a multi-tenant Progressive Web App (PWA) using Firebase as the backend platform. The application needed to support multiple organizations with different user roles, work offline, and maintain separate development, staging, and production environments.
Here's what I learned about Firebase infrastructure setup, custom claims for role-based access control, and multi-environment deployment strategies.
The Requirements
The application needed:
- Multi-tenant architecture - Multiple organizations using one platform
- Role-based access control - Different permission levels per organization
- Multi-environment deployment - Separate dev, staging, and production
- Offline-first capability - Works without internet connection
- Real-time updates - Data syncs instantly across devices
- Automated backups - Production data protection
Tech Stack Overview
- Frontend: React + TypeScript
- Backend: Firebase (Authentication, Firestore, Cloud Storage, Hosting)
- Build System: pnpm workspaces
- CI/CD: GitHub Actions + Firebase CLI
- Backup: Google Cloud Console
Multi-Environment Firebase Setup
I created three separate Firebase projects for proper environment isolation:
| Environment | Purpose | Deployment |
|---|---|---|
| Development | Local testing with emulator | Manual |
| Staging | Pre-production testing | Automated via GitHub Actions |
| Production | Live users | Automated via GitHub Actions |
Each project has completely isolated:
- Firebase Authentication users
- Firestore database
- Cloud Storage bucket
- Hosting deployment
Why separate projects instead of one project with different databases?
Firebase doesn't support multiple databases per project for the free tier, and separating projects provides:
- Complete isolation (no risk of staging affecting production)
- Independent security rules per environment
- Separate usage quotas
- Different service account permissions
Environment Configuration Pattern
Managing environment variables correctly was critical. Here's the structure:
app/
├── .env # Development (uses emulator)
├── .env.staging # Staging deployment
├── .env.production # Production deployment
└── src/
└── config/
└── firebase.ts # Firebase initialization
Development (.env):
REACT_APP_FIREBASE_PROJECT_ID=my-app-dev
REACT_APP_USE_EMULATOR=true
REACT_APP_FIREBASE_API_KEY=your-dev-api-key
REACT_APP_FIREBASE_AUTH_DOMAIN=my-app-dev.firebaseapp.com
Production (.env.production):
REACT_APP_FIREBASE_PROJECT_ID=my-app-production
REACT_APP_USE_EMULATOR=false
REACT_APP_FIREBASE_API_KEY=your-prod-api-key
REACT_APP_FIREBASE_AUTH_DOMAIN=my-app-production.firebaseapp.com
Firebase Custom Claims for Multi-Tenant RBAC
The key challenge was implementing role-based access control (RBAC) that works across multiple organizations.
Why Custom Claims?
Instead of storing user roles in Firestore and checking them on every request, I used Firebase Custom Claims:
✅ Built into ID tokens - No extra database reads
✅ Enforced at security rules level - Server-side validation
✅ Multi-tenant support - One user, multiple organizations, different roles
✅ Works offline - Cached in the client
Custom Claims Structure
interface CustomClaims {
isAdmin?: boolean; // Platform super-admin
organizations?: Record<OrgId, Role>; // Org-specific roles
}
// Example: User with different roles in two organizations
{
"isAdmin": false,
"organizations": {
"org_abc123": "admin",
"org_xyz789": "viewer"
}
}
Setting Custom Claims (Firebase Admin SDK)
I created a helper script for managing custom claims:
// scripts/set-user-claims.js
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert('./service-account.json')
});
async function setUserClaims(email, organizations) {
try {
const user = await admin.auth().getUserByEmail(email);
await admin.auth().setCustomUserClaims(user.uid, {
isAdmin: false,
organizations: organizations
});
console.log(`✅ Claims updated for ${email}`);
} catch (error) {
console.error('❌ Error:', error.message);
}
}
// Usage
setUserClaims('user@example.com', {
'org_abc123': 'admin'
});
Important: Custom claims have a 1,000 byte limit per user. For large-scale multi-tenant systems, consider storing detailed permissions in Firestore and using claims only for organization IDs.
Firestore Security Rules with Custom Claims
Security rules enforce multi-tenant isolation using custom claims:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper function to check organization access
function hasOrgAccess(orgId, allowedRoles) {
return request.auth != null &&
request.auth.token.organizations[orgId] in allowedRoles;
}
// Organization data - only accessible by members
match /organizations/{orgId} {
allow read: if hasOrgAccess(orgId, ['admin', 'editor', 'viewer']);
allow write: if hasOrgAccess(orgId, ['admin']);
}
// Organization documents - different access levels
match /organizations/{orgId}/documents/{docId} {
allow read: if hasOrgAccess(orgId, ['admin', 'editor', 'viewer']);
allow create, update: if hasOrgAccess(orgId, ['admin', 'editor']);
allow delete: if hasOrgAccess(orgId, ['admin']);
}
}
}
Testing security rules: Always use Firebase Console → Firestore → Rules → Rules Playground before deploying to production.
GitHub Actions Deployment Pipeline
Automated deployment to staging when pushing to the staging branch:
# .github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
push:
branches: [ staging ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build for staging
run: |
cp .env.staging .env
pnpm build
- name: Deploy to Firebase Hosting
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STAGING }}'
projectId: my-app-staging
channelId: live
Pro tip: Store Firebase service account JSON as GitHub secrets, not in your repository.
Production Backup Strategy
I implemented three backup layers for production Firestore:
1. Daily Automated Backups (7-day retention)
gcloud firestore backups schedules create \
--database='(default)' \
--recurrence=daily \
--retention=7d
2. Weekly Automated Backups (8-week retention)
gcloud firestore backups schedules create \
--database='(default)' \
--recurrence=weekly \
--retention=8w \
--day-of-week=SUN
3. Monthly Exports to Cloud Storage (365-day retention)
# Create storage bucket
gsutil mb -l us-central1 gs://my-app-backups
# Create Cloud Scheduler job for monthly exports
gcloud scheduler jobs create http firestore-monthly-export \
--location=us-central1 \
--schedule="0 2 1 * *" \
--uri="https://firestore.googleapis.com/v1/projects/my-app-production/databases/(default):exportDocuments" \
--http-method=POST \
--oauth-service-account-email=backup-sa@my-app-production.iam.gserviceaccount.com \
--headers="Content-Type=application/json" \
--message-body='{"outputUriPrefix":"gs://my-app-backups/monthly"}'
Cost: Native backups are free (pay only on restore). Cloud Storage exports cost ~$0.02-0.05/GB/month. Total estimated cost: under $5/month for most applications.
Five Key Lessons Learned
1. Environment Variables Must Be Explicit
Don't rely on build scripts to automatically switch .env files. Always explicitly copy the correct environment file:
# WRONG - relies on environment detection
pnpm build
# RIGHT - explicit environment file
cp .env.production .env && pnpm build
I learned this the hard way when staging accidentally connected to production Firebase.
2. Firebase Emulator is Essential
The Firebase Emulator Suite is not optional for development:
firebase emulators:start --only auth,firestore,storage
Benefits:
- No API quota consumption
- Fast reset (just restart)
- Offline development
- No accidental production writes
3. Custom Claims Require Admin SDK
Firebase Console has no UI for custom claims. You must use Firebase Admin SDK or create helper scripts.
I maintain a firebase-admin-scripts/ directory with:
-
set-claims.js- Set user custom claims -
get-claims.js- View user's current claims -
list-users.js- List all users with claims
4. Security Rules Need Comprehensive Testing
Write security rules tests alongside your rules:
// firestore.test.js (using @firebase/rules-unit-testing)
describe('Organization access', () => {
it('should allow org member to read documents', async () => {
const db = getFirestore(testEnv, {
uid: 'user123',
token: { organizations: { org_abc: 'viewer' } }
});
const doc = db.collection('organizations').doc('org_abc');
await assertSucceeds(doc.get());
});
it('should deny non-member access', async () => {
const db = getFirestore(testEnv, { uid: 'user456' });
const doc = db.collection('organizations').doc('org_abc');
await assertFails(doc.get());
});
});
5. Document Your Deployment Process
Create a DEPLOYMENT.md file documenting:
- How to deploy to each environment
- Environment variable setup
- Service account configuration
- Rollback procedures
- Common troubleshooting steps
Your future self (and team members) will thank you.
Key Takeaways
If you're building a multi-tenant Firebase application, here's what matters most:
- Use separate Firebase projects per environment - Don't share databases between dev/staging/prod
- Leverage custom claims for RBAC - More secure than Firestore lookups, works offline
- Implement multiple backup layers - Daily, weekly, and monthly with different retention periods
- Test security rules rigorously - Use Rules Playground and unit tests
- Automate deployments - GitHub Actions or similar CI/CD
- Use Firebase Emulator for development - Saves quota and prevents production accidents
- Create Admin SDK helper scripts - Manual custom claims management gets tedious fast
Conclusion
Firebase is powerful for building multi-tenant applications, but proper infrastructure setup is crucial. The key is treating Firebase like any production infrastructure:
- Multiple isolated environments
- Automated deployments
- Comprehensive backups
- Strong security rules
- Clear documentation
Invest time upfront in environment configuration, custom claims architecture, and backup automation. It pays off when you need to debug authentication issues, restore data, or onboard new team members.
Have you built multi-tenant applications with Firebase? What challenges did you face with custom claims or security rules? Share your experience in the comments!
Useful Resources
- Firebase Custom Claims Documentation
- Firestore Security Rules Guide
- Firebase Emulator Suite
- Firestore Backup Schedules
- Testing Security Rules
Questions about Firebase infrastructure or DevOps? Connect with me on LinkedIn or visit my portfolio.
Top comments (0)