# How to Integrate Lovable with Google Cloud Firestore

- Tool: Lovable
- Difficulty: Intermediate
- Time required: 40 minutes
- Last updated: March 2026

## TL;DR

Connect Google Cloud Firestore to Lovable using the Firestore REST API v1 with a Google service account for OAuth2 authentication. Create an Edge Function that generates a Google access token from the service account credentials and uses it to read and write Firestore documents. Use this when you need Firestore's document database without the full Firebase platform.

## Connect Google Cloud Firestore Directly to Lovable Without Full Firebase

Firestore is available in two contexts: as part of Firebase, or as a standalone Google Cloud service. The full Firebase integration brings Firebase Auth, Storage, and Hosting alongside Firestore, with the Firebase Admin SDK wrapping all the platform pieces. But sometimes you only need Firestore — you have a GCP project with Firestore enabled, you don't use Firebase Auth, or your organization manages cloud resources through Google Cloud Console rather than the Firebase Console.

This standalone Firestore integration uses the Firestore REST API v1 directly, authenticated with a Google Cloud service account. The pattern is similar to the Firebase integration but scoped to Firestore only — no Firebase SDK, no Firebase Auth concepts. You create a service account in Google Cloud Console, assign it the Cloud Datastore User or Firestore Editor role, download the service account JSON, and store it in Lovable Cloud Secrets. The Edge Function handles the Google JWT-to-access-token exchange that Firestore's REST API requires.

The Deno runtime's Web Crypto API handles JWT signing natively, so you don't need external libraries for the authentication step. This makes the Edge Function self-contained and avoids dependency on npm packages that may have compatibility issues in Deno's environment.

## Before you start

- A Lovable account with a project open and Lovable Cloud enabled
- A Google Cloud project with Firestore (Native mode) enabled at console.cloud.google.com
- A Google Cloud service account with the 'Cloud Datastore User' role (for read/write) or 'Cloud Datastore Viewer' (for read-only)
- A service account JSON key file downloaded from Google Cloud Console → IAM & Admin → Service Accounts
- Your Google Cloud project ID and at least one Firestore collection with documents

## Step-by-step guide

### 1. Create a Google Cloud Service Account for Firestore

In the Google Cloud Console at console.cloud.google.com, select your project from the top dropdown and navigate to IAM & Admin → Service Accounts. Click 'Create Service Account'. Give it a meaningful name like 'lovable-firestore-access' and a description like 'Service account for Lovable Edge Function Firestore access'.

Click 'Create and continue'. On the 'Grant this service account access to the project' step, add a role. For read-write access to Firestore, use 'Cloud Datastore User'. For read-only, use 'Cloud Datastore Viewer'. For full admin access (including schema changes), use 'Cloud Datastore Owner' — but this is overly broad for most Lovable apps. Click 'Continue' and then 'Done'.

After creation, click on the service account in the list and navigate to the 'Keys' tab. Click 'Add key' → 'Create new key' → select 'JSON' → 'Create'. Google downloads a JSON file containing: type, project_id, private_key_id, private_key (RSA private key), client_email, client_id, and several auth URIs. This file is your service account credential — protect it like a password. You will paste its contents into Lovable Cloud Secrets in the next step.

**Expected result:** A service account exists in Google Cloud IAM with Firestore permissions. A JSON key file is downloaded to your computer with the service account credentials.

### 2. Store Google Service Account Credentials in Cloud Secrets

In Lovable, click the '+' button next to the Preview panel to open the Cloud tab, then navigate to Secrets. Add these secrets:

- GCP_SERVICE_ACCOUNT_JSON: paste the entire contents of the JSON key file as a single value
- GCP_PROJECT_ID: your Google Cloud project ID (e.g., my-project-12345)

The service account JSON is a multi-line string that may cause issues when pasted into some secret managers. If the Lovable Secrets panel truncates or mangles the value, minify the JSON first by removing all whitespace (use an online JSON minifier or paste into the browser console and run JSON.stringify(JSON.parse(yourJson))).

The Edge Function will parse this JSON to extract the client_email and private_key fields needed for JWT signing. The private key contains literal newline characters — when stored in an environment variable, these are often encoded as the two-character sequence backslash-n rather than actual newlines. The Edge Function must handle this encoding by replacing \\n with actual newline characters before using the key for JWT signing.

**Expected result:** GCP_SERVICE_ACCOUNT_JSON and GCP_PROJECT_ID are saved in Cloud Secrets and accessible via Deno.env.get() in Edge Functions.

### 3. Create the Firestore REST API Edge Function

The core challenge of the standalone Firestore integration is authentication. Firestore's REST API requires a Google OAuth2 access token, obtained by signing a JWT with the service account private key and exchanging it at Google's token endpoint.

