# How to Integrate Retool with Google Cloud Firestore

- Tool: Retool
- Difficulty: Intermediate
- Time required: 20 minutes
- Last updated: April 2026

## TL;DR

Connect Retool to Google Cloud Firestore by adding a Firebase Resource in the Resources tab, authenticating with a Google service account JSON key, and selecting 'Firestore' as the service type per query. You can then build document CRUD interfaces, run compound queries with filters and ordering, paginate with startAfter cursors, and display nested Firestore data in Retool Tables — all without writing API client code.

## Build a Firestore Document Management Panel with Retool

Google Cloud Firestore is widely used as the backend database for mobile apps, web applications, and serverless platforms. However, Firestore's own data console in the Google Cloud Console is a development tool — it is not designed for operations teams who need to search, filter, and edit production data daily. Retool provides a purpose-built interface for Firestore operations by exposing the Firestore API through its native Firebase connector and wrapping it in drag-and-drop UI components.

With a Retool–Firestore integration, ops teams can query Firestore collections with compound filters (combining multiple where clauses and ordering), paginate through large collections using Firestore's cursor-based startAfter mechanism, and perform document CRUD operations through polished forms without needing access to the Google Cloud Console. Teams already using Firestore for their app data can use Retool to build admin panels for customer support, data correction, and operational reporting.

Firestore's document/collection model differs fundamentally from relational databases — there are no JOINs, no schema enforcement, and pagination works via document cursors rather than OFFSET. This page covers these Firestore-specific patterns and how to work with them effectively in Retool's query editor and component system.

## Before you start

- A Google Cloud or Firebase project with Firestore enabled (either Firestore Native mode or Datastore mode — this guide covers Native mode)
- A Google service account with the 'Cloud Datastore User' role or the 'Firebase Admin SDK Administrator Service Agent' role, with a JSON key file downloaded from Google Cloud Console → IAM & Admin → Service Accounts
- Familiarity with your Firestore data model: collection names, document field names, and any composite indexes required for compound queries
- A Retool account with permission to create and edit Resources
- Basic familiarity with Retool's query editor, Table component, and event handler system

## Step-by-step guide

### 1. Create the Firebase Resource for Firestore access

Navigate to the Resources tab in Retool's left navigation. Click Add Resource in the top-right corner. In the resource type picker, find the Google section and click Firebase.

