Skip to content
tRPC

Firebase

Call procedures from Firebase callable functions and map their errors to HttpsError.

A callable function gets JSON data and Firebase's auth, and reports errors as HttpsError. Call the procedure with call() and map the error code:

import {
  HttpsError,
  onCall,
  type FunctionsErrorCode,
} from 'firebase-functions/v2/https';
import { call, isTrpcError } from '@trpc12/trpc/server';

const firebaseCode: Record<string, FunctionsErrorCode> = {
  BAD_REQUEST: 'invalid-argument',
  UNPROCESSABLE_CONTENT: 'invalid-argument',
  UNAUTHORIZED: 'unauthenticated',
  FORBIDDEN: 'permission-denied',
  NOT_FOUND: 'not-found',
  CONFLICT: 'already-exists',
  PRECONDITION_FAILED: 'failed-precondition',
  TOO_MANY_REQUESTS: 'resource-exhausted',
  TIMEOUT: 'deadline-exceeded',
  CLIENT_CLOSED_REQUEST: 'cancelled',
  NOT_IMPLEMENTED: 'unimplemented',
  SERVICE_UNAVAILABLE: 'unavailable',
};

export const createPost = onCall(async (request) => {
  try {
    return await call(appRouter.post.create, request.data, {
      ctx: { uid: request.auth?.uid ?? null },
    });
  } catch (error) {
    const code = isTrpcError(error) ? firebaseCode[error.code] : undefined;
    // Anything else reaches the client as Firebase's masked `internal`.
    if (!isTrpcError(error) || code === undefined) throw error;
    throw new HttpsError(code, error.message, error.data);
  }
});

The input is validated by the procedure as usual. Firebase's wire format is plain JSON, so outputs lose Dates, Maps and the other types the tRPC serializer keeps; return strings or numbers for those.

Firebase hosting can also serve handler.fetch through an HTTPS function (onRequest) behind Express (Express), which keeps the whole protocol.