The JWT for Google's service accounts has specific required claims: iss (the service account client_email), scope (space-separated Google API scopes — for Firestore use 'https://www.googleapis.com/auth/datastore'), aud (always 'https://oauth2.googleapis.com/token'), iat (current timestamp), and exp (expiry, maximum 1 hour from iat).

Deno's built-in Web Crypto API can sign this JWT using the RS256 algorithm and the service account's RSA private key. After signing, the JWT is exchanged at the Google token endpoint for a short-lived access token. This access token is then used as a Bearer token in all Firestore REST API calls.

The Firestore REST API URL structure is: https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default)/documents/{collection}/{documentId}. For listing a collection, omit the document ID. For structured queries (Firestore's equivalent of SQL WHERE), use the :runQuery endpoint with a StructuredQuery payload.

```
// Key portion of the Google OAuth2 JWT flow for Deno:
// supabase/functions/firestore-proxy/index.ts (auth section)

const serviceAccount = JSON.parse(Deno.env.get("GCP_SERVICE_ACCOUNT_JSON") || "{}");
const privateKeyPem = serviceAccount.private_key.replace(/\\n/g, "\n");

// Import RSA private key for signing
const keyData = privateKeyPem
  .replace(/-----BEGIN PRIVATE KEY-----/, "")
  .replace(/-----END PRIVATE KEY-----/, "")
  .replace(/\n/g, "");
const binaryKey = Uint8Array.from(atob(keyData), c => c.charCodeAt(0));
const cryptoKey = await crypto.subtle.importKey(
  "pkcs8",
  binaryKey,
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false,
  ["sign"]
);

// Build JWT claims
const now = Math.floor(Date.now() / 1000);
const claims = {
  iss: serviceAccount.client_email,
  scope: "https://www.googleapis.com/auth/datastore",
  aud: "https://oauth2.googleapis.com/token",
  iat: now,
  exp: now + 3600,
};

// Sign JWT (header.payload.signature)
const header = btoa(JSON.stringify({ alg: "RS256", typ: "JWT" })).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
const payload = btoa(JSON.stringify(claims)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
const signingInput = `${header}.${payload}`;
const signatureBytes = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", cryptoKey, new TextEncoder().encode(signingInput));
const signature = btoa(String.fromCharCode(...new Uint8Array(signatureBytes))).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
const jwt = `${signingInput}.${signature}`;

// Exchange JWT for access token
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt }),
});
const { access_token } = await tokenResponse.json();
```

**Expected result:** The firestore-proxy Edge Function is deployed and can authenticate with Google Cloud. Testing with a simple getDocument call returns a Firestore document's fields in JSON format.

### 4. Build Lovable Components That Read Firestore Documents

With the Edge Function deployed and authentication working, open the Lovable chat and describe the frontend components you want. Provide your Firestore collection names and document field names — Lovable generates React components that call the firestore-proxy Edge Function.

Firestore REST API responses have a specific structure that differs from a flat JSON object. A document response looks like: { name: 'projects/my-project/databases/(default)/documents/articles/docId', fields: { title: { stringValue: 'Hello' }, count: { integerValue: '42' }, active: { booleanValue: true } }, createTime: '...', updateTime: '...' }. The field values are wrapped in type-specific objects.

When prompting Lovable, describe your Firestore fields and their types so Lovable can generate code that unwraps these type-value objects. Lovable can create a utility function that converts Firestore's field format ({ stringValue: 'text' }, { integerValue: '42' }) into plain JavaScript values ('text', 42) before rendering.

For listing collections, the REST API returns a documents array with the same per-field type-value structure. Pagination uses a pageToken from the previous response as a parameter for the next request.

**Expected result:** A blog posts management page loads in Lovable showing documents from your Firestore collection. Creating, editing, and publishing posts all work with changes reflecting in the Google Cloud Firestore Console.

### 5. Implement Firestore Structured Queries for Filtering

The Firestore REST API supports structured queries through its :runQuery endpoint — the equivalent of Firestore's JavaScript SDK query() method. Structured queries let you filter documents by field value, order results, and limit the response count, all server-side without fetching an entire collection.

The structured query format is different from a simple URL parameter — it's a JSON POST body sent to: POST https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default)/documents:runQuery. The body describes the collection, filter conditions, ordering, and limits using Firestore's StructuredQuery schema.

Ask Lovable to extend the firestore-proxy Edge Function with a 'query' operation that accepts filter parameters and constructs the StructuredQuery payload. Common filter operators include EQUAL, GREATER_THAN, LESS_THAN, ARRAY_CONTAINS, and IN. Compound filters use composite filters with AND or OR operators.

For complex query requirements, RapidDev's team can help design Firestore indexes and structured query patterns for your specific collection structure.

**Expected result:** Filtered queries return only matching Firestore documents. Filtering posts by status or date range works correctly, and the results match what you see in the Google Cloud Firestore Console.

## Best practices

- Replace escaped newline sequences in the service account private key after JSON parsing — this is the most common cause of JWT signing failures in Deno
- Cache Google OAuth2 access tokens for their 3600-second validity period using module-level variables in the Edge Function to avoid per-request token generation overhead
- Extract document IDs from the Firestore name field (last segment of the path) in your Edge Function before returning data to the frontend
- Create a utility function to unwrap Firestore's typed field format (stringValue, integerValue, booleanValue) to plain JavaScript values before returning from the Edge Function
- Use the :runQuery structured query endpoint rather than listing all documents and filtering in JavaScript for any collection with more than a few hundred documents
- Create Firestore composite indexes in the Google Cloud Console before running multi-field queries — Firestore returns an error with a direct link to create the required index
- Use the 'Cloud Datastore Viewer' role for read-only Lovable apps rather than the broader 'Cloud Datastore User' role to limit blast radius if credentials are compromised
- Differentiate this standalone integration from the full Firebase integration — use this when you have a GCP project without the Firebase Console configured

## Use cases

### Build a content management app backed by standalone Firestore

Your Google Cloud project uses Firestore as its database without the full Firebase setup. You want a Lovable-built content editor where team members create, edit, and publish articles stored in Firestore collections, without migrating to Supabase or setting up Firebase Auth.

Prompt example:

```
I have a Google Cloud project called my-project-id with Firestore enabled. My collections are: articles (documents with title, body, author, status: 'draft'/'published', createdAt, updatedAt, slug), and tags (documents with name, slug, count). I've stored my service account JSON in Cloud Secrets as GCP_SERVICE_ACCOUNT_JSON. Create an Edge Function called firestore-proxy that authenticates with Google OAuth2 using the service account and queries Firestore collections. Then build a content dashboard showing all articles with status badges, an editor form for creating/updating articles, and a publish toggle.
```

### Display real-time data from a Firestore collection in a Lovable dashboard

Your GCP backend writes IoT sensor data, analytics events, or user activity to Firestore. A Lovable-built dashboard reads this data on demand, filtered and sorted by timestamp, without needing Firebase's client SDK or real-time listener setup.

Prompt example:

```
I have a Firestore collection called sensor_readings in GCP project my-gcp-project with documents containing: sensorId (string), location (string), temperature (number), humidity (number), pressure (number), recordedAt (timestamp string). Using GCP_SERVICE_ACCOUNT_JSON from Cloud Secrets, create an Edge Function that queries sensor_readings using Firestore's REST API structured query, filtered by sensorId and a date range. Build a monitoring dashboard with sensor selector, date range picker, and charts for each metric using Recharts.
```

### Use Firestore as a configuration and feature flag store

Your team manages application configuration, feature flags, and A/B test parameters in a Firestore collection. A Lovable app reads these at startup and caches them, with an admin interface for updating flags without code deployments.

Prompt example:

```
I use Firestore to store feature flags in a collection called feature_flags. Each document is named by the flag key and has fields: enabled (boolean), rolloutPercentage (number 0-100), description (string), updatedAt (string), updatedBy (string). Using GCP_SERVICE_ACCOUNT_JSON from Cloud Secrets, create an Edge Function to list all flags, get a single flag, and update flag values. Then build a feature flag management page with a table showing all flags, toggle switches for enabled/disabled, percentage sliders for partial rollouts, and a change log.
```

## Troubleshooting

### Edge Function returns '400 Bad Request: Invalid JWT' when exchanging the service account token

Cause: The JWT is malformed, the private key has encoding issues (escaped newlines not converted to real newlines), or the JWT claims are incorrect.

Solution: The most common cause is the private key containing the literal two-character sequence backslash-n instead of real newline characters. After parsing the service account JSON, replace escaped newlines: privateKey.replace(/\\n/g, '\n'). Also verify the JWT claims include the correct scope ('https://www.googleapis.com/auth/datastore') and that the aud is exactly 'https://oauth2.googleapis.com/token' — not the Firestore API URL.

```
// After parsing GCP_SERVICE_ACCOUNT_JSON:
const serviceAccount = JSON.parse(Deno.env.get("GCP_SERVICE_ACCOUNT_JSON") || "{}");
// Fix escaped newlines in private key
const privateKey = serviceAccount.private_key.replace(/\\n/g, "\n");
```

### Firestore REST API returns '403 Permission denied on resource project'

Cause: The service account does not have sufficient IAM permissions on the project or database, or the Firestore database is in a different region than the default.

Solution: Verify the service account has the 'Cloud Datastore User' role in Google Cloud Console → IAM & Admin → IAM. Find the service account by its email address (shown in the service account JSON as client_email) and check its assigned roles. If using a Firestore named database (not the '(default)' database), update the REST API URL to use the correct database name. Also ensure Firestore is enabled in the correct Google Cloud project.

### Firestore documents display raw type-value objects like {stringValue: 'hello'} instead of plain values

Cause: The Firestore REST API returns fields in its typed value format, and the React component is rendering the raw API response without unwrapping the field values.

Solution: Add a utility function to convert Firestore's typed field format to plain JavaScript values before rendering. Pass this through the Edge Function response or handle it in the React component.

```
// Utility to unwrap Firestore field values
function unwrapFirestoreValue(value: Record<string, unknown>): unknown {
  if ('stringValue' in value) return value.stringValue;
  if ('integerValue' in value) return Number(value.integerValue);
  if ('doubleValue' in value) return value.doubleValue;
  if ('booleanValue' in value) return value.booleanValue;
  if ('timestampValue' in value) return value.timestampValue;
  if ('arrayValue' in value) return (value.arrayValue as {values?: unknown[]}).values?.map(unwrapFirestoreValue) || [];
  if ('mapValue' in value) return unwrapFirestoreFields((value.mapValue as {fields?: Record<string, unknown>}).fields || {});
  return null;
}

function unwrapFirestoreFields(fields: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, unwrapFirestoreValue(v as Record<string, unknown>)]));
}
```

### Access token expires and subsequent Firestore calls return 401 Unauthorized

Cause: Google OAuth2 access tokens expire after 1 hour, and the Edge Function is not refreshing them.

Solution: Implement access token caching with expiry checking in your Edge Function. Store the token and its expiry timestamp in a module-level variable. Before each Firestore call, check if the token expires in less than 60 seconds — if so, generate a new token. This avoids generating a token on every request while preventing expired token failures.

```
// Module-level token cache (outside the serve() handler)
let cachedToken: string | null = null;
let tokenExpiry: number = 0;

async function getAccessToken(): Promise<string> {
  if (cachedToken && Date.now() / 1000 < tokenExpiry - 60) {
    return cachedToken;
  }
  // ... generate new token ...
  cachedToken = newToken;
  tokenExpiry = Date.now() / 1000 + 3600;
  return cachedToken;
}
```

## Frequently asked questions

### What is the difference between this integration and the Firebase integration page?

This integration uses Firestore as a standalone Google Cloud service, accessing it through the Google Cloud REST API with a GCP service account. The Firebase integration page covers the full Firebase platform — Firestore plus Firebase Auth, Cloud Storage, and other Firebase-specific features. If you manage your database through the Google Cloud Console rather than the Firebase Console, and you don't use Firebase Auth, this standalone integration is the right approach.

### Does Firestore's real-time listener functionality work with this REST API approach?

No. The Firestore REST API v1 used in this integration is request-response only — it does not support Firestore's real-time onSnapshot listeners that stream document changes over WebSockets. For real-time updates, you would need to install the Firebase JS SDK in your Lovable React frontend and use its WebSocket-based listener directly on the client side. Use this Edge Function pattern for queries and writes, and the Firebase JS SDK for real-time features.

### Can I access Firestore subcollections through this integration?

Yes. Subcollection paths use a slash-separated format in the REST API URL. For a subcollection comments under a document articles/articleId, the path is: projects/{projectId}/databases/(default)/documents/articles/articleId/comments. Pass this full path as the collection parameter in your Edge Function calls. Listing subcollections or querying across all subcollections (collection group queries) requires the :runQuery endpoint with an allDescendants flag.

### How do I create Firestore indexes for my queries?

Simple single-field queries (filtering on one field) use Firestore's automatic single-field indexes. Compound queries that filter on multiple fields or combine a filter with an order-by require composite indexes created in the Google Cloud Console or Firebase Console under Firestore → Indexes. When a query requires an index that doesn't exist, Firestore's API returns a 400 error with a direct link to create the required index — click the link in the error response to create it.

### Can I use Firestore in Datastore mode versus Native mode with this integration?

This integration uses the Cloud Datastore API scope, which works with both Firestore in Native mode and Datastore mode. The REST API URL structure and document format are slightly different for Datastore mode — it uses the Cloud Datastore REST API (datastore.googleapis.com) rather than the Firestore REST API (firestore.googleapis.com). If your project uses Datastore mode (the older format), you need different API endpoints and request structures.

---

Source: https://www.rapidevelopers.com/lovable-integration/google-cloud-firestore
© RapidDev — https://www.rapidevelopers.com/lovable-integration/google-cloud-firestore
