TechLead
Lesson 7 of 8
8 min read
Firebase

Cloud Functions

Run serverless backend code in response to Firebase events and HTTP requests

What are Cloud Functions?

Cloud Functions for Firebase let you run backend code automatically in response to events triggered by Firebase features and HTTPS requests. Your code is stored in Google's cloud and runs in a managed environment, so there's no need to manage or scale your own servers.

Functions can be triggered by events from Firebase services (Authentication, Firestore, Storage, etc.), HTTP requests, scheduled times (cron), or Google Cloud services.

⚑ Common Use Cases

  • πŸ”” Notifications: Send push notifications when data changes.
  • πŸ“§ Emails: Send welcome emails when users sign up.
  • πŸ–ΌοΈ Image Processing: Generate thumbnails when images are uploaded.
  • πŸ”’ Data Validation: Validate and sanitize data before storage.
  • 🌐 APIs: Build REST APIs and webhooks.
  • ⏰ Scheduled Tasks: Run cleanup jobs on a schedule.

Setting Up Cloud Functions

Initialize Cloud Functions in your project:

# Initialize functions in your project
firebase init functions

# Choose options:
# - JavaScript or TypeScript
# - ESLint (recommended)
# - Install dependencies

This creates a functions directory structure:

my-project/
β”œβ”€β”€ functions/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   └── index.ts        # Function entry point
β”‚   β”œβ”€β”€ package.json
β”‚   └── tsconfig.json
β”œβ”€β”€ firebase.json
└── ...

HTTP Functions

Create functions that respond to HTTP requests:

import { onRequest } from 'firebase-functions/v2/https';

// Simple HTTP function
export const helloWorld = onRequest((request, response) => {
  response.send('Hello from Firebase!');
});

// With CORS enabled
export const api = onRequest({ cors: true }, (request, response) => {
  const { name } = request.query;
  response.json({ message: `Hello, ${name || 'World'}!` });
});

// Handle different HTTP methods
export const users = onRequest({ cors: true }, async (request, response) => {
  switch (request.method) {
    case 'GET':
      // Handle GET request
      response.json({ users: [] });
      break;
    case 'POST':
      // Handle POST request
      const { email, name } = request.body;
      response.json({ id: 'new-user-id', email, name });
      break;
    default:
      response.status(405).send('Method Not Allowed');
  }
});

Firestore Triggers

Run functions when Firestore documents change:

import { onDocumentCreated, onDocumentUpdated, onDocumentDeleted } from 'firebase-functions/v2/firestore';
import { getFirestore } from 'firebase-admin/firestore';

// Initialize admin SDK
import { initializeApp } from 'firebase-admin/app';
initializeApp();

const db = getFirestore();

// Trigger when a document is created
export const onUserCreated = onDocumentCreated('users/{userId}', async (event) => {
  const snapshot = event.data;
  if (!snapshot) return;

  const userData = snapshot.data();
  const userId = event.params.userId;

  // Create a profile document
  await db.collection('profiles').doc(userId).set({
    displayName: userData.name,
    createdAt: new Date(),
    postsCount: 0
  });

  console.log(`Profile created for user: ${userId}`);
});

// Trigger when a document is updated
export const onUserUpdated = onDocumentUpdated('users/{userId}', async (event) => {
  const before = event.data?.before.data();
  const after = event.data?.after.data();

  // Check what changed
  if (before?.email !== after?.email) {
    console.log(`Email changed from ${before?.email} to ${after?.email}`);
    // Send email verification
  }
});

// Trigger when a document is deleted
export const onUserDeleted = onDocumentDeleted('users/{userId}', async (event) => {
  const userId = event.params.userId;

  // Clean up related data
  await db.collection('profiles').doc(userId).delete();
  await db.collection('posts').where('authorId', '==', userId).get()
    .then(snapshot => {
      const batch = db.batch();
      snapshot.docs.forEach(doc => batch.delete(doc.ref));
      return batch.commit();
    });
});

Authentication Triggers

Run functions when users sign up or delete their account:

import { beforeUserCreated, beforeUserSignedIn } from 'firebase-functions/v2/identity';
import { onCall } from 'firebase-functions/v2/https';
import { getAuth } from 'firebase-admin/auth';

// Before user is created (can modify or block)
export const validateNewUser = beforeUserCreated((event) => {
  const user = event.data;

  // Block sign-ups from certain domains
  if (user.email?.endsWith('@blocked.com')) {
    throw new Error('This email domain is not allowed');
  }

  // Add custom claims
  return {
    customClaims: {
      role: 'user',
      createdVia: 'signup'
    }
  };
});

// Callable function to set admin role
export const setAdminRole = onCall(async (request) => {
  // Check if caller is admin
  if (request.auth?.token.role !== 'admin') {
    throw new Error('Only admins can set admin roles');
  }

  const { userId } = request.data;
  await getAuth().setCustomUserClaims(userId, { role: 'admin' });

  return { success: true };
});

Storage Triggers

Process files when they're uploaded to Storage:

import { onObjectFinalized, onObjectDeleted } from 'firebase-functions/v2/storage';
import { getStorage } from 'firebase-admin/storage';
import * as path from 'path';

