Building Cloud Functions as Services for Firestore in Firebase

Building Cloud Functions as Services for Firestore in Firebase 

In the world of serverless development, Firebase has revolutionized how we build scalable apps with its seamless integration of authentication, real-time databases, and hosting. At the heart of many Firebase projects lies Firestore, a NoSQL document database that excels at handling structured data with real-time synchronization. However, while Firestore’s client SDKs make it tempting to query and manipulate data directly from your app’s frontend (what we’ll call “direct calls to the page”), this approach can introduce security vulnerabilities, expose sensitive logic, and complicate maintenance as your app grows. 

This article explores why and how to shift toward using Cloud Functions for Firebase as intermediary services—essentially, creating backend APIs that proxy requests to Firestore. By routing operations through these functions, you gain better control, enhanced security, and the flexibility to handle complex logic server-side. We’ll cover the rationale, best practices, and a step-by-step guide to implementation. 

Why Avoid Direct Firestore Calls from the Client? 

Direct client-side interactions with Firestore are Firebase’s bread-and-butter for rapid prototyping and real-time features. Your app can read/write documents, listen for changes, and even work offline with local persistence. But as your app matures, limitations emerge: 

  • Security Risks: Client-side code is visible to users. Malicious actors can inspect network requests, tamper with data, or bypass validation. For instance, if your app directly writes to Firestore, a savvy user could modify JavaScript to insert unauthorized data, even if security rules are in place. Security rules help, but they’re not foolproof against determined attacks—always assume the client is compromised. 
  • Exposed Sensitive Operations: Direct calls might reveal API keys or require embedding business logic (e.g., payment processing or third-party API integrations) in the client, risking leaks. Cloud Functions let you hide these behind the server. 
  • Complex Queries and Aggregations: Firestore shines for simple reads/writes but struggles with joins, aggregations, or multi-collection queries without multiple client-side calls. Functions consolidate this into a single, efficient endpoint. 
  • No Real-Time for Aggregated Data: Direct queries support snapshots for live updates, but custom computations (e.g., user analytics) don’t. Functions can precompute and push updates via triggers. 
  • Offline Limitations: Functions require an online connection, so reserve direct calls for read-heavy, real-time UI elements like chat messages. Use functions for writes or sensitive reads to ensure consistency. 

In short: Use direct calls for simple, public reads (e.g., product listings). Route everything else through functions to enforce validation, logging, and rate limiting server-side. 

When to Use Cloud Functions as Firestore Services 

Cloud Functions act as event-driven, serverless compute—triggered by HTTP requests, Firestore changes, or schedules. For Firestore services, focus on HTTPS callable functions (preferred over raw HTTP for automatic auth handling) or HTTP triggers for REST-like APIs. 

Key Use Cases

  • Data Validation and Writes: Sanitize inputs, check permissions, and write to Firestore atomically. 
  • Aggregated Queries: Fetch related documents across collections and return a unified response. 
  • Third-Party Integrations: Call external APIs (e.g., Stripe for payments) without exposing keys. 
  • Scheduled Tasks: Use Firestore triggers to update caches or send notifications. 
  • Microservices: Build reusable endpoints for multiple clients (web, mobile). 

Trade-offs: Functions add latency (~100-200ms cold starts) and costs (billed per invocation), but they’re scalable and integrate natively with Firebase Auth and App Check for abuse prevention. 

Best Practices for Implementation 

Before diving into code, heed these guidelines: 

  • Use Callable Functions: They auto-bundle Firebase Auth tokens, reducing boilerplate. 
  • Enforce Auth and App Check: Always verify context.auth in functions; enable App Check to block bots. 
  • Error Handling: Return structured errors (e.g., { code: ‘INVALID_ARGUMENT’, message: ‘…’ }) for client-side parsing. 
  • Idempotency: For writes, use transaction IDs to avoid duplicates. 
  • Testing: Use the Firebase Emulator Suite for local dev—no deployment needed. 
  • Monitoring: Integrate with Google Cloud Logging; set budgets to cap costs. 
  • Performance: Keep functions lightweight; offload heavy compute to Cloud Run if needed. 

Avoid functions for pure real-time reads—stick to client SDKs there to leverage offline sync and snapshots. 

Step-by-Step Guide: Building a Firestore Service 

Let’s build a simple service: A callable function that creates a user profile in Firestore, validates input, and integrates with a third-party email service (simulated). We’ll use Node.js/TypeScript. 

