Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with Google Cloud Firestore

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.

What you'll learn

  • How to create a Google Cloud service account with Firestore access permissions
  • How to generate Google OAuth2 access tokens in a Deno Edge Function using Web Crypto JWT signing
  • How to call the Firestore REST API v1 for document reads, writes, and queries
  • How standalone Firestore differs from Firebase's platform integration in Lovable
  • How to structure Firestore collection and document paths for REST API access
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read40 minutesDatabaseMarch 2026RapidDev Engineering Team
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.

Integration method

Edge Function Integration

Google Cloud Firestore connects to Lovable through an Edge Function that authenticates with Google's OAuth2 service using a service account, then calls the Firestore REST API v1. Credentials are stored in Cloud Secrets, and the Edge Function handles JWT signing and token exchange before making Firestore calls.

Prerequisites

  • 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.

Pro tip: Use the principle of least privilege — if your Lovable app only reads Firestore, assign 'Cloud Datastore Viewer' only. This limits the impact if the service account key is ever compromised. You can always upgrade permissions later.

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.

Pro tip: Verify the stored JSON by adding a temporary console.log in your Edge Function that prints the first 50 characters of GCP_SERVICE_ACCOUNT_JSON. If it starts with '{"type":"service_account"', the JSON stored correctly. Remove the log after verifying.

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.

Lovable Prompt

Create an Edge Function called firestore-proxy at supabase/functions/firestore-proxy/index.ts. It should: parse GCP_SERVICE_ACCOUNT_JSON from Deno.env.get to get client_email and private_key, use Deno's Web Crypto API (importKey with RS256 and the RSA private key) to sign a JWT with the Google service account token claims, exchange the JWT at https://oauth2.googleapis.com/token for an OAuth2 access token (scope: https://www.googleapis.com/auth/datastore), then use that access token to call the Firestore REST API at https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default)/documents. Support operations: getDocument (GET by collection/docId), listDocuments (GET collection with optional pageSize/pageToken), createDocument (POST to collection), updateDocument (PATCH to collection/docId with updateMask), deleteDocument (DELETE by collection/docId). Cache the access token for up to 3500 seconds to avoid generating a new one per request. Include CORS headers.

Paste this in Lovable chat

supabase/functions/firestore-proxy/index.ts
1// Key portion of the Google OAuth2 JWT flow for Deno:
2// supabase/functions/firestore-proxy/index.ts (auth section)
3
4const serviceAccount = JSON.parse(Deno.env.get("GCP_SERVICE_ACCOUNT_JSON") || "{}");
5const privateKeyPem = serviceAccount.private_key.replace(/\\n/g, "\n");
6
7// Import RSA private key for signing
8const keyData = privateKeyPem
9 .replace(/-----BEGIN PRIVATE KEY-----/, "")
10 .replace(/-----END PRIVATE KEY-----/, "")
11 .replace(/\n/g, "");
12const binaryKey = Uint8Array.from(atob(keyData), c => c.charCodeAt(0));
13const cryptoKey = await crypto.subtle.importKey(
14 "pkcs8",
15 binaryKey,
16 { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
17 false,
18 ["sign"]
19);
20
21// Build JWT claims
22const now = Math.floor(Date.now() / 1000);
23const claims = {
24 iss: serviceAccount.client_email,
25 scope: "https://www.googleapis.com/auth/datastore",
26 aud: "https://oauth2.googleapis.com/token",
27 iat: now,
28 exp: now + 3600,
29};
30
31// Sign JWT (header.payload.signature)
32const header = btoa(JSON.stringify({ alg: "RS256", typ: "JWT" })).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
33const payload = btoa(JSON.stringify(claims)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
34const signingInput = `${header}.${payload}`;
35const signatureBytes = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", cryptoKey, new TextEncoder().encode(signingInput));
36const signature = btoa(String.fromCharCode(...new Uint8Array(signatureBytes))).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
37const jwt = `${signingInput}.${signature}`;
38
39// Exchange JWT for access token
40const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
41 method: "POST",
42 headers: { "Content-Type": "application/x-www-form-urlencoded" },
43 body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt }),
44});
45const { access_token } = await tokenResponse.json();

