Firebase HTTPS callable functions use the onCall handler from firebase-functions/v2/https to create server-side endpoints that automatically handle authentication, CORS, and data serialization. The client calls them with httpsCallable from the Firebase SDK. Unlike raw onRequest functions, callable functions verify the Firebase ID token, pass the authenticated user's context to your handler, and serialize request/response data automatically.
Creating Secure Server-Side Endpoints with Firebase Callable Functions
Callable functions are the recommended way to run server-side logic from Firebase client apps. They handle auth token verification, CORS headers, and JSON serialization out of the box, so you can focus on business logic. This tutorial shows you how to write a v2 callable function, call it from a web app, validate inputs, handle errors, and test locally with emulators.
Prerequisites
- A Firebase project on the Blaze plan (required for Cloud Functions)
- Cloud Functions initialized in your project (firebase init functions)
- Firebase JS SDK v9+ installed in your client app
- Node.js 18+ installed
Step-by-step guide
Create a basic callable function with onCall
Create a basic callable function with onCall
Import onCall from firebase-functions/v2/https and export a named function. The handler receives a request object containing data (the payload from the client) and auth (the authenticated user's token, if any). Return a value or object that Firebase automatically serializes and sends to the client.
1// functions/src/index.ts2import { onCall, HttpsError } from 'firebase-functions/v2/https'34export const greetUser = onCall((request) => {5 // request.auth is populated if the caller is authenticated6 if (!request.auth) {7 throw new HttpsError('unauthenticated', 'You must be signed in')8 }910 const name = request.data.name11 if (!name || typeof name !== 'string') {12 throw new HttpsError('invalid-argument', 'Name is required')13 }1415 return {16 message: `Hello, ${name}! Your UID is ${request.auth.uid}`17 }18})Expected result: The function is ready to deploy and will greet authenticated users by name.
Call the function from the client
Call the function from the client
Import getFunctions and httpsCallable from firebase/functions. Create a callable reference using the function name, then call it with your data payload. The response contains a data property with whatever the function returned.
1// src/lib/functions.ts2import { getFunctions, httpsCallable } from 'firebase/functions'34const functions = getFunctions()56export async function greetUser(name: string) {7 const greet = httpsCallable(functions, 'greetUser')8 const result = await greet({ name })9 return result.data as { message: string }10}1112// Usage in a component13const response = await greetUser('Alice')14console.log(response.message) // "Hello, Alice! Your UID is abc123"Expected result: The client receives the response object from the callable function with the greeting message.
Add input validation with HttpsError
Add input validation with HttpsError
Use HttpsError to throw structured errors that the client can handle by code. Firebase defines standard error codes: 'invalid-argument', 'unauthenticated', 'permission-denied', 'not-found', 'already-exists', and more. The client receives these as a FirebaseError with the code and message.
1// functions/src/index.ts2import { onCall, HttpsError } from 'firebase-functions/v2/https'34interface CreatePostData {5 title: string6 body: string7 category: string8}910export const createPost = onCall(async (request) => {11 if (!request.auth) {12 throw new HttpsError('unauthenticated', 'Sign in required')13 }1415 const { title, body, category } = request.data as CreatePostData1617 if (!title || title.length > 200) {18 throw new HttpsError('invalid-argument', 'Title must be 1-200 characters')19 }20 if (!body || body.length > 10000) {21 throw new HttpsError('invalid-argument', 'Body must be 1-10000 characters')22 }23 if (!['tech', 'design', 'business'].includes(category)) {24 throw new HttpsError('invalid-argument', 'Invalid category')25 }2627 // Use Admin SDK to write to Firestore (bypasses security rules)28 const { getFirestore } = await import('firebase-admin/firestore')29 const db = getFirestore()3031 const docRef = await db.collection('posts').add({32 title,33 body,34 category,35 authorId: request.auth.uid,36 createdAt: new Date()37 })3839 return { postId: docRef.id }40})Expected result: Invalid input throws a typed error the client can catch. Valid input creates a Firestore document and returns the new ID.
Handle errors on the client
Handle errors on the client
When a callable function throws an HttpsError, the client receives it as a standard error. Catch it and check the code property to display appropriate messages. The error includes the code, message, and optional details.
1import { getFunctions, httpsCallable } from 'firebase/functions'2import { FirebaseError } from 'firebase/app'34const functions = getFunctions()56async function submitPost(title: string, body: string, category: string) {7 const createPost = httpsCallable(functions, 'createPost')89 try {10 const result = await createPost({ title, body, category })11 const { postId } = result.data as { postId: string }12 console.log('Post created:', postId)13 return postId14 } catch (error) {15 if (error instanceof FirebaseError) {16 switch (error.code) {17 case 'functions/unauthenticated':18 alert('Please sign in to create a post')19 break20 case 'functions/invalid-argument':21 alert(error.message)22 break23 default:24 alert('Something went wrong. Please try again.')25 }26 }27 throw error28 }29}Expected result: Specific error types trigger targeted error messages in the UI.
Test the callable function with the Emulator Suite
Test the callable function with the Emulator Suite
Connect your client to the Functions emulator during development so you can test without deploying. The emulator runs your function code locally and provides instant feedback through logs in the terminal.
1// In your client initialization code2import { getFunctions, connectFunctionsEmulator } from 'firebase/functions'34const functions = getFunctions()56if (import.meta.env.DEV) {7 connectFunctionsEmulator(functions, '127.0.0.1', 5001)8}910// Start the emulator:11// firebase emulators:start --only functionsExpected result: Client calls route to the local Functions emulator. Function logs appear in the terminal running the emulators.
Deploy the callable function
Deploy the callable function
Deploy your function to Firebase and verify it works in production. Use selective deployment to push only functions. After deployment, remove the emulator connection on the client and the function is live.
1firebase deploy --only functions:greetUser,functions:createPostExpected result: Functions are deployed and accessible from your production client app.
Complete working example
1// functions/src/index.ts2import { onCall, HttpsError } from 'firebase-functions/v2/https'3import { initializeApp } from 'firebase-admin/app'4import { getFirestore } from 'firebase-admin/firestore'56initializeApp()7const db = getFirestore()89// Simple callable function with auth check10export const greetUser = onCall((request) => {11 if (!request.auth) {12 throw new HttpsError('unauthenticated', 'You must be signed in')13 }14 const name = request.data.name15 if (!name || typeof name !== 'string') {16 throw new HttpsError('invalid-argument', 'Name is required')17 }18 return { message: `Hello, ${name}!`, uid: request.auth.uid }19})2021// Callable function with Firestore write22export const createPost = onCall(async (request) => {23 if (!request.auth) {24 throw new HttpsError('unauthenticated', 'Sign in required')25 }2627 const { title, body, category } = request.data28 if (!title || typeof title !== 'string' || title.length > 200) {29 throw new HttpsError('invalid-argument', 'Title must be 1-200 chars')30 }31 if (!body || typeof body !== 'string' || body.length > 10000) {32 throw new HttpsError('invalid-argument', 'Body must be 1-10000 chars')33 }3435 const docRef = await db.collection('posts').add({36 title,37 body,38 category: category || 'general',39 authorId: request.auth.uid,40 authorEmail: request.auth.token.email || null,41 createdAt: new Date(),42 updatedAt: new Date()43 })4445 return { postId: docRef.id }46})4748// Callable function to delete own post49export const deletePost = onCall(async (request) => {50 if (!request.auth) {51 throw new HttpsError('unauthenticated', 'Sign in required')52 }5354 const { postId } = request.data55 if (!postId || typeof postId !== 'string') {56 throw new HttpsError('invalid-argument', 'Post ID is required')57 }5859 const postRef = db.collection('posts').doc(postId)60 const post = await postRef.get()6162 if (!post.exists) {63 throw new HttpsError('not-found', 'Post does not exist')64 }65 if (post.data()?.authorId !== request.auth.uid) {66 throw new HttpsError('permission-denied', 'You can only delete your own posts')67 }6869 await postRef.delete()70 return { deleted: true }71})Common mistakes when writing HTTPS Callable Functions in Firebase
Why it's a problem: Using onRequest instead of onCall and then manually parsing auth tokens and handling CORS
How to avoid: Use onCall for endpoints called from Firebase client SDKs. It handles auth verification, CORS, and data serialization automatically. Use onRequest only for webhooks or third-party integrations that need raw HTTP access.
Why it's a problem: Throwing regular JavaScript errors instead of HttpsError, which causes the client to receive a generic 'internal' error
How to avoid: Always throw HttpsError with a specific code and message. Regular throws are caught by Firebase and converted to 'internal' errors, hiding the actual problem from the client.
Why it's a problem: Trusting client-sent data without validation, allowing invalid or malicious input
How to avoid: Validate every field in request.data on the server. Check types, lengths, and allowed values. The client can send anything — never trust it.
Why it's a problem: Forgetting to initialize firebase-admin before using Firestore or other admin services in the function
How to avoid: Call initializeApp() from firebase-admin/app at the top level of your functions file, before any function handlers. This only needs to be done once.
Best practices
- Use onCall for client-facing functions and onRequest only for webhooks or external API integrations
- Always validate input data and throw HttpsError with specific codes for meaningful client-side error handling
- Check request.auth before performing any authenticated operation and throw 'unauthenticated' if missing
- Use the Admin SDK inside callable functions to bypass security rules and perform privileged operations securely
- Test functions locally with the Emulator Suite before deploying to avoid unnecessary deploys and cold start waiting
- For applications with many callable functions handling complex business logic, RapidDev can help structure your functions architecture and implement robust error handling patterns
- Deploy functions selectively by name to avoid accidentally updating unrelated functions
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me how to write a Firebase Cloud Functions v2 callable function using onCall that receives a title and body, validates input, writes to Firestore with the Admin SDK, and returns the new document ID. Include the client-side code to call it with httpsCallable and handle errors.
Create a set of Firebase callable functions for a blog app: createPost (validates title/body, writes to Firestore), deletePost (checks ownership before deleting), and getPost (fetches a post by ID). Use onCall v2 syntax with HttpsError for validation. Include the client-side caller functions with TypeScript types.
Frequently asked questions
What is the difference between onCall and onRequest?
onCall automatically handles authentication (verifies Firebase ID tokens), CORS headers, and JSON serialization. onRequest gives you a raw Express-compatible handler where you must handle all of these manually. Use onCall for client app endpoints and onRequest for webhooks or third-party APIs.
Can I call a callable function without being authenticated?
Yes. If the caller is not signed in, request.auth will be null. Your function can check for this and either allow unauthenticated access or throw an 'unauthenticated' HttpsError.
What are the timeout limits for callable functions?
V2 callable functions can run for up to 60 minutes (3600 seconds). V1 callable functions are limited to 9 minutes (540 seconds). The default timeout is 60 seconds for v2. Configure it in the function options: onCall({ timeoutSeconds: 300 }, handler).
Can I use callable functions with App Check?
Yes. Callable functions automatically verify App Check tokens when App Check is enabled. Set the enforceAppCheck option to true to reject requests without valid App Check tokens.
How do I return a large amount of data from a callable function?
Callable functions have a maximum response size of 10 MB. For larger data, write the data to Cloud Storage and return a signed URL, or paginate the response using offset and limit parameters.
Can I call a callable function from another Cloud Function?
While possible, it is better to extract the shared logic into a regular async function and call it directly from both functions. This avoids unnecessary HTTP overhead and auth token verification between server-side functions.
Do callable functions support streaming responses?
No. Callable functions return a single JSON response. For streaming, use an onRequest function with Server-Sent Events, or write incremental results to a Firestore document that the client listens to with onSnapshot.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation