<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: hello world_leo</title>
    <description>The latest articles on DEV Community by hello world_leo (@leo_rio).</description>
    <link>https://dev.to/leo_rio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3395052%2Ffd80024d-a2a7-43d5-aebb-7b76f5bf5044.png</url>
      <title>DEV Community: hello world_leo</title>
      <link>https://dev.to/leo_rio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leo_rio"/>
    <language>en</language>
    <item>
      <title>Building a Multi-Tenant Firebase Application: Environment Setup and Role-Based Access Control</title>
      <dc:creator>hello world_leo</dc:creator>
      <pubDate>Sat, 14 Feb 2026 14:27:01 +0000</pubDate>
      <link>https://dev.to/leo_rio/managing-production-firebase-infrastructure-multi-environment-deployment-for-a-react-pwa-1nfp</link>
      <guid>https://dev.to/leo_rio/managing-production-firebase-infrastructure-multi-environment-deployment-for-a-react-pwa-1nfp</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's what I learned about Firebase infrastructure setup, custom claims for role-based access control, and multi-environment deployment strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Requirements
&lt;/h2&gt;

&lt;p&gt;The application needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant architecture&lt;/strong&gt; - Multiple organizations using one platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based access control&lt;/strong&gt; - Different permission levels per organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-environment deployment&lt;/strong&gt; - Separate dev, staging, and production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline-first capability&lt;/strong&gt; - Works without internet connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time updates&lt;/strong&gt; - Data syncs instantly across devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated backups&lt;/strong&gt; - Production data protection&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React + TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Firebase (Authentication, Firestore, Cloud Storage, Hosting)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build System:&lt;/strong&gt; pnpm workspaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD:&lt;/strong&gt; GitHub Actions + Firebase CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup:&lt;/strong&gt; Google Cloud Console&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multi-Environment Firebase Setup
&lt;/h2&gt;

&lt;p&gt;I created three separate Firebase projects for proper environment isolation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;Local testing with emulator&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging&lt;/td&gt;
&lt;td&gt;Pre-production testing&lt;/td&gt;
&lt;td&gt;Automated via GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Live users&lt;/td&gt;
&lt;td&gt;Automated via GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each project has completely isolated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firebase Authentication users&lt;/li&gt;
&lt;li&gt;Firestore database&lt;/li&gt;
&lt;li&gt;Cloud Storage bucket&lt;/li&gt;
&lt;li&gt;Hosting deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why separate projects instead of one project with different databases?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firebase doesn't support multiple databases per project for the free tier, and separating projects provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete isolation (no risk of staging affecting production)&lt;/li&gt;
&lt;li&gt;Independent security rules per environment&lt;/li&gt;
&lt;li&gt;Separate usage quotas&lt;/li&gt;
&lt;li&gt;Different service account permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Environment Configuration Pattern
&lt;/h2&gt;

&lt;p&gt;Managing environment variables correctly was critical. Here's the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;app/
├── .env                    &lt;span class="c"&gt;# Development (uses emulator)&lt;/span&gt;
├── .env.staging            &lt;span class="c"&gt;# Staging deployment&lt;/span&gt;
├── .env.production         &lt;span class="c"&gt;# Production deployment&lt;/span&gt;
└── src/
    └── config/
        └── firebase.ts     &lt;span class="c"&gt;# Firebase initialization&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Development (.env):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REACT_APP_FIREBASE_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-app-dev
&lt;span class="nv"&gt;REACT_APP_USE_EMULATOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;REACT_APP_FIREBASE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-dev-api-key
&lt;span class="nv"&gt;REACT_APP_FIREBASE_AUTH_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-app-dev.firebaseapp.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Production (.env.production):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REACT_APP_FIREBASE_PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-app-production
&lt;span class="nv"&gt;REACT_APP_USE_EMULATOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="nv"&gt;REACT_APP_FIREBASE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-prod-api-key
&lt;span class="nv"&gt;REACT_APP_FIREBASE_AUTH_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-app-production.firebaseapp.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Firebase Custom Claims for Multi-Tenant RBAC
&lt;/h2&gt;

