Skip to content

Callable Cloud Functions: Client-Server APIs with Firebase Auth

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Callable Cloud Functions: Client. We cover key concepts, practical examples, and best practices to help you master this topic.

Callable Cloud Functions are HTTPS functions with Firebase Authentication and CORS built in, enabling secure client-to-server communication without managing HTTP middleware.

What You'll Learn

How to write callable functions, handle authentication and errors, pass parameters, call from client SDKs, manage context, and structure multi-endpoint function suites.

Why It Matters

Raw HTTP functions require manual auth verification, CORS setup, and error formatting. Callable functions handle these automatically. DodaTech's Antivirus Pro uses callable functions for license validation, device registration, and remote scan triggers.

Real-World Use

A mobile app triggers a remote scan on the user's device by calling a callable function. The function verifies the user, triggers the scan via FCM, and returns the scan status.

flowchart LR
    A["Client App\ncalling function.httpsCallable()"] --> B["Callable Function\nAuth + CORS Built-in"]
    B --> C{"context.auth\nverified?"}
    C -->|No| D["Throw\nunauthenticated"]
    C -->|Yes| E["Run Business\nLogic"]
    E --> F["Return Result\nto Client"]
    style A fill:#dbeafe,stroke:#2563eb
    style B fill:#fef3c7,stroke:#d97706
    style F fill:#bbf7d0,stroke:#16a34a

Writing a Callable Function

exports.registerDevice = functions.https.onCall(async (data, context) => {
  // Auth check — automatic with callable functions
  if (!context.auth) {
    throw new functions.https.HttpsError(
      "unauthenticated",
      "You must be signed in to register a device"
    );
  }

  const { deviceName, deviceOs, deviceToken } = data;

  if (!deviceName || !deviceOs) {
    throw new functions.https.HttpsError(
      "invalid-argument",
      "deviceName and deviceOs are required"
    );
  }

  const device = {
    userId: context.auth.uid,
    name: deviceName,
    os: deviceOs,
    token: deviceToken || null,
    registeredAt: admin.firestore.FieldValue.serverTimestamp(),
    lastSeen: admin.firestore.FieldValue.serverTimestamp()
  };

  const docRef = await admin.firestore()
    .collection("devices")
    .add(device);

  console.log("Device registered:", docRef.id, "for user:", context.auth.uid);

  return {
    deviceId: docRef.id,
    message: "Device registered successfully"
  };
});

Calling from Client (Web)

import { getFunctions, httpsCallable } from "firebase/functions";

const functions = getFunctions();
const registerDevice = httpsCallable(functions, "registerDevice");

async function addDevice(name, os) {
  try {
    const result = await registerDevice({
      deviceName: name,
      deviceOs: os
    });
    console.log("Device registered:", result.data.deviceId);
    console.log("Message:", result.data.message);
  } catch (error) {
    if (error.code === "functions/unauthenticated") {
      console.error("Please sign in first");
    } else if (error.code === "functions/invalid-argument") {
      console.error("Missing required fields");
    } else {
      console.error("Error:", error.message);
    }
  }
}

addDevice("Home PC", "Windows 11");
// Expected output: Device registered: abc123xyz
//                  Message: Device registered successfully

Error Handling Patterns

exports.analyzeFile = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError(
      "unauthenticated", "Sign in required"
    );
  }

  const { fileId } = data;

  // Validate input
  if (!fileId || typeof fileId !== "string") {
    throw new functions.https.HttpsError(
      "invalid-argument", "Valid fileId is required"
    );
  }

  try {
    // Check file exists and belongs to user
    const fileDoc = await admin.firestore()
      .collection("files")
      .doc(fileId)
      .get();

    if (!fileDoc.exists) {
      throw new functions.https.HttpsError(
        "not-found", "File not found"
      );
    }

    if (fileDoc.data().userId !== context.auth.uid) {
      throw new functions.https.HttpsError(
        "permission-denied", "You do not own this file"
      );
    }

    // Simulate analysis
    const result = {
      fileId,
      status: "clean",
      threats: [],
      analyzedAt: new Date().toISOString()
    };

    console.log("Analysis complete for file:", fileId);
    return result;

  } catch (error) {
    if (error instanceof functions.https.HttpsError) {
      throw error;
    }
    console.error("Analysis error:", error);
    throw new functions.https.HttpsError(
      "internal", "Analysis failed. Please try again."
    );
  }
});

