Callable Cloud Functions: Client-Server APIs with Firebase Auth
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
- What auth information is available in a callable function's context?
- How do callable functions differ from regular HTTP functions?
- What error codes does
HttpsErrorsupport? - How do you pass complex data from client to callable function?
Answers:
context.authcontainsuid,token(with claims), andemailfrom Firebase Authentication.- Callable functions have automatic CORS, auth context, error Serialization, and work with the Firebase SDK. HTTP functions require manual setup.
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.- 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
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