1. Set Up Your Environment 

  • Install Firebase CLI: npm install -g firebase-tools. 
  • Init a project: firebase login, then firebase init functions in your project root. 
  • Choose JavaScript or TypeScript (we’ll use TS for type safety). 
  • Install deps: cd functions && npm install firebase-admin firebase-functions. 

Your functions/package.json should include: 

json 

 “dependencies”: { 

   “firebase-admin”: “^12.0.0”

   “firebase-functions”: “^5.0.0” 

 } 

2. Write the Cloud Function 

In functions/src/index.ts: 

typescript 

import * as functions from ‘firebase-functions’

import * as admin from ‘firebase-admin’

 

admin.initializeApp(); 

 

// Callable function: createUserProfile 

export const createUserProfile = functions.https.onCall(async (data, context) =>

 // Auth check: Ensure user is logged in 

 if (!context.auth) { 

   throw new functions.https.HttpsError(‘unauthenticated’, ‘Must be logged in.’); 

 } 

 

 // Validate input 

 const { name, email, preferences } = data

 if (!name || typeof name !== ‘string’ || name.length < 2) { 

   throw new functions.https.HttpsError(‘invalid-argument’, ‘Name must be a string >= 2 chars.’); 

 } 

 if (!email || !/^[^s@]+@[^s@]+.[^s@]+$/.test(email)) { 

   throw new functions.https.HttpsError(‘invalid-argument’, ‘Invalid email.’); 

 } 

 

 const userId = context.auth.uid

 const profile =

   uid: userId

   name

   email

   preferences: preferences || {}, 

   createdAt: admin.firestore.FieldValue.serverTimestamp(), 

 }; 

 

 try

   // Transaction for atomicity 

   await admin.firestore().runTransaction(async (transaction) =>

     const userRef = admin.firestore().collection(‘users’).doc(userId); 

     const doc = await transaction.get(userRef); 

     if (doc.exists) { 

       throw new functions.https.HttpsError(‘already-exists’, ‘Profile already exists.’); 

     } 

     transaction.set(userRef, profile); 

   }); 

 

   // Simulate third-party integration (e.g., send welcome email via SendGrid) 

   console.log(`Welcome email sent to ${email}`); 

 

   return { success: true, message: ‘Profile created!’ }; 

 } catch (error) { 

   console.error(‘Error creating profile:’, error); 

   throw new functions.https.HttpsError(‘internal’, ‘Failed to create profile.’); 

 } 

}); 

This function: 

  • Validates and sanitizes data server-side. 
  • Uses the Admin SDK for privileged Firestore access (bypasses security rules safely). 
  • Handles errors gracefully. 

3. Deploy the Function 

From functions/: 

eploy –only functions 

Your function is now live at a URL like https://us-central1-yourproject.cloudfunctions.net/createUserProfile. Note the region for low latency. 

4. Call from Your Client App 

In a web app (using Firebase JS SDK v9+ modular): 

javascript 

import { getFunctions, httpsCallable } from ‘firebase/functions’

import { getAuth } from ‘firebase/auth’

 

const auth = getAuth(); 

const functions = getFunctions(); // Specify region if needed: getFunctions(‘us-central1’) 

 

// Assume user is signed in 

const createProfile = httpsCallable(functions, ‘createUserProfile’); 

createProfile({ name: ‘John Doe’, email:john@example.com, preferences: { theme: ‘dark’ } }) 

 .then((result) =>

   console.log(result.data); // { success: true, message: ‘Profile created!’ } 

 }) 

 .catch((error) =>

   console.error(error.code, error.message); 

 }); 

For mobile (Flutter/Android/iOS), use the equivalent SDK methods—auth tokens are auto-attached. 

5. Secure and Optimize 

  • Security Rules: Lock down Firestore: allow read, write: if false; since all access routes through functions. 
  • App Check: Enforce it in the console to verify requests. 
  • Testing Locally: firebase emulators:start –only functions to test without deploying. 

Real-World Examples and Samples 

Firebase’s official samples showcase this pattern: 

  • User Status Indicator: Uses functions to sync online/offline status across Firestore and Realtime Database. 
  • Image Processing: Triggers on uploads to moderate/convert files before storing in Firestore metadata. 
  • Analytics to BigQuery: Pipes Firestore changes to external analytics via functions. 

For a full REST API, wrap multiple operations in Express.js within a single function 

 

Leave a comment

Your email address will not be published. Required fields are marked *