Multi-Result Functions

exports.batchDeviceStatus = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError("unauthenticated", "Sign in required");
  }

  const { deviceIds } = data;

  if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length > 50) {
    throw new functions.https.HttpsError(
      "invalid-argument",
      "Provide up to 50 device IDs as an array"
    );
  }

  // Fetch all devices in parallel
  const snapshots = await Promise.all(
    deviceIds.map((id) =>
      admin.firestore().collection("devices").doc(id).get()
    )
  );

  const devices = {};
  snapshots.forEach((snap) => {
    if (snap.exists && snap.data().userId === context.auth.uid) {
      devices[snap.id] = snap.data();
    }
  });

  console.log("Returning", Object.keys(devices).length, "devices");
  return { devices };
});

Common Mistakes

1. Returning Objects That Can't Be Serialized

Callable functions serialize return data to JSON. Cannot return dates, undefined, or circular references. Convert dates to strings before returning.

2. Throwing Errors Without HttpsError

Throwing generic Error objects returns unhelpful "INTERNAL" errors to clients. Always throw functions.https.HttpsError with appropriate codes.

3. Exceeding Payload Size

Callable functions have a 10MB request and response limit. For large data, use Cloud Storage or paginated responses.

4. Not Validating Input Types

Always validate that data properties exist and are the expected type. Invalid input causes confusing server errors.

5. Forgetting Rate Limiting

Callable functions don't have built-in rate limiting. Implement rate limiting per user using Firestore counters or Cloud Memorystore.

Practice Questions

  1. What auth information is available in a callable function's context?
  2. How do callable functions differ from regular HTTP functions?
  3. What error codes does HttpsError support?
  4. How do you pass complex data from client to callable function?

Answers:

  1. context.auth contains uid, token (with claims), and email from Firebase Authentication.
  2. Callable functions have automatic CORS, auth context, error Serialization, and work with the Firebase SDK. HTTP functions require manual setup.
  3. unauthenticated, permission-denied, not-found, invalid-argument, already-exists, aborted, unavailable, internal, unknown, data-loss, deadline-exceeded, failed-precondition, out-of-range, resource-exhausted, cancelled.
  4. Pass a plain JavaScript object as the first argument. Arrays, nested objects, and primitives serialize to JSON automatically.

Challenge: Build a callable function suite for a device management system: register device, get device status (single + batch), trigger remote scan, and get scan results — all with proper auth and error handling.

FAQ

Are callable functions cheaper than HTTP functions?

Callable functions and HTTP functions use the same pricing. Callable functions save development time by handling auth, CORS, and serialization.

Can I use callable functions with non-Firebase clients?

Callable functions require the Firebase SDK on the client. For third-party clients, use regular HTTP functions with manual auth.

How do I test callable functions locally?

Use the Firebase Emulator Suite. The local emulator provides authentication simulation and function invocation.

What is the timeout for callable functions?

Default is 60 seconds, configurable up to 540 seconds for long-running operations.

Can callable functions return streaming data?

No, callable functions return a single JSON response. For streaming, use Cloud Functions with HTTP/2 or WebSocket through Cloud Run.

Mini Project

Build a device management API with callable functions: register device (validates input, checks ownership), get device list (paginated), trigger remote scan (returns scan ID), and get scan status. Implement proper error handling and test from a web client.

What's Next

Function Deployment & CI/CD — deploy, manage versions, and automate Cloud Functions releases.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro