---
description: Add Cloud Firestore to a Capacitor app and learn real-time listeners, offline sync, and CRUD operations, plus the setup and gotchas that trip teams up.
title: Capacitor Firestore: Real-Time Data & Offline Sync - Capawesome
image: https://capawesome.io/docs/assets/images/social/blog/capacitor-firebase-cloud-firestore-guide.png
---

<!doctype html> 

[Skip to content ](#capacitor-firestore-real-time-data-offline-sync) 

[📲 Introducing **Build Sharing** — get your builds onto testers' devices with a link & QR code. No account required. ](/blog/share-mobile-app-builds-with-testers/) 

* [ SDKs ](/docs/sdks/)
* [ Formbricks ](/docs/sdks/capacitor/formbricks/)
* [ Geocoder ](/docs/sdks/capacitor/geocoder/)
* [ Google Sign-In ](/docs/sdks/capacitor/google-sign-in/)
* [ Grafana Faro ](/docs/sdks/capacitor/grafana-faro/)
* [ Gyroscope ](/docs/sdks/capacitor/gyroscope/)
* [ Haptics ](/docs/sdks/capacitor/haptics/)
* [ Home Indicator ](/docs/sdks/capacitor/home-indicator/)
* [ In-App Browser ](/docs/sdks/capacitor/in-app-browser/)
* [ Install Referrer ](/docs/sdks/capacitor/install-referrer/)
* [ Intercom ](/docs/sdks/capacitor/intercom/)
* [ Intune ](/docs/sdks/capacitor/intune/)
* [ Keep Awake ](/docs/sdks/capacitor/keep-awake/)
* [ libSQL ](/docs/sdks/capacitor/libsql/)
* [ Light Sensor ](/docs/sdks/capacitor/light-sensor/)
* [ Live Update ](/docs/sdks/capacitor/live-update/)
* [ Localization ](/docs/sdks/capacitor/localization/)
* [ Mail Composer ](/docs/sdks/capacitor/mail-composer/)
* [ Managed Configurations ](/docs/sdks/capacitor/managed-configurations/)
* [ Maps Launcher ](/docs/sdks/capacitor/maps-launcher/)
* [ Media Session ](/docs/sdks/capacitor/media-session/)
* [ ML Kit ](/docs/sdks/capacitor/mlkit/)
* [ Navigation Bar ](/docs/sdks/capacitor/navigation-bar/)
* [ Network ](/docs/sdks/capacitor/network/)
* [ NFC ](/docs/sdks/capacitor/nfc/)
* [ Node.js ](/docs/sdks/capacitor/nodejs/)
* [ OAuth ](/docs/sdks/capacitor/oauth/)
* [ Passkeys ](/docs/sdks/capacitor/passkeys/)
* [ Password Autofill ](/docs/sdks/capacitor/password-autofill/)
* [ PDF Generator ](/docs/sdks/capacitor/pdf-generator/)
* [ PDF Viewer ](/docs/sdks/capacitor/pdf-viewer/)
* [ Pedometer ](/docs/sdks/capacitor/pedometer/)
* [ Permissions ](/docs/sdks/capacitor/permissions/)
* [ Phone Dialer ](/docs/sdks/capacitor/phone-dialer/)
* [ Photo Editor ](/docs/sdks/capacitor/photo-editor/)
* [ Photo Manipulator ](/docs/sdks/capacitor/photo-manipulator/)
* [ PixLive ](/docs/sdks/capacitor/pixlive/)
* [ PostHog ](/docs/sdks/capacitor/posthog/)
* [ Printer ](/docs/sdks/capacitor/printer/)
* [ Privacy Screen ](/docs/sdks/capacitor/privacy-screen/)
* [ Proximity Sensor ](/docs/sdks/capacitor/proximity-sensor/)
* [ Purchases ](/docs/sdks/capacitor/purchases/)
* [ RealtimeKit ](/docs/sdks/capacitor/realtimekit/)
* [ Root Detection ](/docs/sdks/capacitor/root-detection/)
* [ Screen Brightness ](/docs/sdks/capacitor/screen-brightness/)
* [ Screen Orientation ](/docs/sdks/capacitor/screen-orientation/)
* [ Screen Reader ](/docs/sdks/capacitor/screen-reader/)
* [ Screenshot ](/docs/sdks/capacitor/screenshot/)
* [ Secure Preferences ](/docs/sdks/capacitor/secure-preferences/)
* [ Settings Launcher ](/docs/sdks/capacitor/settings-launcher/)
* [ Shake ](/docs/sdks/capacitor/shake/)
* [ Silent Mode ](/docs/sdks/capacitor/silent-mode/)
* [ SIM ](/docs/sdks/capacitor/sim/)
* [ SMS Composer ](/docs/sdks/capacitor/sms-composer/)
* [ Speech Recognition ](/docs/sdks/capacitor/speech-recognition/)
* [ Speech Synthesis ](/docs/sdks/capacitor/speech-synthesis/)
* [ Share Target ](/docs/sdks/capacitor/share-target/)
* [ Square Mobile Payments ](/docs/sdks/capacitor/square-mobile-payments/)
* [ SQLite ](/docs/sdks/capacitor/sqlite/)
* [ Superwall ](/docs/sdks/capacitor/superwall/)
* [ System WebView ](/docs/sdks/capacitor/system-webview/)
* [ Tauri ](/docs/sdks/capacitor/tauri/)
* [ Text Interaction ](/docs/sdks/capacitor/text-interaction/)
* [ Text Zoom ](/docs/sdks/capacitor/text-zoom/)
* [ Thermal State ](/docs/sdks/capacitor/thermal-state/)
* [ Toast ](/docs/sdks/capacitor/toast/)
* [ Torch ](/docs/sdks/capacitor/torch/)
* [ Vault ](/docs/sdks/capacitor/vault/)
* [ Volume ](/docs/sdks/capacitor/volume/)
* [ Wallet ](/docs/sdks/capacitor/wallet/)
* [ Wifi ](/docs/sdks/capacitor/wifi/)
* [ YouTube Player ](/docs/sdks/capacitor/youtube-player/)
* [ Zip ](/docs/sdks/capacitor/zip/)
* [ Cordova ](/docs/sdks/cordova/)
* [ Cloud ](/docs/cloud/)
* [ Integrations ](/docs/cloud/live-updates/integrations/)
* Concepts
* Reference
* [ Troubleshooting ](/docs/cloud/live-updates/troubleshooting/)
* [ FAQ ](/docs/cloud/live-updates/faq/)
* [ Native Builds ](/docs/cloud/native-builds/)
* [ Set Up Environments ](/docs/cloud/native-builds/environments/)
* [ Overwrite Native Configurations ](/docs/cloud/native-builds/native-configurations/)
* [ Auto-Increment Build Numbers ](/docs/cloud/native-builds/auto-incrementing-build-numbers/)
* [ Configure the Web Build Script ](/docs/cloud/native-builds/web-build-script/)
* [ Build from a Monorepo ](/docs/cloud/native-builds/monorepo/)
* [ Use pnpm, Yarn, or bun ](/docs/cloud/native-builds/package-managers/)
* [ Install Private npm Packages ](/docs/cloud/native-builds/npm-private-registry/)
* [ Override the Java Version ](/docs/cloud/native-builds/override-java-version/)
* [ Custom iOS Provisioning Profiles ](/docs/cloud/native-builds/custom-ios-provisioning-profiles/)
* [ Build without Git ](/docs/cloud/native-builds/build-without-git/)
* [ Access Git Behind a Firewall ](/docs/cloud/native-builds/firewall-access/)
* [ Integrations ](/docs/cloud/native-builds/integrations/)
* Reference
* [ Troubleshooting ](/docs/cloud/native-builds/troubleshooting/)
* [ FAQ ](/docs/cloud/native-builds/faq/)
* [ App Store Publishing ](/docs/cloud/app-store-publishing/)
* [ Submit a Build ](/docs/cloud/app-store-publishing/submit-a-build/)
* [ Submit Automatically After a Build ](/docs/cloud/app-store-publishing/submit-automatically/)
* [ Troubleshooting ](/docs/cloud/app-store-publishing/troubleshooting/)
* [ FAQ ](/docs/cloud/app-store-publishing/faq/)
* [ Automations ](/docs/cloud/automations/)
* [ Reference ](/docs/cloud/automations/reference/)
* [ Troubleshooting ](/docs/cloud/automations/troubleshooting/)
* [ FAQ ](/docs/cloud/automations/faq/)
* [ Assist ](/docs/cloud/assist/)
* [ CLI ](/docs/cloud/cli/)
* APIs and SDKs
* [ Webhooks ](/docs/cloud/webhooks/)
* [ Integrations ](/docs/cloud/integrations/)
* Notifications
* Account
* [ Organization ](/docs/cloud/organizations/)
* [ Two-Factor Enforcement ](/docs/cloud/organizations/two-factor-authentication/)
* [ Audit Logs ](/docs/cloud/organizations/audit-logs/)
* [ Billing ](/docs/cloud/organizations/billing/)
* [ License Keys ](/docs/cloud/license-keys/)
* [ AI ](/docs/ai/)
* [ Insiders ](/docs/insiders/)
* [ Billing & Plans ](/docs/insiders/billing-and-plans/)
* [ FAQ ](/docs/insiders/faq/)
* [ License ](https://capawesome.io/legal/eula/)
* [ Support ](/docs/support/)
* [ Contributing ](/docs/contributing/)
* Contributing code
* [ Code of Conduct ](/docs/contributing/code-of-conduct/)
* [ Questions ](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#creating-a-discussion)
* [ Blog ](/blog/)
* Categories

* [ Why Use Cloud Firestore in a Capacitor App? ](#why-use-cloud-firestore-in-a-capacitor-app)
* [ Before You Start ](#before-you-start)
* [ Step 1: Create the Firestore Database ](#step-1-create-the-firestore-database)
* [ Step 2: Install the Plugin ](#step-2-install-the-plugin)
* [ Step 3: Add Firebase to Your Native Apps ](#step-3-add-firebase-to-your-native-apps)
* [ Working with Documents: A Product Catalog Example ](#working-with-documents-a-product-catalog-example)
* [ Offline Sync: Working Without a Connection ](#offline-sync-working-without-a-connection)
* [ Run on a Device and Verify ](#run-on-a-device-and-verify)
* [ Firestore Best Practices ](#firestore-best-practices)
* [ Limitations ](#limitations)
* [ Common Errors and Troubleshooting ](#common-errors-and-troubleshooting)
* [ FAQ ](#faq)
* [ Ship Data-Layer Changes Without a Full Release ](#ship-data-layer-changes-without-a-full-release)
* [ Conclusion ](#conclusion)

* Related links

# Capacitor Firestore: Real-Time Data & Offline Sync[¶](#capacitor-firestore-real-time-data-offline-sync "Permanent link")

Storing and syncing app data usually means picking between rolling your own backend or relying on the Firebase JavaScript SDK inside a WebView, which comes with weaker performance and its own authentication quirks on native. The [Capacitor Firebase Cloud Firestore plugin](/docs/sdks/capacitor/firebase/cloud-firestore/), sponsored by [AppScreens](https://appscreens.com/?%5Flocale=en&utm%5Fsource=capawesome&utm%5Fmedium=referral&utm%5Fcampaign=capawesome&gclid=capawesome), gives you the native Android and iOS Firestore SDKs behind one Capacitor API instead.

This guide walks the whole thing end to end — creating the Firestore database, installing and configuring the plugin, reading and writing data, and how offline sync and real-time listeners behave in practice — plus the production practices and gotchas that trip teams up in real apps.

[ ![Build and deploy your Capacitor app with Capawesome Cloud](https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1) ](/) 

## How to Use Cloud Firestore in a Capacitor App[¶](#how-to-use-cloud-firestore-in-a-capacitor-app "Permanent link")

Getting Cloud Firestore working in a Capacitor app takes five steps:

1. **Create the Firestore database** in your Firebase project and set its security rules.
2. **Install** `@capacitor-firebase/firestore` and sync the native projects.
3. **Add the native config files** (`google-services.json` on Android, `GoogleService-Info.plist` on iOS).
4. **(Optional) enable offline persistence** with `enablePersistence()` before any other Firestore call.
5. **Read and write documents** with `addDocument()`, `getDocument()`, and `getCollection()`, and subscribe to snapshot listeners for real-time updates.

The rest of this guide covers each step in detail, plus offline sync, best practices, and troubleshooting.

## What Is Cloud Firestore?[¶](#what-is-cloud-firestore "Permanent link")

[Cloud Firestore](https://firebase.google.com/docs/firestore) is Firebase's document-oriented NoSQL database. Data lives in collections of documents, queries can filter and sort on any field, and clients can subscribe to real-time updates instead of polling. The [Capacitor Firebase Cloud Firestore plugin](/docs/sdks/capacitor/firebase/cloud-firestore/) exposes this through the native Firestore SDKs on Android and iOS, plus the Firebase JS SDK on web, behind one shared TypeScript API.

A working example can be found here: [capawesome-team/capacitor-firebase-plugin-demo](https://github.com/capawesome-team/capacitor-firebase-plugin-demo).

### Why Not Just Use the Firebase JS SDK?[¶](#why-not-just-use-the-firebase-js-sdk "Permanent link")

Before this plugin existed, using Firestore in a Capacitor app meant loading the Firebase JavaScript SDK inside the WebView on every platform, Android and iOS included. That works, but every read, write, and real-time update then crosses the WebView-to-native JS bridge instead of talking to the platform's native Firestore SDK directly, and offline caching falls back to the WebView's IndexedDB implementation instead of the native SDKs' own on-device cache. There's also an authentication wrinkle to plan around: if the rest of your app signs users in natively (see the [Capacitor Firebase Authentication guide](/blog/capacitor-firebase-authentication-guide/)), that sign-in doesn't automatically authenticate the web layer, so Firestore security rules checking `request.auth` would see no signed-in user unless you separately authenticate the JS SDK too. This plugin sidesteps all of that by using the native Firestore SDKs on Android and iOS directly, only falling back to the Firebase JS SDK on the web platform, where there's no native alternative.

Advanced Operations

This guide covers the operations most apps need. For less common ones like batched writes and collection groups, see the [Usage section of the plugin docs](https://capawesome.io/docs/sdks/capacitor/firebase/cloud-firestore/#usage).

## Why Use Cloud Firestore in a Capacitor App?[¶](#why-use-cloud-firestore-in-a-capacitor-app "Permanent link")

The plugin's own use cases map to five real scenarios:

* **Real-time updates.** Keep your UI in sync by listening to document and collection changes with snapshot listeners, instead of polling.
* **User-generated content.** Create, read, update, and delete documents for user profiles, posts, or other app data.
* **Complex queries.** Filter and sort collections with composite filters and query constraints like `where`, `orderBy`, and `limit`.
* **Offline support.** Enable offline persistence and control network access to work with locally cached data.
* **Atomic writes.** Perform multiple write operations as a single all-or-nothing batch with `writeBatch(...)`.

## Before You Start[¶](#before-you-start "Permanent link")

This guide assumes you already have a Capacitor app with the `android` and/or `ios` platforms added, and a Firebase project (creating one takes a minute in the [Firebase console](https://console.firebase.google.com/)). Everything specific to Firestore we'll set up below.

## Step 1: Create the Firestore Database[¶](#step-1-create-the-firestore-database "Permanent link")

1. In the [Firebase console](https://console.firebase.google.com/), open **Build → Firestore Database** and click **Create database**.
2. Pick a location (this choice is permanent) and a starting mode for the security rules:
  * **Production mode** locks the database down — no client can read or write until you write rules that allow it.
  * **Test mode** allows open access for a short window, fine for prototyping but never for production.
3. Under the **Rules** tab, write rules that match how your app authenticates. A common starting point is "only signed-in users can touch the data":  
`[](#%5F%5Fcodelineno-0-1)rules_version = '2';  
[](#%5F%5Fcodelineno-0-2)service cloud.firestore {  
[](#%5F%5Fcodelineno-0-3)  match /databases/{database}/documents {  
[](#%5F%5Fcodelineno-0-4)    match /products/{productId} {  
[](#%5F%5Fcodelineno-0-5)      allow read, write: if request.auth != null;  
[](#%5F%5Fcodelineno-0-6)    }  
[](#%5F%5Fcodelineno-0-7)  }  
[](#%5F%5Fcodelineno-0-8)}  
`  
Because this plugin signs users in **natively**, `request.auth` is populated automatically on Android and iOS — see the [Capacitor Firebase Authentication guide](/blog/capacitor-firebase-authentication-guide/) for wiring up sign-in.

## Step 2: Install the Plugin[¶](#step-2-install-the-plugin "Permanent link")

Install the plugin and sync the native projects:

`[](#%5F%5Fcodelineno-1-1)npm install @capacitor-firebase/firestore
[](#%5F%5Fcodelineno-1-2)npx cap sync
`

## Step 3: Add Firebase to Your Native Apps[¶](#step-3-add-firebase-to-your-native-apps "Permanent link")

The plugin talks to Firestore through the native config files Firebase generates. The full reference is the plugin's [Add Firebase to your project](https://github.com/capawesome-team/capacitor-firebase/blob/main/docs/firebase-setup.md) guide; in short:

* **Android** — register an Android app in the console, download `google-services.json`, and place it in `android/app/google-services.json`.
* **iOS** — register an iOS app, download `GoogleService-Info.plist`, move it to `ios/App/App/GoogleService-Info.plist`, and drag it into the Xcode project (add it to all targets).
* **Web** — register a Web app and initialize the Firebase JS SDK with the config snippet the console gives you. Only needed if you target the web platform.

If you're on **iOS with Swift Package Manager**, add this to `capacitor.config.ts` to avoid a package identity collision (requires Capacitor CLI 8.4.0+):

`[](#%5F%5Fcodelineno-2-1){
[](#%5F%5Fcodelineno-2-2)  "experimental": {
[](#%5F%5Fcodelineno-2-3)    "ios": {
[](#%5F%5Fcodelineno-2-4)      "spm": {
[](#%5F%5Fcodelineno-2-5)        "packageOptions": {
[](#%5F%5Fcodelineno-2-6)          "@capacitor-firebase/firestore": { "symlink": true }
[](#%5F%5Fcodelineno-2-7)        }
[](#%5F%5Fcodelineno-2-8)      }
[](#%5F%5Fcodelineno-2-9)    }
[](#%5F%5Fcodelineno-2-10)  }
[](#%5F%5Fcodelineno-2-11)}
`

To point the plugin at a Firestore database other than the default one, set the `databaseId` [configuration option](https://capawesome.io/docs/sdks/capacitor/firebase/cloud-firestore/#configuration) (Android and iOS only).

## Working with Documents: A Product Catalog Example[¶](#working-with-documents-a-product-catalog-example "Permanent link")

Instead of a method-by-method reference (the [plugin's Usage docs](https://capawesome.io/docs/sdks/capacitor/firebase/cloud-firestore/#usage) already list every signature), here's how the core operations fit together when you're building a real feature — a `products` catalog behind a small store screen. If Firestore's data model is new to you, Firebase's own [Understand Cloud Firestore](https://firebase.google.com/docs/firestore) guide is worth ten minutes first.

### Adding a Product: Auto-Generated ID or One You Choose[¶](#adding-a-product-auto-generated-id-or-one-you-choose "Permanent link")

When you add a brand-new product and don't care what its ID is, let Firestore generate one for you with [addDocument(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#adddocument):

`[](#%5F%5Fcodelineno-3-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-3-2)
[](#%5F%5Fcodelineno-3-3)const addDocument = async () => {
[](#%5F%5Fcodelineno-3-4)  const { reference } = await FirebaseFirestore.addDocument({
[](#%5F%5Fcodelineno-3-5)    reference: 'products',
[](#%5F%5Fcodelineno-3-6)    data: { name: 'Wireless Keyboard', price: 49.99, inStock: true },
[](#%5F%5Fcodelineno-3-7)  });
[](#%5F%5Fcodelineno-3-8)  return reference.id;
[](#%5F%5Fcodelineno-3-9)};
`

When the ID is meaningful to your app — a SKU, a slug, a user's UID — write straight to that path with [setDocument(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#setdocument) instead:

`[](#%5F%5Fcodelineno-4-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-4-2)
[](#%5F%5Fcodelineno-4-3)const setDocument = async () => {
[](#%5F%5Fcodelineno-4-4)  await FirebaseFirestore.setDocument({
[](#%5F%5Fcodelineno-4-5)    reference: 'products/SKU-1024',
[](#%5F%5Fcodelineno-4-6)    data: { name: 'Wireless Keyboard', price: 49.99, inStock: true },
[](#%5F%5Fcodelineno-4-7)    merge: true,
[](#%5F%5Fcodelineno-4-8)  });
[](#%5F%5Fcodelineno-4-9)};
`

The rule of thumb: `addDocument(...)` is for "store this and hand me back an ID," while `setDocument(...)` is for "this record belongs at an ID I already know." Add `{ merge: true }`, as above, when the document may already exist and you only want to update the fields you're passing — bumping a price without wiping the rest of the product — rather than replacing the whole document.

### Reading One Product vs. a Filtered List[¶](#reading-one-product-vs-a-filtered-list "Permanent link")

When you already have the ID — the user tapped a specific product — read that single document with [getDocument(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#getdocument):

`[](#%5F%5Fcodelineno-5-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-5-2)
[](#%5F%5Fcodelineno-5-3)const getDocument = async () => {
[](#%5F%5Fcodelineno-5-4)  const { snapshot } = await FirebaseFirestore.getDocument({
[](#%5F%5Fcodelineno-5-5)    reference: 'products/SKU-1024',
[](#%5F%5Fcodelineno-5-6)  });
[](#%5F%5Fcodelineno-5-7)  return snapshot;
[](#%5F%5Fcodelineno-5-8)};
`

More often you're rendering a list — every in-stock product, most expensive first, capped to one page. That's a query with [getCollection(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#getcollection), where field filters go in `compositeFilter` and non-filter constraints like ordering and limiting go in `queryConstraints`:

`[](#%5F%5Fcodelineno-6-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-6-2)
[](#%5F%5Fcodelineno-6-3)const getCollection = async () => {
[](#%5F%5Fcodelineno-6-4)  const { snapshots } = await FirebaseFirestore.getCollection({
[](#%5F%5Fcodelineno-6-5)    reference: 'products',
[](#%5F%5Fcodelineno-6-6)    compositeFilter: {
[](#%5F%5Fcodelineno-6-7)      type: 'and',
[](#%5F%5Fcodelineno-6-8)      queryConstraints: [
[](#%5F%5Fcodelineno-6-9)        { type: 'where', fieldPath: 'inStock', opStr: '==', value: true },
[](#%5F%5Fcodelineno-6-10)      ],
[](#%5F%5Fcodelineno-6-11)    },
[](#%5F%5Fcodelineno-6-12)    queryConstraints: [
[](#%5F%5Fcodelineno-6-13)      { type: 'orderBy', fieldPath: 'price', directionStr: 'desc' },
[](#%5F%5Fcodelineno-6-14)      { type: 'limit', limit: 10 },
[](#%5F%5Fcodelineno-6-15)    ],
[](#%5F%5Fcodelineno-6-16)  });
[](#%5F%5Fcodelineno-6-17)  return snapshots;
[](#%5F%5Fcodelineno-6-18)};
`

It's recommended to specify the `type` property first in each constraint, so TypeScript can narrow and suggest the remaining properties for you.

### Removing a Product[¶](#removing-a-product "Permanent link")

Deleting is the most straightforward operation here — point [deleteDocument(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#deletedocument) at a path and the document is gone:

`[](#%5F%5Fcodelineno-7-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-7-2)
[](#%5F%5Fcodelineno-7-3)const deleteDocument = async () => {
[](#%5F%5Fcodelineno-7-4)  await FirebaseFirestore.deleteDocument({
[](#%5F%5Fcodelineno-7-5)    reference: 'products/SKU-1024',
[](#%5F%5Fcodelineno-7-6)  });
[](#%5F%5Fcodelineno-7-7)};
`

### Reacting to Changes in Real Time[¶](#reacting-to-changes-in-real-time "Permanent link")

The reason to reach for Firestore over a plain REST endpoint is that you don't have to poll for updates. Subscribe once and your callback re-runs every time the data changes — an admin edits a price, another device marks an item out of stock — with [addDocumentSnapshotListener(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#adddocumentsnapshotlistener) for a single product or [addCollectionSnapshotListener(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#addcollectionsnapshotlistener) for the whole list:

`[](#%5F%5Fcodelineno-8-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-8-2)
[](#%5F%5Fcodelineno-8-3)const addDocumentSnapshotListener = async () => {
[](#%5F%5Fcodelineno-8-4)  const callbackId = await FirebaseFirestore.addDocumentSnapshotListener(
[](#%5F%5Fcodelineno-8-5)    { reference: 'products/SKU-1024' },
[](#%5F%5Fcodelineno-8-6)    (event, error) => {
[](#%5F%5Fcodelineno-8-7)      if (error) {
[](#%5F%5Fcodelineno-8-8)        console.error(error);
[](#%5F%5Fcodelineno-8-9)      } else {
[](#%5F%5Fcodelineno-8-10)        console.log(event);
[](#%5F%5Fcodelineno-8-11)      }
[](#%5F%5Fcodelineno-8-12)    },
[](#%5F%5Fcodelineno-8-13)  );
[](#%5F%5Fcodelineno-8-14)  return callbackId;
[](#%5F%5Fcodelineno-8-15)};
[](#%5F%5Fcodelineno-8-16)
[](#%5F%5Fcodelineno-8-17)const addCollectionSnapshotListener = async () => {
[](#%5F%5Fcodelineno-8-18)  const callbackId = await FirebaseFirestore.addCollectionSnapshotListener(
[](#%5F%5Fcodelineno-8-19)    { reference: 'products' },
[](#%5F%5Fcodelineno-8-20)    (event, error) => {
[](#%5F%5Fcodelineno-8-21)      if (error) {
[](#%5F%5Fcodelineno-8-22)        console.error(error);
[](#%5F%5Fcodelineno-8-23)      } else {
[](#%5F%5Fcodelineno-8-24)        console.log(event);
[](#%5F%5Fcodelineno-8-25)      }
[](#%5F%5Fcodelineno-8-26)    },
[](#%5F%5Fcodelineno-8-27)  );
[](#%5F%5Fcodelineno-8-28)  return callbackId;
[](#%5F%5Fcodelineno-8-29)};
`

These subscriptions also keep firing against the local cache when the device goes offline, which the [Offline Sync](#offline-sync-working-without-a-connection) section covers next. To stop a listener when you're done with it, pass its callback ID to [removeSnapshotListener(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#removesnapshotlistener), or drop them all at once with [removeAllListeners()](/docs/sdks/capacitor/firebase/cloud-firestore/#removealllisteners):

`[](#%5F%5Fcodelineno-9-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-9-2)
[](#%5F%5Fcodelineno-9-3)const removeSnapshotListener = async (callbackId: string) => {
[](#%5F%5Fcodelineno-9-4)  await FirebaseFirestore.removeSnapshotListener({ callbackId });
[](#%5F%5Fcodelineno-9-5)};
[](#%5F%5Fcodelineno-9-6)
[](#%5F%5Fcodelineno-9-7)const removeAllListeners = async () => {
[](#%5F%5Fcodelineno-9-8)  await FirebaseFirestore.removeAllListeners();
[](#%5F%5Fcodelineno-9-9)};
`

### Wiring a Listener Into Angular, React, or Vue[¶](#wiring-a-listener-into-angular-react-or-vue "Permanent link")

Snapshot listeners are where frameworks differ, Angular especially: plugin events fire outside its change-detection zone, so you have to update state inside `NgZone.run(...)` or the UI won't refresh. In React and Vue, the important part is removing the listener when the component goes away.

AngularReactVue

`[](#%5F%5Fcodelineno-10-1)import { Injectable, NgZone, signal } from '@angular/core';
[](#%5F%5Fcodelineno-10-2)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-10-3)
[](#%5F%5Fcodelineno-10-4)@Injectable({ providedIn: 'root' })
[](#%5F%5Fcodelineno-10-5)export class ProductsService {
[](#%5F%5Fcodelineno-10-6)  readonly products = signal<unknown[]>([]);
[](#%5F%5Fcodelineno-10-7)
[](#%5F%5Fcodelineno-10-8)  constructor(private readonly zone: NgZone) {
[](#%5F%5Fcodelineno-10-9)    FirebaseFirestore.addCollectionSnapshotListener(
[](#%5F%5Fcodelineno-10-10)      { reference: 'products' },
[](#%5F%5Fcodelineno-10-11)      (event, error) => {
[](#%5F%5Fcodelineno-10-12)        if (!error) {
[](#%5F%5Fcodelineno-10-13)          this.zone.run(() => this.products.set(event?.snapshots ?? []));
[](#%5F%5Fcodelineno-10-14)        }
[](#%5F%5Fcodelineno-10-15)      },
[](#%5F%5Fcodelineno-10-16)    );
[](#%5F%5Fcodelineno-10-17)  }
[](#%5F%5Fcodelineno-10-18)}
`

`[](#%5F%5Fcodelineno-11-1)import { useEffect, useState } from 'react';
[](#%5F%5Fcodelineno-11-2)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-11-3)
[](#%5F%5Fcodelineno-11-4)export const useProducts = () => {
[](#%5F%5Fcodelineno-11-5)  const [products, setProducts] = useState<unknown[]>([]);
[](#%5F%5Fcodelineno-11-6)  useEffect(() => {
[](#%5F%5Fcodelineno-11-7)    const callbackId = FirebaseFirestore.addCollectionSnapshotListener(
[](#%5F%5Fcodelineno-11-8)      { reference: 'products' },
[](#%5F%5Fcodelineno-11-9)      (event, error) => {
[](#%5F%5Fcodelineno-11-10)        if (!error) setProducts(event?.snapshots ?? []);
[](#%5F%5Fcodelineno-11-11)      },
[](#%5F%5Fcodelineno-11-12)    );
[](#%5F%5Fcodelineno-11-13)    return () => {
[](#%5F%5Fcodelineno-11-14)      callbackId.then(id =>
[](#%5F%5Fcodelineno-11-15)        FirebaseFirestore.removeSnapshotListener({ callbackId: id }),
[](#%5F%5Fcodelineno-11-16)      );
[](#%5F%5Fcodelineno-11-17)    };
[](#%5F%5Fcodelineno-11-18)  }, []);
[](#%5F%5Fcodelineno-11-19)  return products;
[](#%5F%5Fcodelineno-11-20)};
`

`[](#%5F%5Fcodelineno-12-1)import { onMounted, onUnmounted, ref } from 'vue';
[](#%5F%5Fcodelineno-12-2)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-12-3)
[](#%5F%5Fcodelineno-12-4)export const useProducts = () => {
[](#%5F%5Fcodelineno-12-5)  const products = ref<unknown[]>([]);
[](#%5F%5Fcodelineno-12-6)  let callbackId: string | undefined;
[](#%5F%5Fcodelineno-12-7)  onMounted(async () => {
[](#%5F%5Fcodelineno-12-8)    callbackId = await FirebaseFirestore.addCollectionSnapshotListener(
[](#%5F%5Fcodelineno-12-9)      { reference: 'products' },
[](#%5F%5Fcodelineno-12-10)      (event, error) => {
[](#%5F%5Fcodelineno-12-11)        if (!error) products.value = event?.snapshots ?? [];
[](#%5F%5Fcodelineno-12-12)      },
[](#%5F%5Fcodelineno-12-13)    );
[](#%5F%5Fcodelineno-12-14)  });
[](#%5F%5Fcodelineno-12-15)  onUnmounted(() => {
[](#%5F%5Fcodelineno-12-16)    if (callbackId) FirebaseFirestore.removeSnapshotListener({ callbackId });
[](#%5F%5Fcodelineno-12-17)  });
[](#%5F%5Fcodelineno-12-18)  return products;
[](#%5F%5Fcodelineno-12-19)};
`

## Offline Sync: Working Without a Connection[¶](#offline-sync-working-without-a-connection "Permanent link")

This is where the native SDKs earn their keep. With offline persistence enabled, Firestore serves reads from an on-device cache when there's no connection and queues your writes locally, then syncs everything automatically the moment the device is back online. Your snapshot listeners keep firing against the local cache in the meantime, so the UI stays responsive whether the user is on the subway or in airplane mode. On Android and iOS this cache is the native SDK's own store, not the WebView's IndexedDB, which is the whole reason to reach for this plugin instead of the Firebase JS SDK for an offline-first app.

### Turn On Persistence First[¶](#turn-on-persistence-first "Permanent link")

Offline support isn't on by default. You opt in with [enablePersistence()](/docs/sdks/capacitor/firebase/cloud-firestore/#enablepersistence), and it comes with one hard rule: **it must be called before any other Firestore method**, or it throws. Call it once, right after your app starts, before you touch `getDocument`, `addDocument`, or anything else:

`[](#%5F%5Fcodelineno-13-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-13-2)
[](#%5F%5Fcodelineno-13-3)await FirebaseFirestore.enablePersistence({
[](#%5F%5Fcodelineno-13-4)  cacheSizeBytes: 100 * 1024 * 1024, // 100 MB, the default
[](#%5F%5Fcodelineno-13-5)});
`

The cache defaults to 100 MB and evicts least-recently-used documents once it fills up, so you rarely need to touch `cacheSizeBytes` unless you're caching an unusually large dataset. There's a matching [disablePersistence()](/docs/sdks/capacitor/firebase/cloud-firestore/#disablepersistence) if you ever need to turn it back off, and it's subject to the same "before any other method" rule.

### Know Whether Data Came From the Cache or the Server[¶](#know-whether-data-came-from-the-cache-or-the-server "Permanent link")

Every snapshot carries a `metadata` object with two flags that are essential for good offline UX:

* **`fromCache`** — `true` when the data was served from the local cache rather than confirmed by the server.
* **`hasPendingWrites`** — `true` when the snapshot includes local writes that haven't reached the server yet.

Together they let you show a "saving…" spinner or an "offline, changes will sync" banner instead of leaving the user guessing. By default a listener only fires when the _data_ changes, so to also get notified when only these flags flip (for example, a pending write finally reaching the server), pass `includeMetadataChanges: true`:

`[](#%5F%5Fcodelineno-14-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-14-2)
[](#%5F%5Fcodelineno-14-3)const listenWithMetadata = async () => {
[](#%5F%5Fcodelineno-14-4)  const callbackId = await FirebaseFirestore.addDocumentSnapshotListener(
[](#%5F%5Fcodelineno-14-5)    {
[](#%5F%5Fcodelineno-14-6)      reference: 'products/SKU-1024',
[](#%5F%5Fcodelineno-14-7)      includeMetadataChanges: true,
[](#%5F%5Fcodelineno-14-8)    },
[](#%5F%5Fcodelineno-14-9)    (event, error) => {
[](#%5F%5Fcodelineno-14-10)      if (error) {
[](#%5F%5Fcodelineno-14-11)        console.error(error);
[](#%5F%5Fcodelineno-14-12)      } else if (event?.snapshot) {
[](#%5F%5Fcodelineno-14-13)        const { fromCache, hasPendingWrites } = event.snapshot.metadata;
[](#%5F%5Fcodelineno-14-14)        console.log('offline copy:', fromCache);
[](#%5F%5Fcodelineno-14-15)        console.log('unsynced changes:', hasPendingWrites);
[](#%5F%5Fcodelineno-14-16)      }
[](#%5F%5Fcodelineno-14-17)    },
[](#%5F%5Fcodelineno-14-18)  );
[](#%5F%5Fcodelineno-14-19)  return callbackId;
[](#%5F%5Fcodelineno-14-20)};
`

### Handle Pending Server Timestamps[¶](#handle-pending-server-timestamps "Permanent link")

When a write sets a field with `FieldValue.serverTimestamp()` — as the [recordSale() example](#use-field-values-instead-of-read-modify-write) does for `updatedAt` — the server hasn't stamped that field yet while the device is offline. So what should your UI show in the meantime? Control that per listener with the `serverTimestamps` option:

* `none` (the default) returns the pending timestamp as `null`.
* `estimate` fills it with the local device's best guess at the time.
* `previous` returns the field's last known value.

For anything you render immediately after an optimistic write, `estimate` usually gives the least jarring result:

`[](#%5F%5Fcodelineno-15-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-15-2)
[](#%5F%5Fcodelineno-15-3)await FirebaseFirestore.addDocumentSnapshotListener(
[](#%5F%5Fcodelineno-15-4)  { reference: 'products/SKU-1024', serverTimestamps: 'estimate' },
[](#%5F%5Fcodelineno-15-5)  (event, error) => {
[](#%5F%5Fcodelineno-15-6)    /* event.snapshot.data.updatedAt is now a local estimate while offline */
[](#%5F%5Fcodelineno-15-7)  },
[](#%5F%5Fcodelineno-15-8));
`

### Force Offline Mode for Testing[¶](#force-offline-mode-for-testing "Permanent link")

You don't need airplane mode to verify your offline paths. Toggle the network directly to exercise cache reads and queued writes on demand:

`[](#%5F%5Fcodelineno-16-1)import { FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-16-2)
[](#%5F%5Fcodelineno-16-3)await FirebaseFirestore.disableNetwork();
[](#%5F%5Fcodelineno-16-4)// ...reads now come from cache, writes queue locally...
[](#%5F%5Fcodelineno-16-5)await FirebaseFirestore.enableNetwork();
`

### Reset the Cache When You Need To[¶](#reset-the-cache-when-you-need-to "Permanent link")

If you need to wipe cached documents and pending writes entirely (a full sign-out, say), [clearPersistence()](/docs/sdks/capacitor/firebase/cloud-firestore/#clearpersistence) does that, but only when called right after the app starts or after it's fully shut down, never while Firestore is actively in use.

## Run on a Device and Verify[¶](#run-on-a-device-and-verify "Permanent link")

Build and launch the app on a device or emulator so the native Firestore SDKs are in play:

`[](#%5F%5Fcodelineno-17-1)npx cap sync
[](#%5F%5Fcodelineno-17-2)npx cap run android   # or: npx cap run ios
`

Then confirm a write round-trips both ways:

* **In the Firebase console**, open **Firestore Database → Data** — the document you just wrote should appear in the `products` collection.
* **In your app**, attach a snapshot listener and confirm its callback fires with the new data.

If a write fails with a permission error, it's almost always your security rules (Step 1) rejecting an unauthenticated request rather than a bug in the call.

## Firestore Best Practices[¶](#firestore-best-practices "Permanent link")

### Count Documents Without Downloading Them[¶](#count-documents-without-downloading-them "Permanent link")

If you just need a number, don't call `getCollection(...)` and check `.length`. That downloads every matching document just to throw the data away. Use [getCountFromServer(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#getcountfromserver) instead. It runs the aggregation on Firestore's servers and returns only the count, which is both cheaper and faster for large collections.

### Use Field Values Instead of Read-Modify-Write[¶](#use-field-values-instead-of-read-modify-write "Permanent link")

Incrementing a counter or appending to an array by reading the document, modifying it in JavaScript, then writing it back is a race condition waiting to happen between two clients. Use the `FieldValue` helpers so the operation happens atomically on the server instead:

`[](#%5F%5Fcodelineno-18-1)import { FieldValue, FirebaseFirestore } from '@capacitor-firebase/firestore';
[](#%5F%5Fcodelineno-18-2)
[](#%5F%5Fcodelineno-18-3)const recordSale = async () => {
[](#%5F%5Fcodelineno-18-4)  await FirebaseFirestore.updateDocument({
[](#%5F%5Fcodelineno-18-5)    reference: 'products/SKU-1024',
[](#%5F%5Fcodelineno-18-6)    data: {
[](#%5F%5Fcodelineno-18-7)      stockCount: FieldValue.increment(-1),
[](#%5F%5Fcodelineno-18-8)      updatedAt: FieldValue.serverTimestamp(),
[](#%5F%5Fcodelineno-18-9)      categories: FieldValue.arrayUnion('bestseller'),
[](#%5F%5Fcodelineno-18-10)      tags: FieldValue.arrayRemove('clearance'),
[](#%5F%5Fcodelineno-18-11)      legacyField: FieldValue.delete(),
[](#%5F%5Fcodelineno-18-12)    },
[](#%5F%5Fcodelineno-18-13)  });
[](#%5F%5Fcodelineno-18-14)};
`

### Clean Up Snapshot Listeners[¶](#clean-up-snapshot-listeners "Permanent link")

Every `addDocumentSnapshotListener(...)` or `addCollectionSnapshotListener(...)` call keeps a connection open and returns a callback ID. If you don't call [removeSnapshotListener(...)](/docs/sdks/capacitor/firebase/cloud-firestore/#removesnapshotlistener) (or `removeAllListeners()`) when a screen unmounts or a user signs out, you end up with stale listeners still firing, burning battery and bandwidth for updates nothing is using anymore.

### Don't Manually Add Firebase's Native SDKs Alongside the Plugin[¶](#dont-manually-add-firebases-native-sdks-alongside-the-plugin "Permanent link")

The plugin already bundles the Firebase iOS and Android SDKs it needs. If you've also manually added Firebase pods or Swift Package Manager dependencies to your Xcode project (often left over from following a generic Firebase tutorial before adding this plugin), you'll get duplicate-symbol build failures or Firestore silently failing on iOS while the exact same code works fine on web. Remove any manually added Firebase frameworks from your native project and let the plugin manage that dependency.

## Limitations[¶](#limitations "Permanent link")

* The `databaseId` configuration option, for using a Firestore database other than the default one, is only available on Android and iOS.
* Data types are limited to what can be represented in JSON, plus the plugin's own `Timestamp`, `GeoPoint`, and `FieldValue` types — there's no support for arbitrary custom classes.

## Common Errors and Troubleshooting[¶](#common-errors-and-troubleshooting "Permanent link")

* **`PERMISSION_DENIED` / "Missing or insufficient permissions."** Your security rules are rejecting the request — usually because the user isn't signed in and your rules require `request.auth != null`. Check the rules (Step 1) and confirm sign-in works.
* **App crashes on launch, or Firestore isn't configured.** The `google-services.json` / `GoogleService-Info.plist` file is missing, misplaced, or (on iOS) wasn't added to the Xcode project. Re-check Step 3 and run `npx cap sync`.
* **`enablePersistence()` throws.** Something called another Firestore method first — it has to run before any other call. See [Turn On Persistence First](#turn-on-persistence-first).
* **A query throws asking for an index.** Composite queries (multiple `where` clauses, or `where` combined with `orderBy`) need a composite index. Firestore's error message includes a direct link to create it in the console.
* **Duplicate-symbol build errors, or Firestore failing only on iOS.** You've manually added Firebase pods or SPM dependencies alongside the plugin. Remove them and let the plugin manage the Firebase SDK.
* **Snapshot listeners never stop and drain the battery.** You're not removing listeners — track the callback IDs and call `removeSnapshotListener(...)` when a screen unmounts.

## FAQ[¶](#faq "Permanent link")

### Does Cloud Firestore work offline in a Capacitor app?[¶](#does-cloud-firestore-work-offline-in-a-capacitor-app "Permanent link")

Yes. Call [enablePersistence()](/docs/sdks/capacitor/firebase/cloud-firestore/#enablepersistence) once at startup, before any other Firestore call, and the native SDKs serve reads from an on-device cache and queue writes while the device is offline, syncing automatically when the connection returns. Queued writes even survive an app restart. See [Offline Sync](#offline-sync-working-without-a-connection) for the full picture.

### How are `Timestamp` and `GeoPoint` values represented, since JSON can't hold them?[¶](#how-are-timestamp-and-geopoint-values-represented-since-json-cant-hold-them "Permanent link")

The plugin exposes its own `Timestamp` and `GeoPoint` types so these fields round-trip correctly instead of being flattened into plain strings or numbers when you read a document back. You don't need to manually convert dates to ISO strings to work around this, unlike in earlier versions of the plugin.

### Why does `enablePersistence()` throw an error in my app?[¶](#why-does-enablepersistence-throw-an-error-in-my-app "Permanent link")

Almost always because something else called a Firestore method first. `enablePersistence()` has to run before any other Firestore call in your app's lifecycle, including reads that might happen in a service or a route guard that fires earlier than you expect. Trace what runs first on app start if you hit this.

### Do writes made while offline get lost if the app is closed?[¶](#do-writes-made-while-offline-get-lost-if-the-app-is-closed "Permanent link")

No, as long as offline persistence is enabled. Writes you make offline are queued in the on-device cache, and that queue survives the app being closed or the device restarting. The next time the app launches with a connection, Firestore replays the pending writes to the server automatically. Without `enablePersistence()`, the cache is in-memory only and those queued writes are lost when the app terminates.

### Is Cloud Firestore free to use?[¶](#is-cloud-firestore-free-to-use "Permanent link")

Firestore has a genuine free tier (a daily quota of reads, writes, deletes, and storage), and usage-based pricing beyond that. Real-time listeners count as reads whenever the listened data changes, so a busy collection with many active listeners can use up the free tier faster than the equivalent number of one-off `getDocument` calls. Check the current [Firebase pricing page](https://firebase.google.com/pricing) before designing a data model with heavy real-time usage at scale.

## Ship Data-Layer Changes Without a Full Release[¶](#ship-data-layer-changes-without-a-full-release "Permanent link")

Your Firestore reads, writes, and queries live in your app's web layer, so a change to how your app stores or queries data doesn't have to wait on an app store review. [Capawesome Cloud](https://capawesome.io/) builds your iOS and Android apps in the cloud and pushes those web-layer changes straight to users with live updates — so you can fix a broken query or tweak your data model the same day you find the problem, and automate the store submission when you do need a native release.

[Book a Capawesome Cloud Demo](https://cal.com/team/capawesome/cloud-demo)

## Conclusion[¶](#conclusion "Permanent link")

Cloud Firestore's real value for a Capacitor app is the combination of real-time listeners, offline persistence, and atomic writes behind one native API on Android, iOS, and web. The practical gotchas are mostly about ordering and cleanup: `enablePersistence()` has to run first, snapshot listeners need to be removed when you're done with them, and `getCountFromServer()` exists specifically so you stop paying to download documents you only wanted to count.

Be sure to check out the [API Reference](/docs/sdks/capacitor/firebase/cloud-firestore/#api) to see what else you can do with this plugin, and thanks again to our sponsor [AppScreens](https://appscreens.com/?%5Flocale=en&utm%5Fsource=capawesome&utm%5Fmedium=referral&utm%5Fcampaign=capawesome&gclid=capawesome), a dedicated screenshot mockup generator for app developers.

If you want to go deeper from here:

* [Firebase Authentication in Capacitor: Setup & Best Practices](/blog/capacitor-firebase-authentication-guide/) — wire up the native sign-in your security rules depend on.
* [Upload & Manage Files with Firebase Storage in Capacitor](/blog/capacitor-firebase-cloud-storage-guide/) — the sibling plugin for files, often paired with Firestore to store file references alongside structured data.
* [Track App Events with Firebase Analytics in Capacitor](/blog/capacitor-firebase-analytics-guide/) — measure how users interact with the data-driven screens you just built.
* [Announcing the Capacitor Firebase Cloud Firestore Plugin](/blog/announcing-the-capacitor-firebase-cloud-firestore-plugin/) — the original release announcement for this plugin.

Questions or something you ran into that isn't covered here? Drop into the [Capawesome Discord server](https://discord.gg/VCXxSVjefW) — and subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/) if you want the next deep-dive in your inbox.

July 29, 2026 

Back to top

```json
{
      "@context": "https://schema.org",
      "@type": "BlogPosting",
      "headline": "Capacitor Firestore: Real-Time Data \u0026 Offline Sync",
      "description": "Add Cloud Firestore to a Capacitor app and learn real-time listeners, offline sync, and CRUD operations, plus the setup and gotchas that trip teams up.",
      "image": "https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png",
      "datePublished": "2026-07-30T00:00:00+00:00",
      "dateModified": "2026-07-30T00:00:00+00:00",
      "author": [
        {
          "@type": "Person",
          "name": "Dayana Jabif",
          "url": "https://github.com/djabif"
        }
      ],
      "publisher": {
        "@type": "Organization",
        "name": "Capawesome",
        "url": "https://capawesome.io",
        "logo": {
          "@type": "ImageObject",
          "url": "https://capawesome.io/assets/images/logo.svg"
        }
      },
      "articleSection": "Capacitor",
      "keywords": ["Capacitor", "Firebase", "Guides", "SDKs"],
      "isPartOf": {
        "@type": "Blog",
        "@id": "https://capawesome.io/blog/#blog"
      },
      "mainEntityOfPage": "https://capawesome.io/blog/capacitor-firebase-cloud-firestore-guide/",
      "url": "https://capawesome.io/blog/capacitor-firebase-cloud-firestore-guide/"
    }
{
      "@context": "https://schema.org",
      "@type": "BreadcrumbList",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "name": "Home",
          "item": "https://capawesome.io/"
        },
        {
          "@type": "ListItem",
          "position": 2,
          "name": "Blog",
          "item": "https://capawesome.io/blog/"
        },
        {
          "@type": "ListItem",
          "position": 3,
          "name": "Capacitor Firestore: Real-Time Data \u0026 Offline Sync",
          "item": "https://capawesome.io/blog/capacitor-firebase-cloud-firestore-guide/"
        }
      ]
    }
{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What Is Cloud Firestore?", "acceptedAnswer": {"@type": "Answer", "text": "Cloud Firestore is Firebase's document-oriented NoSQL database. Data lives in collections of documents, queries can filter and sort on any field, and clients can subscribe to real-time updates instead of polling. The Capacitor Firebase Cloud Firestore plugin exposes this through the native Firestore SDKs on Android and iOS, plus the Firebase JS SDK on web, behind one shared TypeScript API. A working example can be found here: capawesome-team/capacitor-firebase-plugin-demo."}}, {"@type": "Question", "name": "Why Not Just Use the Firebase JS SDK?", "acceptedAnswer": {"@type": "Answer", "text": "Before this plugin existed, using Firestore in a Capacitor app meant loading the Firebase JavaScript SDK inside the WebView on every platform, Android and iOS included. That works, but every read, write, and real-time update then crosses the WebView-to-native JS bridge instead of talking to the platform's native Firestore SDK directly, and offline caching falls back to the WebView's IndexedDB implementation instead of the native SDKs' own on-device cache. There's also an authentication wrinkle to plan around: if the rest of your app signs users in natively (see the Capacitor Firebase Authentication guide), that sign-in doesn't automatically authenticate the web layer, so Firestore security rules checking request.auth would see no signed-in user unless you separately authenticate the JS SDK too. This plugin sidesteps all of that by using the native Firestore SDKs on Android and iOS directly, only falling back to the Firebase JS SDK on the web platform, where there's no native alternative. Advanced Operations This guide covers the operations most apps need. For less common ones like batched writes and collection groups, see the Usage section of the plugin docs."}}, {"@type": "Question", "name": "Why Use Cloud Firestore in a Capacitor App?", "acceptedAnswer": {"@type": "Answer", "text": "The plugin's own use cases map to five real scenarios: Real-time updates. Keep your UI in sync by listening to document and collection changes with snapshot listeners, instead of polling. User-generated content. Create, read, update, and delete documents for user profiles, posts, or other app data. Complex queries. Filter and sort collections with composite filters and query constraints like where, orderBy, and limit. Offline support. Enable offline persistence and control network access to work with locally cached data. Atomic writes. Perform multiple write operations as a single all-or-nothing batch with writeBatch(...)."}}, {"@type": "Question", "name": "Does Cloud Firestore work offline in a Capacitor app?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Call enablePersistence() once at startup, before any other Firestore call, and the native SDKs serve reads from an on-device cache and queue writes while the device is offline, syncing automatically when the connection returns. Queued writes even survive an app restart. See Offline Sync for the full picture."}}, {"@type": "Question", "name": "How are Timestamp and GeoPoint values represented, since JSON can't hold them?", "acceptedAnswer": {"@type": "Answer", "text": "The plugin exposes its own Timestamp and GeoPoint types so these fields round-trip correctly instead of being flattened into plain strings or numbers when you read a document back. You don't need to manually convert dates to ISO strings to work around this, unlike in earlier versions of the plugin."}}, {"@type": "Question", "name": "Why does enablePersistence() throw an error in my app?", "acceptedAnswer": {"@type": "Answer", "text": "Almost always because something else called a Firestore method first. enablePersistence() has to run before any other Firestore call in your app's lifecycle, including reads that might happen in a service or a route guard that fires earlier than you expect. Trace what runs first on app start if you hit this."}}, {"@type": "Question", "name": "Do writes made while offline get lost if the app is closed?", "acceptedAnswer": {"@type": "Answer", "text": "No, as long as offline persistence is enabled. Writes you make offline are queued in the on-device cache, and that queue survives the app being closed or the device restarting. The next time the app launches with a connection, Firestore replays the pending writes to the server automatically. Without enablePersistence(), the cache is in-memory only and those queued writes are lost when the app terminates."}}, {"@type": "Question", "name": "Is Cloud Firestore free to use?", "acceptedAnswer": {"@type": "Answer", "text": "Firestore has a genuine free tier (a daily quota of reads, writes, deletes, and storage), and usage-based pricing beyond that. Real-time listeners count as reads whenever the listened data changes, so a busy collection with many active listeners can use up the free tier faster than the equivalent number of one-off getDocument calls. Check the current Firebase pricing page before designing a data model with heavy real-time usage at scale."}}], "url": "https://capawesome.io/blog/capacitor-firebase-cloud-firestore-guide/"}
```