In the resource configuration form:
- 'Resource name': Enter 'Firestore Production' or a name that identifies the environment. If your team uses separate Firebase projects for development and production, create one resource per project.
- 'Service Account Key': Paste the entire contents of your Google service account JSON key file. The JSON block starts with `{ "type": "service_account", "project_id": "...`. Make sure you paste the full JSON — missing curly braces or truncated content will cause authentication failures. Retool encrypts the key immediately on save.
- 'Database URL': Enter your Firebase Realtime Database URL (format: https://your-project-id-default-rtdb.firebaseio.com). Even if you are only using Firestore, Retool uses this field to identify the Firebase project. If your project has no Realtime Database, you can use a placeholder URL matching your project ID format.

Click Save Changes. Retool will test the service account credentials. If verification succeeds, a green Connected status appears.

Note: this single Firebase Resource grants access to all Firebase services for the connected project (Firestore, Realtime Database, Auth, and Cloud Messaging). You select the specific service within each query. You do not need a separate resource for Firestore alone.

**Expected result:** The Firebase Resource appears in the Resources tab with a green Connected indicator. The resource is available in the Retool query editor's resource dropdown.

### 2. Query a Firestore collection with filters

Open your Retool app and click + New query in the Code panel. Select your Firebase Resource from the dropdown. The query editor shows a Firebase service picker at the top — click the 'Service' dropdown and select 'Firestore'. The action options update to show Firestore-specific operations.

From the 'Action type' dropdown, select 'Query collection'. New input fields appear:
- 'Collection': Enter the name of your Firestore collection (e.g., 'users', 'orders', 'products'). Collection names are case-sensitive and must match exactly what is in Firestore.
- 'Filters': Click 'Add filter' to add WHERE conditions. Each filter has three parts: field name, operator (== != < <= > >= array-contains array-contains-any in not-in), and value. For example: field 'status', operator '==', value `{{ statusFilter.value || 'active' }}`.
- 'Order by': Click 'Add order' to set the sort field and direction. Note: Firestore requires an index for any query that filters on one field and orders by a different field — see the Troubleshooting section for index error handling.
- 'Limit': Set a maximum document count (e.g., 25). Firestore does not have OFFSET pagination — use cursor-based pagination instead (covered in step 3).

Name the query 'getUsers' and set 'Run query on page load' to On. Click Run to test it.

Firestore returns documents as an array of objects. Each object includes the document fields plus a special `__id__` field containing the Firestore document ID. Reference the document ID as `{{ getUsers.data[0].__id__ }}` in subsequent queries.

```
// Transformer: reshape Firestore query results for display
// Firestore returns document data with nested fields and Timestamps
const docs = data;
return docs.map(doc => ({
  id: doc.__id__,
  email: doc.email || '',
  status: doc.status || 'unknown',
  created_at: doc.created_at
    ? new Date(doc.created_at._seconds * 1000).toLocaleDateString()
    : 'N/A',
  plan: doc.subscription?.plan || 'free',
  last_login: doc.last_login
    ? new Date(doc.last_login._seconds * 1000).toLocaleString()
    : 'Never'
}));
```

**Expected result:** The query returns documents from the Firestore collection as an array. The transformer converts Timestamp fields to readable dates and flattens nested objects. The query result is ready to bind to a Table component.

### 3. Implement cursor-based pagination for large collections

Firestore does not support OFFSET-based pagination (no SKIP N rows). Instead, Firestore uses document cursors: you pass the last document from the current page as a 'start after' reference, and Firestore returns the next page starting after that document. Retool's Firestore query supports this via the 'Start after document ID' field.

Create a state variable to track the pagination cursor. In Retool's State panel (bottom of the canvas), add an Array variable named 'pageHistory' with default value []. This array will store document IDs representing the start of each page, enabling both 'Next' and 'Previous' navigation.

Add a second state variable 'currentPageIndex' (Number, default 0).

Modify your 'getUsers' query:
- In the 'Start after document ID' field, enter: `{{ pageHistory.value[currentPageIndex.value] || '' }}`
- An empty string means 'start from the beginning'.

Add a 'Next Page' button with this JavaScript event handler:
```javascript
// Save the last document ID from current results as the next page cursor
const lastDoc = getUsers.data[getUsers.data.length - 1];
if (lastDoc) {
  pageHistory.setValue([...pageHistory.value, lastDoc.__id__]);
  currentPageIndex.setValue(currentPageIndex.value + 1);
  getUsers.trigger();
}
```

Add a 'Previous Page' button:
```javascript
if (currentPageIndex.value > 0) {
  currentPageIndex.setValue(currentPageIndex.value - 1);
  getUsers.trigger();
}
```

Disable the Previous button when `{{ currentPageIndex.value === 0 }}` and the Next button when `{{ getUsers.data.length < 25 }}` (fewer results than the limit means the last page has been reached).

```
// JavaScript event handler: Next Page button
const lastDoc = getUsers.data[getUsers.data.length - 1];
if (lastDoc && lastDoc.__id__) {
  // Append the last document ID as the cursor for the next page
  pageHistory.setValue([...pageHistory.value, lastDoc.__id__]);
  currentPageIndex.setValue(currentPageIndex.value + 1);
  getUsers.trigger();
}
```

**Expected result:** The Next and Previous buttons navigate through the Firestore collection 25 documents at a time. The Previous button is disabled on the first page. Changing filters resets to page one.

### 4. Add Firestore document CRUD operations

With the collection query and pagination in place, add full CRUD operations to make the panel functional for ops teams.

**Get a single document**: Create a query with Action 'Get document', Collection `{{ table1.selectedRow.id ? collectionName : '' }}`, and Document ID `{{ table1.selectedRow.id }}`. Name it 'getDocument'. This loads the full document when a row is selected in the Table, including fields not shown in the collection query.

**Add a document**: Create a query with Action 'Add document to collection'. Set Collection to your collection name and Document data to a JSON object referencing form inputs: `{ "email": "{{ emailInput.value }}", "status": "{{ statusSelect.value }}", "created_at": {{ new Date() }} }`. Name it 'addDocument'. Wire a 'Create' button's click event to this query, with getUsers triggered on success to refresh the table.

**Update a document (partial update)**: Create a query with Action 'Update document'. Set Collection to your collection name, Document ID to `{{ table1.selectedRow.id }}`, and Update data to only the fields being changed: `{ "status": "{{ statusSelect.value }}", "updated_at": {{ new Date() }} }`. The 'Update' action merges changes without overwriting other fields — use 'Set document' if you want to replace the entire document.

**Delete a document**: Create a query with Action 'Delete document'. Set Collection and Document ID from the selected row. Name it 'deleteDocument'. Wire a 'Delete' button with a confirmation modal: 'Are you sure you want to delete document {{ table1.selectedRow.id }}? This cannot be undone.'

Arrange a Form component on the canvas that loads the selected document's fields when a table row is clicked (bind Form field default values to `{{ getDocument.data.fieldName }}`), and wire the form Submit to the updateDocument query.

```
// Firestore update document data — partial field update
// Only includes fields that have been modified
{
  "status": "{{ statusSelect.value }}",
  "plan": "{{ planSelect.value }}",
  "notes": "{{ notesInput.value }}",
  "updated_at": "{{ new Date().toISOString() }}",
  "updated_by": "{{ current_user.email }}"
}
```

**Expected result:** The CRUD panel is fully functional: clicking a table row loads the document in the detail form, Submit updates specific fields without overwriting others, the Add form creates new documents and refreshes the table, and Delete removes the document after confirmation.

### 5. Work with Firestore subcollections and nested data

Firestore's hierarchical data model allows subcollections nested under documents (e.g., `users/{userId}/orders/{orderId}`). Retool's Firestore query editor supports subcollection access by specifying the full collection path including parent document IDs.

To query a subcollection, set the 'Collection' field to the full path with the parent document ID embedded using Retool template syntax:
- Collection: `users/{{ userTable.selectedRow.id }}/orders`

This path dynamically changes based on which user is selected in the left-panel table. Create a query named 'getUserOrders' with this collection path, add appropriate filters and ordering, and set 'Run query on page load' to Off. In the userTable's 'Row selection' event handler, trigger getUserOrders so the subcollection query fires when a new user is selected.

For deeply nested Firestore documents that contain arrays of embedded objects (e.g., a document with an 'address_history' array of address objects), display the array data in a JSON Viewer component or pre-process it in a transformer:

`return data.map(doc => ({ ...doc, address_count: (doc.address_history || []).length, latest_address: doc.address_history?.[0]?.city || 'N/A' }));`

For Firestore documents with many nested fields (the document is itself a nested object tree), use a JavaScript transformer to flatten the relevant nested values into top-level properties for cleaner Table display:

`return data.map(doc => ({ id: doc.__id__, email: doc.profile?.email, city: doc.profile?.address?.city, plan: doc.subscription?.plan }));`

For complex Firestore data models involving collection group queries, transactions, and batch writes across multiple documents, RapidDev's team can help design and implement the full Retool architecture.

```
// Transformer: flatten nested Firestore document structure
// Input: Firestore documents with nested profile and subscription objects
const docs = data;
return docs.map(doc => ({
  id: doc.__id__,
  email: doc.profile?.email || doc.email || '',
  display_name: doc.profile?.displayName || '',
  city: doc.profile?.address?.city || '',
  country: doc.profile?.address?.country || '',
  plan: doc.subscription?.plan || 'free',
  plan_expires: doc.subscription?.expires_at
    ? new Date(doc.subscription.expires_at._seconds * 1000).toLocaleDateString()
    : 'N/A',
  tags: Array.isArray(doc.tags) ? doc.tags.join(', ') : '',
  doc_count: Array.isArray(doc.orders) ? doc.orders.length : 0
}));
```

**Expected result:** Selecting a user in the left panel table triggers the subcollection query and populates the right panel with that user's orders. Nested document fields are flattened by the transformer and display correctly in Table columns.

## Best practices

- Use the 'Update document' action (not 'Set document') in Retool for admin panel edit forms. 'Set document' replaces the entire document including fields not shown in your form, causing data loss for fields outside the form scope.
- Always include a `created_at` and `updated_at` field in documents written from Retool, set to `{{ new Date().toISOString() }}`. Firestore does not automatically track modification timestamps — without them, audit and debugging capabilities are severely limited.
- Convert all Firestore Timestamp fields in a query transformer rather than in individual component expressions. A centralized transformer keeps your component bindings clean and ensures consistent date formatting across the entire app.
- Store the Google service account JSON key as a Retool Configuration Variable (Settings → Configuration Variables) marked as secret rather than pasting it directly into the resource settings. This prevents the key from being visible to non-admin Retool users who view the resource configuration.
- Create Firestore composite indexes proactively for all filter-plus-sort combinations your Retool queries use. Index creation takes time — building indexes before go-live prevents query failures in production. Maintain a list of required indexes in your Firebase project documentation.
- Use Firestore's 'Query collection' action with explicit Limit values in all Retool queries. Without a Limit, Firestore returns all matching documents — on large collections this can exceed Retool's 100MB response size limit and cause timeouts.
- Implement optimistic UI updates in Retool: after a successful Firestore write, immediately update the local state variable that drives the Table rather than waiting for a full collection re-query. This makes the UI feel fast even on slower Firestore connections.
- For Retool apps used in production by multiple concurrent operators, add a 'Last modified by' field to every document write that captures `current_user.email`. This creates a lightweight audit trail within Firestore itself without requiring a separate audit log collection.

## Use cases

### Build a Firestore document search and edit panel for support teams

Create a Retool app where support agents can search a Firestore collection by multiple field values (user ID, email, status), view the matching documents in a Table, click a row to load the full document details in a form panel, and submit edits that update specific fields without overwriting the entire document. The panel shows Firestore document IDs alongside data and allows adding new documents through a create form.

Prompt example:

```
Build a Retool Firestore admin panel with search inputs for email and status fields, a Table showing query results with document ID, created_at, and key user fields, a detail Form on the right that loads the selected document's fields, and Save and Delete buttons that trigger Firestore update and delete queries.
```

### Build a paginated Firestore collection browser with compound filters

Create a Retool data browser for a high-volume Firestore collection (orders, events, audit logs) that supports compound WHERE filters, ordering by timestamp, and cursor-based pagination that loads the next page starting after the last document of the current page. Operators can filter by date range, status, and user segment, and export the current page as CSV.

Prompt example:

```
Build a Retool Firestore collection browser with date range, status Select, and user ID filters, a Table showing paginated results with 25 documents per page, Next and Previous page buttons using Firestore cursor-based pagination, and a CSV export button for the current results.
```

### Build a Firestore subcollection explorer for nested data management

Create a Retool two-panel app where the left panel shows a parent collection (e.g., 'users'), and selecting a user on the left loads that user's subcollection (e.g., 'orders' or 'notifications') in the right panel. Operations staff can drill into nested Firestore data, edit subcollection documents, and add or delete subcollection entries without navigating the Google Cloud Console.

Prompt example:

```
Build a Retool Firestore explorer with a users Table on the left bound to the 'users' collection, and when a user row is selected, a second Table on the right loads that user's 'orders' subcollection documents. Include an Add Order form and Delete button for the subcollection.
```

## Troubleshooting

### Firestore query returns 'The query requires an index' or 'FAILED_PRECONDITION: The query requires an index'

Cause: Firestore requires composite indexes for queries that filter on one field and order by a different field, or for queries with multiple inequality filters. Without the index, Firestore refuses to execute the query.

Solution: The error message includes a direct link to create the required index in the Firebase Console — click it to auto-populate the index creation form. Index creation takes 1-5 minutes for small collections and longer for large ones. While the index is building, the query will continue to return this error. If you do not have access to the Firebase Console, share the index creation link with your Firebase project administrator. For Retool development, simplify the query to single-field filters until the composite index is ready.

### Firestore Timestamp fields display as '[object Object]' in Retool Tables

Cause: Retool's Firestore connector returns Timestamp values as JavaScript objects with `_seconds` and `_nanoseconds` properties rather than as Date strings. The Table component does not know how to display plain objects.

Solution: Add a query transformer that converts all Timestamp fields to readable strings. Use `new Date(doc.created_at._seconds * 1000).toLocaleDateString()` to convert each Timestamp to a human-readable date. Apply this pattern to all Timestamp fields in your Firestore documents. If you are unsure which fields are Timestamps, check the raw query response in Retool's query editor Result tab — Timestamp objects are identifiable by the `{ _seconds: ..., _nanoseconds: ... }` structure.

```
// Fix for Firestore Timestamp display in transformer
const docs = data;
return docs.map(doc => ({
  ...doc,
  // Convert all known Timestamp fields
  created_at: doc.created_at?._seconds
    ? new Date(doc.created_at._seconds * 1000).toLocaleDateString()
    : 'N/A',
  updated_at: doc.updated_at?._seconds
    ? new Date(doc.updated_at._seconds * 1000).toLocaleString()
    : 'N/A'
}));
```

### Firestore 'Get document' query returns empty data for a document ID that exists in the Firebase Console

Cause: The document ID string contains extra whitespace, or the collection path has a typo. Firestore document IDs are exact-match — a trailing space or wrong case returns empty rather than an error.

Solution: Add .trim() to the document ID expression in your Retool query: `{{ table1.selectedRow.id.trim() }}`. Verify the collection name matches exactly (Firestore collection names are case-sensitive). Check that the document ID is not composite (some Firestore document IDs are generated as paths including slashes — in that case, you need the collection path AND the document ID separately).

### Firebase Resource connection fails with 'Error: Could not refresh access token' after previously working

Cause: The Google service account JSON key has been revoked or deactivated in Google Cloud Console, or the key was deleted as part of a security rotation. Service account keys do not expire automatically but can be revoked at any time by Google Cloud administrators.

Solution: Go to Google Cloud Console → IAM & Admin → Service Accounts → select your service account → Keys tab. Check whether the key ID from your JSON file shows as Active. If it is Disabled or Deleted, create a new key, download the JSON, and update the Retool Firebase Resource with the new key contents. If the key still shows as Active, verify the service account itself has not had its Firestore permissions revoked in the IAM Permissions tab.

## Frequently asked questions

### Can Retool run Firestore collection group queries (querying all subcollections with the same name)?

Retool's native Firestore action picker does not expose collection group queries directly. As a workaround, use a generic REST API Resource targeting the Firestore REST API endpoint https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default)/documents:runQuery with a structured query body that sets `from.allDescendants: true`. You will need to configure Bearer Token authentication using a short-lived OAuth token generated from your service account.

### Does Retool support Firestore real-time listeners?

Retool apps do not support Firestore real-time listeners (onSnapshot). Retool queries are request/response — they return data at the moment the query runs and do not maintain a live connection. To approximate real-time updates, enable Retool's built-in query polling by setting 'Refresh interval' on the collection query (e.g., every 5 seconds). This periodically re-fetches the collection, catching recent changes at the cost of additional Firestore read operations.

### How do I write Firestore batch operations from Retool?

Retool's native Firestore action picker does not include batch write operations. For batch updates across multiple documents, use a JavaScript query in Retool that calls multiple Firestore queries in sequence: `await updateDoc1.trigger(); await updateDoc2.trigger();`. For true atomic Firestore transactions, use a Retool Workflow that calls a Cloud Function or a serverless endpoint that executes the Firestore transaction server-side.

### Can I use Retool's Firestore Resource with Firestore in Datastore mode?

Retool's Firebase connector targets Firestore in Native mode, which uses the Cloud Firestore API. Firestore in Datastore mode uses the Cloud Datastore API — a separate API with different endpoints and query syntax. Retool does not have a native Datastore mode connector. If you are using Firestore in Datastore mode, connect via a REST API Resource targeting the Cloud Datastore REST API (https://datastore.googleapis.com/v1/) with OAuth 2.0 authentication.

### What is the maximum number of documents Retool can retrieve from Firestore in one query?

Retool's Firestore queries are subject to the platform's 100MB response size limit. In practice, you should set a Limit of 25-100 documents per query and use cursor-based pagination for larger datasets. Retrieving thousands of documents in a single Retool query is not recommended — it strains Retool's response handling, inflates Firestore read costs (each document returned counts as one read), and makes the UI unresponsive.

### How do I filter Firestore documents by a date range in Retool?

Firestore stores dates as Timestamp objects. To filter by date range, pass JavaScript Date objects or ISO strings as the filter values — Retool's Firestore connector handles the conversion to Firestore Timestamps automatically. In the query filter, set field 'created_at', operator '>=', value `{{ new Date(dateFilter.startDate) }}` and add a second filter for the end date with '<='. Ensure a composite index exists on the 'created_at' field for range queries to execute efficiently.

---

Source: https://www.rapidevelopers.com/retool-integrations/google-cloud-firestore
© RapidDev — https://www.rapidevelopers.com/retool-integrations/google-cloud-firestore