Pro tip: Token generation adds ~200-400ms to each request. Implement module-level token caching: store the access token and its expiry time in a variable outside the request handler. Check if the cached token is still valid (more than 60 seconds remaining) before generating a new one.

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.

Lovable Prompt

Using the firestore-proxy Edge Function, build a blog posts management page. My Firestore collection is 'posts' with documents having fields: title (string), body (string), author (string), status (string: draft/published/archived), slug (string), createdAt (string timestamp), updatedAt (string timestamp), viewCount (integer). Create a helper function that converts Firestore's field value format ({stringValue: 'x'}, {integerValue: '5'}) to plain JavaScript values. Build: a table listing all posts with title, author, status badge, view count, and created date; filters for status; an edit form that loads the document's current values and updates changed fields using PATCH; and a publish/unpublish toggle button.

Paste this in Lovable chat

Pro tip: Firestore document IDs are embedded in the name field as the last path segment. Extract them with: const docId = document.name.split('/').pop(). This is the ID to use for update and delete operations.

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.

Lovable Prompt

Extend the firestore-proxy Edge Function to support a 'query' operation. It should accept: collection (string), where (array of objects with field, operator (EQUAL/GREATER_THAN/LESS_THAN/ARRAY_CONTAINS), value), orderBy (optional array of objects with field and direction), limit (optional number). Construct and send a Firestore StructuredQuery to the :runQuery endpoint. Handle Firestore's field value format in the query (wrapping values in stringValue, integerValue, booleanValue objects based on JavaScript type). Return the documents array with field values unwrapped to plain JavaScript. Use this to add date-range filtering to my posts page.

Paste this in Lovable chat

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.

Common 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.

Lovable Prompt

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.

Copy this prompt to try it in Lovable

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.

Lovable Prompt

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.

Copy this prompt to try it in Lovable

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.

Lovable Prompt

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.

Copy this prompt to try it in Lovable

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.

typescript
1// After parsing GCP_SERVICE_ACCOUNT_JSON:
2const serviceAccount = JSON.parse(Deno.env.get("GCP_SERVICE_ACCOUNT_JSON") || "{}");
3// Fix escaped newlines in private key
4const 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.

typescript
1// Utility to unwrap Firestore field values
2function unwrapFirestoreValue(value: Record<string, unknown>): unknown {
3 if ('stringValue' in value) return value.stringValue;
4 if ('integerValue' in value) return Number(value.integerValue);
5 if ('doubleValue' in value) return value.doubleValue;
6 if ('booleanValue' in value) return value.booleanValue;
7 if ('timestampValue' in value) return value.timestampValue;
8 if ('arrayValue' in value) return (value.arrayValue as {values?: unknown[]}).values?.map(unwrapFirestoreValue) || [];
9 if ('mapValue' in value) return unwrapFirestoreFields((value.mapValue as {fields?: Record<string, unknown>}).fields || {});
10 return null;
11}
12
13function unwrapFirestoreFields(fields: Record<string, unknown>): Record<string, unknown> {
14 return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, unwrapFirestoreValue(v as Record<string, unknown>)]));
15}

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.

typescript
1// Module-level token cache (outside the serve() handler)
2let cachedToken: string | null = null;
3let tokenExpiry: number = 0;
4
5async function getAccessToken(): Promise<string> {
6 if (cachedToken && Date.now() / 1000 < tokenExpiry - 60) {
7 return cachedToken;
8 }
9 // ... generate new token ...
10 cachedToken = newToken;
11 tokenExpiry = Date.now() / 1000 + 3600;
12 return cachedToken;
13}

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

Alternatives

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.