Learn Google Firestore Using Azure Functions and Firebase Admin SDK

Sep 18, 2026DatabasesEngineeringCloud
Summarize with AI: Google AI Claude ChatGPT Perplexity Grok

AI links open with a title + excerpt (these tools can't fetch the page themselves) — use "Copy full article" to paste the complete text for a fuller summary.

Share:

Introduction

This article demonstrates how to connect Google Firestore from an Azure Functions API using the Firebase Admin SDK.

Our API runs on Azure and our blog is in Cosmos DB. So the first question anybody asks is: why Firestore also?

Why Two Clouds

Contact form messages are not blog content. They are inbound messages, and we have several sites in the group collecting them.

We already had one Firestore project serving the other sites. So we had two options. Add a new Cosmos container for this site and build a merge screen later, or write into the collection the group already reads.

Every message carries a domain field.

const DOMAIN = 'example.com';
const submission: ContactSubmission = { id, name, email, message, createdAt, domain: DOMAIN };
await db.collection(COLLECTION).doc(id).set(submission);

One contacts collection for all sites. The inbox for one site is a filter on domain. The inbox for the whole group is the same query without the filter.

The blog stayed in Cosmos DB. It has no group-wide use, so moving it would give nothing.

Point to note: "which cloud runs my code" and "where should this data live" are two different questions.

With the following steps, you can use Firestore from Azure Functions.

  1. Create the Firebase project and service account.
  2. Store the key in an environment variable.
  3. Initialize the SDK safely.
  4. Write and read the documents.

Create the Firebase Project and Service Account

Open the Firebase console and create a project, or open your existing one. Then go to Project settings and select the Service accounts tab.

Service accounts tab in Firebase project settings

Click Generate new private key. It downloads a JSON file. This file is a full credential — treat it like a password.

Generate the private key JSON file

Now open Firestore Database and create the database, then add a collection named contacts.

Create the contacts collection in Firestore

Store the Key in an Environment Variable

The Firebase Admin SDK normally wants that JSON file on disk. Azure Static Web Apps has no disk to keep it on, and committing it to Git is a straight secret leak.

So we put the whole JSON into one environment variable and parse it at startup.

// api/src/firestore.ts
function getServiceAccount(): Record<string, unknown> {
  const raw = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
  if (!raw) {
    throw new Error(
      'Missing required environment variable: FIREBASE_SERVICE_ACCOUNT_KEY. ' +
      'Set it in local.settings.json for local development, ' +
      'or in Azure Static Web Apps Application Settings for production.'
    );
  }
  try { return JSON.parse(raw); }
  catch { throw new Error('FIREBASE_SERVICE_ACCOUNT_KEY is not valid JSON.'); }
}

Two different errors, two different messages. "Not set" and "set but broken" need two different fixes, and the second one is the one you will actually hit.

The reason is the private key inside that JSON. It is a PEM block full of \n escapes. When you paste it into a settings box it is very easy to break it. A plain JSON.parse failure gives you Unexpected token at position 412 and you will spend twenty minutes finding which variable that was.

My suggestion when you paste it: keep the JSON on one single line, keep the \n inside the private key as literal backslash-n, and wrap the whole value in quotes.

Set FIREBASE_SERVICE_ACCOUNT_KEY in Application Settings

Initialize the SDK Safely

This is the Firebase problem that appears only in serverless.

export function getDb(): Firestore {
  if (!_db) {
    if (!getApps().length) {
      _app = initializeApp({ credential: cert(getServiceAccount() as any) });
    }
    _db = getFirestore(_app ?? getApps()[0]);
  }
  return _db;
}

initializeApp() throws an error if the default app already exists. On a normal server this is hard to hit. In Azure Functions the module stays in memory between requests, and several function files import the same module, so calling it twice is very easy.

So there are three guards here.

  • Our own _db cache.
  • getApps().length, which checks the global Firebase registry, not only our variable.
  • _app ?? getApps()[0], so if some other code created the app first, we attach to that one instead of failing.

It is the same single-client pattern as our Cosmos DB code, with one extra check because Firebase keeps a global state that we do not own.

Write and Read the Documents

The write is POST /api/contact and it is anonymous, so it is checked before it reaches Firestore — required fields, email format, then the Cloudflare Turnstile captcha. Cheap checks first, the network check next, the database write last.

The read is the admin inbox.

const snapshot = await db.collection(COLLECTION)
  .where('domain', '==', DOMAIN)
  .get();

const messages = snapshot.docs
  .map(doc => doc.data() as ContactMessage)
  .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());

See that we filter in Firestore but sort in JavaScript.

Firestore can sort, but if you use where on one field and orderBy on another field, it needs a composite index. Until you create it, the query fails at runtime and gives you a link to go and create it.

Firestore asks you to create a composite index

For an admin page with a few hundred documents, sorting in memory is one less thing to create and remember. But note the .get() with no .limit() — this reads every matching document. When the collection grows you must add the index and paging.

For delete, read the document first and return 404 if it is missing. Firestore delete() on a document that does not exist finishes happily, and then your admin page shows "deleted" when nothing happened.

What This Costs

Let me be honest about the price of mixing two clouds.

  • One more dependency and one more startup path.
  • One more secret to keep out of Git and to rotate.
  • Two mental models — Cosmos SQL and RU in one file, Firestore collections and composite indexes in the next.
  • No transaction across the two databases.

What we got: the messages land where the group already reads them, and we built no merge layer.

Multi-cloud as a plan is usually a mistake. You get the complexity of both and the benefit of neither. Multi-cloud as a result, where one dataset has a clear reason to live somewhere, is only following the data. Keep the line between them clean and write down the reason.

Conclusion

In this article we learned how to create a Firebase service account, keep it in an environment variable, initialize the Admin SDK safely inside Azure Functions, and read and write Firestore documents.

Reference

#firestore#gcp#azure-functions#firebase-admin#multi-cloud

Comments

Be the first to comment.

Leave a comment

Never shown publicly.

Comments are reviewed before appearing publicly.