// Trigger when a file is uploaded
export const processImage = onObjectFinalized(async (event) => {
  const filePath = event.data.name;
  const contentType = event.data.contentType;

  // Only process images
  if (!contentType?.startsWith('image/')) {
    console.log('Not an image, skipping');
    return;
  }

  // Skip if already a thumbnail
  if (filePath?.includes('thumb_')) {
    return;
  }

  const bucket = getStorage().bucket(event.data.bucket);
  const fileName = path.basename(filePath || '');
  const fileDir = path.dirname(filePath || '');

  // Download, resize, and upload thumbnail
  const tempFilePath = `/tmp/${fileName}`;
  await bucket.file(filePath!).download({ destination: tempFilePath });

  // Use sharp or another library for image processing
  // const sharp = require('sharp');
  // await sharp(tempFilePath).resize(200, 200).toFile(thumbPath);

  const thumbFileName = `thumb_${fileName}`;
  const thumbFilePath = path.join(fileDir, thumbFileName);

  await bucket.upload(tempFilePath, {
    destination: thumbFilePath,
    metadata: { contentType }
  });

  console.log(`Thumbnail created: ${thumbFilePath}`);
});

// Trigger when a file is deleted
export const onFileDeleted = onObjectDeleted(async (event) => {
  const filePath = event.data.name;
  console.log(`File deleted: ${filePath}`);

  // Delete associated thumbnail if exists
  if (!filePath?.includes('thumb_')) {
    const bucket = getStorage().bucket(event.data.bucket);
    const fileName = path.basename(filePath || '');
    const fileDir = path.dirname(filePath || '');
    const thumbPath = path.join(fileDir, `thumb_${fileName}`);

    try {
      await bucket.file(thumbPath).delete();
    } catch (error) {
      // Thumbnail might not exist
    }
  }
});

Scheduled Functions

Run functions on a schedule:

import { onSchedule } from 'firebase-functions/v2/scheduler';
import { getFirestore } from 'firebase-admin/firestore';

// Run every day at midnight
export const dailyCleanup = onSchedule('0 0 * * *', async (event) => {
  const db = getFirestore();

  // Delete documents older than 30 days
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

  const oldDocs = await db.collection('logs')
    .where('createdAt', '<', thirtyDaysAgo)
    .get();

  const batch = db.batch();
  oldDocs.docs.forEach(doc => batch.delete(doc.ref));
  await batch.commit();

  console.log(`Deleted ${oldDocs.size} old documents`);
});

// Run every hour
export const hourlySync = onSchedule('0 * * * *', async (event) => {
  console.log('Running hourly sync...');
  // Sync data with external service
});

// Run every Monday at 9 AM
export const weeklyReport = onSchedule('0 9 * * 1', async (event) => {
  console.log('Generating weekly report...');
  // Generate and send report
});

Callable Functions

Functions that can be called directly from client code:

// functions/src/index.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https';

export const addNumbers = onCall((request) => {
  const { a, b } = request.data;

  // Validate input
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new HttpsError('invalid-argument', 'Both arguments must be numbers');
  }

  return { result: a + b };
});

// Protected callable function
export const getUserData = onCall(async (request) => {
  // Check if user is authenticated
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'User must be logged in');
  }

  const userId = request.auth.uid;
  const db = getFirestore();

  const userDoc = await db.collection('users').doc(userId).get();

  if (!userDoc.exists) {
    throw new HttpsError('not-found', 'User not found');
  }

  return userDoc.data();
});
// Client-side usage
import { getFunctions, httpsCallable } from 'firebase/functions';

const functions = getFunctions();

// Call the function
const addNumbers = httpsCallable(functions, 'addNumbers');
const result = await addNumbers({ a: 5, b: 3 });
console.log(result.data.result); // 8

// Call protected function
const getUserData = httpsCallable(functions, 'getUserData');
const userData = await getUserData();
console.log(userData.data);

Environment Configuration

Manage secrets and configuration:

import { defineSecret, defineString } from 'firebase-functions/params';

// Define secrets (stored securely)
const apiKey = defineSecret('API_KEY');
const dbPassword = defineSecret('DB_PASSWORD');

// Define config values
const welcomeMessage = defineString('WELCOME_MESSAGE', {
  default: 'Welcome to our app!'
});

// Use in function
export const secureFunction = onRequest(
  { secrets: [apiKey, dbPassword] },
  async (request, response) => {
    // Access secret values
    const key = apiKey.value();
    const password = dbPassword.value();

    // Use the secrets...
    response.json({ message: welcomeMessage.value() });
  }
);

// Set secrets via CLI:
// firebase functions:secrets:set API_KEY
// firebase functions:secrets:set DB_PASSWORD

Deploying Functions

Deploy your functions to Firebase:

# Deploy all functions
firebase deploy --only functions

# Deploy specific function
firebase deploy --only functions:helloWorld

# Deploy multiple specific functions
firebase deploy --only functions:helloWorld,functions:api

# View function logs
firebase functions:log

# View logs for specific function
firebase functions:log --only helloWorld

Local Testing with Emulator

Test functions locally before deploying:

# Start the emulator
firebase emulators:start --only functions

# Or start all emulators
firebase emulators:start

# Functions will be available at:
# http://localhost:5001/YOUR_PROJECT/us-central1/functionName

# Call HTTP functions
curl http://localhost:5001/my-project/us-central1/helloWorld

# View emulator UI
# http://localhost:4000

πŸ’‘ Key Takeaways

  • β€’ Cloud Functions run serverless backend code in response to events
  • β€’ HTTP functions create REST APIs and webhooks
  • β€’ Firestore/Auth/Storage triggers respond to data changes
  • β€’ Scheduled functions run on cron schedules
  • β€’ Callable functions are invoked directly from client code
  • β€’ Use the emulator for local testing before deployment

πŸ“š Learn More

Continue Learning