&lt;p&gt;The key challenge was implementing role-based access control (RBAC) that works across multiple organizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Custom Claims?
&lt;/h3&gt;

&lt;p&gt;Instead of storing user roles in Firestore and checking them on every request, I used &lt;strong&gt;Firebase Custom Claims&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Built into ID tokens&lt;/strong&gt; - No extra database reads&lt;br&gt;
✅ &lt;strong&gt;Enforced at security rules level&lt;/strong&gt; - Server-side validation&lt;br&gt;
✅ &lt;strong&gt;Multi-tenant support&lt;/strong&gt; - One user, multiple organizations, different roles&lt;br&gt;
✅ &lt;strong&gt;Works offline&lt;/strong&gt; - Cached in the client&lt;/p&gt;
&lt;h3&gt;
  
  
  Custom Claims Structure
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CustomClaims&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// Platform super-admin&lt;/span&gt;
  &lt;span class="nl"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OrgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Org-specific roles&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Example: User with different roles in two organizations&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isAdmin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;organizations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_abc123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;org_xyz789&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Setting Custom Claims (Firebase Admin SDK)
&lt;/h3&gt;

&lt;p&gt;I created a helper script for managing custom claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/set-user-claims.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase-admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./service-account.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setUserClaims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getUserByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setCustomUserClaims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;organizations&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✅ Claims updated for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;❌ Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="nf"&gt;setUserClaims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;org_abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Custom claims have a &lt;strong&gt;1,000 byte limit&lt;/strong&gt; per user. For large-scale multi-tenant systems, consider storing detailed permissions in Firestore and using claims only for organization IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firestore Security Rules with Custom Claims
&lt;/h2&gt;

&lt;p&gt;Security rules enforce multi-tenant isolation using custom claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rules_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="nx"&gt;cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firestore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/documents &lt;/span&gt;&lt;span class="err"&gt;{
&lt;/span&gt;
    &lt;span class="c1"&gt;// Helper function to check organization access&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasOrgAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; 
             &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;allowedRoles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Organization data - only accessible by members&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;organizations&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasOrgAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasOrgAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Organization documents - different access levels&lt;/span&gt;
    &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;organizations&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/documents/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasOrgAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasOrgAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasOrgAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Testing security rules:&lt;/strong&gt; Always use Firebase Console → Firestore → Rules → Rules Playground before deploying to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions Deployment Pipeline
&lt;/h2&gt;

&lt;p&gt;Automated deployment to staging when pushing to the &lt;code&gt;staging&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-staging.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Staging&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;staging&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install pnpm&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g pnpm&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build for staging&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cp .env.staging .env&lt;/span&gt;
          &lt;span class="s"&gt;pnpm build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Firebase Hosting&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FirebaseExtended/action-hosting-deploy@v0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;repoToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.GITHUB_TOKEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;firebaseServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.FIREBASE_SERVICE_ACCOUNT_STAGING&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-staging&lt;/span&gt;
          &lt;span class="na"&gt;channelId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;live&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Store Firebase service account JSON as GitHub secrets, not in your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Backup Strategy
&lt;/h2&gt;

&lt;p&gt;I implemented three backup layers for production Firestore:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Daily Automated Backups (7-day retention)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud firestore backups schedules create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'(default)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--recurrence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;daily &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retention&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Weekly Automated Backups (8-week retention)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud firestore backups schedules create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'(default)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--recurrence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;weekly &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retention&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8w &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--day-of-week&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SUN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Monthly Exports to Cloud Storage (365-day retention)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create storage bucket&lt;/span&gt;
gsutil mb &lt;span class="nt"&gt;-l&lt;/span&gt; us-central1 gs://my-app-backups

&lt;span class="c"&gt;# Create Cloud Scheduler job for monthly exports&lt;/span&gt;
gcloud scheduler &lt;span class="nb"&gt;jobs &lt;/span&gt;create http firestore-monthly-export &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0 2 1 * *"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://firestore.googleapis.com/v1/projects/my-app-production/databases/(default):exportDocuments"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--oauth-service-account-email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;backup-sa@my-app-production.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type=application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message-body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"outputUriPrefix":"gs://my-app-backups/monthly"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Native backups are free (pay only on restore). Cloud Storage exports cost ~$0.02-0.05/GB/month. Total estimated cost: &lt;strong&gt;under $5/month&lt;/strong&gt; for most applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Key Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Environment Variables Must Be Explicit
&lt;/h3&gt;

&lt;p&gt;Don't rely on build scripts to automatically switch &lt;code&gt;.env&lt;/code&gt; files. Always explicitly copy the correct environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# WRONG - relies on environment detection&lt;/span&gt;
pnpm build

&lt;span class="c"&gt;# RIGHT - explicit environment file&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.production .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I learned this the hard way when staging accidentally connected to production Firebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Firebase Emulator is Essential
&lt;/h3&gt;

&lt;p&gt;The Firebase Emulator Suite is not optional for development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase emulators:start &lt;span class="nt"&gt;--only&lt;/span&gt; auth,firestore,storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No API quota consumption&lt;/li&gt;
&lt;li&gt;Fast reset (just restart)&lt;/li&gt;
&lt;li&gt;Offline development&lt;/li&gt;
&lt;li&gt;No accidental production writes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Custom Claims Require Admin SDK
&lt;/h3&gt;

&lt;p&gt;Firebase Console has &lt;strong&gt;no UI for custom claims&lt;/strong&gt;. You must use Firebase Admin SDK or create helper scripts.&lt;/p&gt;

&lt;p&gt;I maintain a &lt;code&gt;firebase-admin-scripts/&lt;/code&gt; directory with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;set-claims.js&lt;/code&gt; - Set user custom claims&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get-claims.js&lt;/code&gt; - View user's current claims
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list-users.js&lt;/code&gt; - List all users with claims&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Security Rules Need Comprehensive Testing
&lt;/h3&gt;

&lt;p&gt;Write security rules tests alongside your rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// firestore.test.js (using @firebase/rules-unit-testing)&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Organization access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should allow org member to read documents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organizations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;org_abc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;organizations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;org_abc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assertSucceeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should deny non-member access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user456&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;organizations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;org_abc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assertFails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Document Your Deployment Process
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;DEPLOYMENT.md&lt;/code&gt; file documenting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to deploy to each environment&lt;/li&gt;
&lt;li&gt;Environment variable setup&lt;/li&gt;
&lt;li&gt;Service account configuration&lt;/li&gt;
&lt;li&gt;Rollback procedures&lt;/li&gt;
&lt;li&gt;Common troubleshooting steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your future self (and team members) will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;If you're building a multi-tenant Firebase application, here's what matters most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use separate Firebase projects per environment&lt;/strong&gt; - Don't share databases between dev/staging/prod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage custom claims for RBAC&lt;/strong&gt; - More secure than Firestore lookups, works offline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement multiple backup layers&lt;/strong&gt; - Daily, weekly, and monthly with different retention periods&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test security rules rigorously&lt;/strong&gt; - Use Rules Playground and unit tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate deployments&lt;/strong&gt; - GitHub Actions or similar CI/CD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Firebase Emulator for development&lt;/strong&gt; - Saves quota and prevents production accidents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create Admin SDK helper scripts&lt;/strong&gt; - Manual custom claims management gets tedious fast&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Firebase is powerful for building multi-tenant applications, but proper infrastructure setup is crucial. The key is treating Firebase like any production infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple isolated environments&lt;/li&gt;
&lt;li&gt;Automated deployments&lt;/li&gt;
&lt;li&gt;Comprehensive backups&lt;/li&gt;
&lt;li&gt;Strong security rules&lt;/li&gt;
&lt;li&gt;Clear documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;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!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/auth/admin/custom-claims" rel="noopener noreferrer"&gt;Firebase Custom Claims Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/firestore/security/get-started" rel="noopener noreferrer"&gt;Firestore Security Rules Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/emulator-suite" rel="noopener noreferrer"&gt;Firebase Emulator Suite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/firestore/docs/backups" rel="noopener noreferrer"&gt;Firestore Backup Schedules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/rules/unit-tests" rel="noopener noreferrer"&gt;Testing Security Rules&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Questions about Firebase infrastructure or DevOps? Connect with me on &lt;a href="https://www.linkedin.com/in/leodandyy/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or visit my &lt;a href="https://leo-rio.com" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>react</category>
      <category>devops</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Emergency Server Recovery: A 4-Hour Race Against Time</title>
      <dc:creator>hello world_leo</dc:creator>
      <pubDate>Tue, 18 Nov 2025 15:57:06 +0000</pubDate>
      <link>https://dev.to/leo_rio/emergency-server-recovery-a-4-hour-race-against-time-1di5</link>
      <guid>https://dev.to/leo_rio/emergency-server-recovery-a-4-hour-race-against-time-1di5</guid>
      <description>&lt;h2&gt;
  
  
  The 3AM Wake-Up Call
&lt;/h2&gt;

&lt;p&gt;You know that feeling when your phone buzzes at an ungodly hour and your stomach drops? That was me, staring at a frantic message: "Site is showing weird content. Help!"&lt;/p&gt;

&lt;p&gt;I grabbed my laptop. The WordPress site was serving pharmaceutical spam to visitors. Classic compromise. The clock started ticking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hour 1: Damage Assessment (03:00 - 04:00)
&lt;/h2&gt;

&lt;p&gt;First rule of server emergencies: don't panic, but move fast.&lt;br&gt;
What I Did First&lt;br&gt;
SSH into the server, check if I still had access. Good news: credentials still worked. Bad news: everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Check recent file modifications

find /var/www/html -type f -mtime -7 -ls | head -20

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tons of suspicious PHP files scattered everywhere. The wp-content/uploads folder was full of backdoors. Someone had gotten in through an outdated plugin, probably.&lt;br&gt;
Quick Isolation&lt;br&gt;
Pulled the site offline with a maintenance page. Better to show "down for maintenance" than spam pills to your visitors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Quick nginx block

location / {

    return 503;

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Took a snapshot of everything before touching anything. You need evidence, and you might need to rollback if things go sideways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hour 2: The Cleanup (04:00 - 05:00)
&lt;/h2&gt;

&lt;p&gt;Finding the Entry Point&lt;br&gt;
Checked Apache/Nginx logs for unusual POST requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep -i "POST" /var/log/nginx/access.log | grep -E "\.(php|asp|jsp)" | tail -100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Found it. An old contact form plugin with a known vulnerability. They uploaded a shell through a file upload field that wasn't properly validated.&lt;br&gt;
Nuclear Option with Surgical Precision&lt;br&gt;
Here's the thing about compromised WordPress sites: you can't trust anything. But you also can't just delete everything because you need the data.&lt;/p&gt;

&lt;p&gt;My approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Backed up the database (even though it might be compromised)&lt;/li&gt;
&lt;li&gt;Downloaded all uploaded media files&lt;/li&gt;
&lt;li&gt;Saved the wp-config.php (to get database credentials)&lt;/li&gt;
&lt;li&gt;Nuked everything else
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#### Backup first

mysqldump -u dbuser -p dbname &amp;gt; backup_$(date +%Y%m%d_%H%M%S).sql

#### Fresh WordPress install

wget https://wordpress.org/latest.tar.gz

tar -xzf latest.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Database Surgery&lt;br&gt;
The database had malicious entries in these tables:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wp_options&lt;/code&gt; (autoload hooks)&lt;br&gt;
&lt;code&gt;wp_posts&lt;/code&gt; (spam content injected)&lt;br&gt;
&lt;code&gt;wp_users&lt;/code&gt; (unknown admin accounts)&lt;/p&gt;

&lt;p&gt;Cleaned them manually. Yes, manually. Running automated scripts on a compromised database is asking for trouble.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Remove suspicious admin users

SELECT * FROM wp_users WHERE user_login NOT IN ('known_admin_1', 'known_admin_2');

DELETE FROM wp_users WHERE ID = [suspicious_id];

-- Check for injected JavaScript in posts

SELECT ID, post_title FROM wp_posts 

WHERE post_content LIKE '%&amp;lt;script%' 

OR post_content LIKE '%iframe%';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hour 3: Hardening &amp;amp; Recovery (05:00 - 06:00)
&lt;/h2&gt;

&lt;p&gt;The Rebuild&lt;br&gt;
Fresh WordPress core, clean database, restored media files. Now comes the part most people skip: actually securing the thing.&lt;br&gt;
File Permissions That Make Sense&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Directories: 755

find /var/www/html -type d -exec chmod 755 {} \;

# Files: 644

find /var/www/html -type f -exec chmod 644 {} \;

# wp-config.php: 440

chmod 440 /var/www/html/wp-config.php

chown www-data:www-data /var/www/html/wp-config.php
Disable File Editing
Added to wp-config.php:

define('DISALLOW_FILE_EDIT', true);

define('DISALLOW_FILE_MODS', true);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more editing themes from the admin panel. If you need to update something, do it through SFTP like a proper developer.&lt;br&gt;
Web Application Firewall&lt;br&gt;
Configured ModSecurity with OWASP rules. Basic stuff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt-get install libapache2-mod-security2

cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changed SecRuleEngine DetectionOnly to SecRuleEngine On.&lt;/p&gt;

&lt;p&gt;The Forgotten Hero: Fail2Ban&lt;br&gt;
Set up Fail2Ban to block brute force attempts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/fail2ban/jail.local

[wordpress]

enabled = true

filter = wordpress

logpath = /var/log/auth.log

maxretry = 3

bantime = 3600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hour 4: Automation &amp;amp; Insurance (06:00 - 07:00)
&lt;/h2&gt;

&lt;p&gt;Automated Backups That Actually Work&lt;br&gt;
Wrote a bash script for daily backups to remote storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

TIMESTAMP=$(date +%Y%m%d_%H%M%S)

BACKUP_DIR="/backups"

# Database

mysqldump -u user -ppassword database &amp;gt; $BACKUP_DIR/db_$TIMESTAMP.sql

# Files

tar -czf $BACKUP_DIR/files_$TIMESTAMP.tar.gz /var/www/html

# Send to remote (S3, or whatever)

aws s3 cp $BACKUP_DIR/ s3://your-bucket/backups/ --recursive

# Keep only last 7 days locally

find $BACKUP_DIR -type f -mtime +7 -delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added it to crontab: &lt;code&gt;0 2 * * * /home/scripts/backup.sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Monitoring Setup&lt;br&gt;
Installed basic monitoring so next time we catch things early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Uptime monitoring

curl -X POST https://cronitor.io/api/monitors \

  -H "Content-Type: application/json" \

  -d '{"name": "client-site", "url": "https://example.com"}'

# File integrity monitoring

apt-get install aide

aide --init
SSL &amp;amp; Security Headers
Fresh SSL certificate with Let's Encrypt:

certbot --nginx -d example.com -d www.example.com

Added security headers to nginx:

add_header X-Frame-Options "SAMEORIGIN" always;

add_header X-Content-Type-Options "nosniff" always;

add_header X-XSS-Protection "1; mode=block" always;

add_header Referrer-Policy "strict-origin-when-cross-origin" always;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Aftermath
&lt;/h2&gt;

&lt;p&gt;By 7AM, site was back online. Clean, secured, monitored.&lt;/p&gt;

&lt;p&gt;What the client saw: Their site back up, faster than before, with new security measures.&lt;/p&gt;

&lt;p&gt;What they didn't see: The 4 hours of SSH sessions, database queries, and three cups of coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons From The Trenches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Worked
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Having a clear mental checklist for compromises&lt;/li&gt;
&lt;li&gt;Not trusting anything on a compromised system&lt;/li&gt;
&lt;li&gt;Taking backups before any action (even if you think you don't need them)&lt;/li&gt;
&lt;li&gt;Hardening during recovery, not after&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I'd Do Different
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Should have set up monitoring earlier (obviously)&lt;/li&gt;
&lt;li&gt;Could have automated the cleanup scripts better&lt;/li&gt;
&lt;li&gt;Next time: keep a USB drive with common tools ready&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prevention Is Cheaper Than Cure
&lt;/h3&gt;

&lt;p&gt;After this incident, I set up these things for all client sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weekly automated backups (tested restores monthly)&lt;/li&gt;
&lt;li&gt;Security plugins with proper configuration&lt;/li&gt;
&lt;li&gt;Update automation for core/plugins/themes&lt;/li&gt;
&lt;li&gt;File integrity monitoring&lt;/li&gt;
&lt;li&gt;Login attempt limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Technical Stack Behind This Recovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OS: Ubuntu 20.04 LTS&lt;/li&gt;
&lt;li&gt;Web Server: Nginx 1.18&lt;/li&gt;
&lt;li&gt;Database: MySQL 8.0&lt;/li&gt;
&lt;li&gt;Backup Storage: AWS S3&lt;/li&gt;
&lt;li&gt;Monitoring: Uptime Robot + custom bash scripts&lt;/li&gt;
&lt;li&gt;Security: ModSecurity, Fail2Ban, Cloudflare WAF&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Emergency recoveries are stressful. Your hands shake a bit when you're running rm -rf on a production server at 5AM. But this is what separates someone who just "knows WordPress" from someone who actually understands infrastructure.&lt;/p&gt;

&lt;p&gt;The client was happy. The site survived. And I learned (again) that keeping systems updated and monitored is way easier than 4-hour emergency sessions.&lt;/p&gt;

&lt;p&gt;Now I keep this checklist printed and stuck to my monitor. Because there will be a next time. There's always a next time.&lt;/p&gt;




&lt;p&gt;Time taken: 4 hours&lt;br&gt;
Coffee consumed: 3 cups&lt;br&gt;
Client panic level: Reduced from 10/10 to 2/10&lt;br&gt;
Would I do it again: Absolutely. But let's try to avoid it, yeah?&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>5 WordPress Performance Issues I Fix Every Week (And How You Can Too)</title>
      <dc:creator>hello world_leo</dc:creator>
      <pubDate>Fri, 26 Sep 2025 13:41:00 +0000</pubDate>
      <link>https://dev.to/leo_rio/5-wordpress-performance-issues-i-fix-every-week-and-how-you-can-too-34go</link>
      <guid>https://dev.to/leo_rio/5-wordpress-performance-issues-i-fix-every-week-and-how-you-can-too-34go</guid>
      <description>&lt;p&gt;As a DevOps engineer working with agencies in Indonesia, I see the same WordPress performance issues over and over again. After optimizing hundreds of WordPress sites, I've identified the 5 most common problems that slow down websites - and the practical solutions that actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cost of Slow WordPress Sites
&lt;/h2&gt;

&lt;p&gt;Before diving into solutions, let's be honest about what's at stake. A 1-second delay in page load time can reduce conversions by 7%. For an e-commerce site making $100,000 per month, that's $7,000 in lost revenue. For a service business, it's lost leads and frustrated potential clients.&lt;/p&gt;

&lt;p&gt;I've seen clients lose significant business because their WordPress sites took 8+ seconds to load. The good news? Most performance issues are fixable with the right approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #1: Database Bloat (Found in 80% of sites I audit)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; WordPress databases accumulate junk over time - spam comments, post revisions, transient data, and unused metadata. I recently audited a 2-year-old site where the database was 400MB, but only 50MB was actual content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check your database size first&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;data_length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;index_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"Size (MB)"&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TABLES&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_database_name'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;index_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quick Wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete spam comments and post revisions&lt;/li&gt;
&lt;li&gt;Clean up unused plugins and themes data&lt;/li&gt;
&lt;li&gt;Remove expired transients&lt;/li&gt;
&lt;li&gt;Use WP-Optimize plugin for regular maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real Result:&lt;/strong&gt; One client's site went from 6.2s to 3.8s load time just from database cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #2: Unoptimized Images (The Silent Performance Killer)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; I regularly see WordPress sites with 5MB+ images loading on mobile devices. A photography portfolio I worked on had 47 images totaling 180MB on their homepage alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implement WebP format with fallbacks&lt;/li&gt;
&lt;li&gt;Use responsive images (WordPress does this automatically if properly configured)&lt;/li&gt;
&lt;li&gt;Compress images before upload&lt;/li&gt;
&lt;li&gt;Set up proper image CDN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quick Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add to functions.php for WebP support&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;add_webp_support&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mimes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$mimes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'webp'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'image/webp'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$mimes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'upload_mimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'add_webp_support'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real Result:&lt;/strong&gt; E-commerce client reduced homepage size from 8.2MB to 1.4MB, improving load time by 65%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #3: Plugin Overload (The "Swiss Army Knife" Problem)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Many sites use plugins that do way more than needed. I audited a simple business site running 47 plugins, including a full e-commerce suite just to display a contact form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Audit Process I Use:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List all active plugins&lt;/li&gt;
&lt;li&gt;Test site performance with each plugin deactivated&lt;/li&gt;
&lt;li&gt;Identify the worst performers&lt;/li&gt;
&lt;li&gt;Find lighter alternatives or custom solutions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Common Culprits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page builders loading 2MB+ of CSS/JS&lt;/li&gt;
&lt;li&gt;Social sharing plugins with 20+ network options&lt;/li&gt;
&lt;li&gt;SEO plugins with overlapping functionality&lt;/li&gt;
&lt;li&gt;Backup plugins running during peak hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real Result:&lt;/strong&gt; Removing 12 unnecessary plugins improved a client's Time to First Byte from 2.1s to 0.8s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #4: Poor Caching Strategy (Or No Caching At All)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; 40% of sites I audit have no caching, and another 30% have poorly configured caching that's actually hurting performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Caching Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server-level:&lt;/strong&gt; Nginx FastCGI cache or Apache mod_cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application-level:&lt;/strong&gt; WordPress object caching with Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser-level:&lt;/strong&gt; Proper cache headers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDN:&lt;/strong&gt; CloudFlare or similar for static assets&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Configuration That Works:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nginx FastCGI cache configuration&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="mi"&gt;60m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$skip_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_no_cache&lt;/span&gt; &lt;span class="nv"&gt;$skip_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other fastcgi settings&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real Result:&lt;/strong&gt; Proper caching reduced server response time from 1.2s to 180ms for a high-traffic news site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #5: Hosting Mismatch (The $5/month Bottleneck)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Running a complex WordPress site on shared hosting that costs $5/month is like trying to run a restaurant out of a food truck. It might work, but you'll hit limits quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red Flags I Look For:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared hosting for sites with 10,000+ monthly visitors&lt;/li&gt;
&lt;li&gt;No SSD storage&lt;/li&gt;
&lt;li&gt;PHP versions older than 7.4&lt;/li&gt;
&lt;li&gt;No server-level caching options&lt;/li&gt;
&lt;li&gt;Limited memory allocation (128MB or less)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Right-Sizing Hosting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0-5K visitors/month:&lt;/strong&gt; Quality shared hosting ($10-20/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5K-50K visitors/month:&lt;/strong&gt; VPS with proper configuration ($20-50/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50K+ visitors/month:&lt;/strong&gt; Managed WordPress hosting or custom VPS ($50+/month)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real Result:&lt;/strong&gt; Moving a client from $8/month shared hosting to $25/month VPS improved load time from 4.5s to 1.8s.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Optimization Process I Use
&lt;/h2&gt;

&lt;p&gt;After fixing hundreds of WordPress sites, here's my systematic approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Audit (Day 1)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Baseline performance testing (GTmetrix, PageSpeed Insights, WebPageTest)&lt;/li&gt;
&lt;li&gt;Database analysis and cleanup opportunities&lt;/li&gt;
&lt;li&gt;Plugin performance profiling&lt;/li&gt;
&lt;li&gt;Server configuration review&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 2: Quick Wins (Day 2)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Database optimization&lt;/li&gt;
&lt;li&gt;Image compression and format conversion&lt;/li&gt;
&lt;li&gt;Plugin audit and removal&lt;/li&gt;
&lt;li&gt;Basic caching implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 3: Advanced Optimization (Day 3)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Server-level caching configuration&lt;/li&gt;
&lt;li&gt;CDN setup and optimization&lt;/li&gt;
&lt;li&gt;Code-level optimizations&lt;/li&gt;
&lt;li&gt;Performance monitoring setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 4: Monitoring (Ongoing)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Weekly performance checks&lt;/li&gt;
&lt;li&gt;Monthly database maintenance&lt;/li&gt;
&lt;li&gt;Quarterly plugin audits&lt;/li&gt;
&lt;li&gt;Performance alerts for regression&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools I Actually Use (Not Sponsored)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Free Tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GTmetrix for performance testing&lt;/li&gt;
&lt;li&gt;Google PageSpeed Insights for Core Web Vitals&lt;/li&gt;
&lt;li&gt;Query Monitor plugin for WordPress debugging&lt;/li&gt;
&lt;li&gt;Chrome DevTools for detailed analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Paid Tools Worth It:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New Relic for server monitoring ($25/month)&lt;/li&gt;
&lt;li&gt;CloudFlare Pro for advanced caching ($20/month)&lt;/li&gt;
&lt;li&gt;WP Rocket for easy caching setup ($59/year)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Results Should You Expect?
&lt;/h2&gt;

&lt;p&gt;Based on my experience optimizing WordPress sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database cleanup:&lt;/strong&gt; 10-30% speed improvement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image optimization:&lt;/strong&gt; 20-50% speed improvement
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin optimization:&lt;/strong&gt; 15-40% speed improvement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proper caching:&lt;/strong&gt; 30-70% speed improvement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting upgrade:&lt;/strong&gt; 25-60% speed improvement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Combined effect:&lt;/strong&gt; Most sites see 40-70% overall improvement when all issues are addressed properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Over-optimization:&lt;/strong&gt; Don't chase perfect scores, chase good user experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin addiction:&lt;/strong&gt; More caching plugins doesn't mean better performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring mobile:&lt;/strong&gt; 60%+ of traffic is mobile, optimize for it first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-time fixes:&lt;/strong&gt; Performance optimization needs ongoing maintenance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap shortcuts:&lt;/strong&gt; Free solutions often cost more in the long run&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Your Next Steps
&lt;/h2&gt;

&lt;p&gt;If your WordPress site is slow, start with these priorities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run a performance audit&lt;/strong&gt; - Get baseline numbers first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean your database&lt;/strong&gt; - Often the biggest quick win&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize images&lt;/strong&gt; - Especially if you have a visual site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit plugins&lt;/strong&gt; - Remove what you don't actually need&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up proper caching&lt;/strong&gt; - This can transform your site&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Performance optimization isn't just about technical tweaks - it's about creating better user experiences that convert visitors into customers.&lt;/p&gt;




</description>
      <category>tutorial</category>
      <category>performance</category>
      <category>wordpress</category>
      <category>php</category>
    </item>
  </channel>
</rss>
