• 🤌 Just the right amount of abstraction.
  • ⚛️ Built natively with React hooks to manage state.
  • 🕹️ Minimal boilerplate — take back full control where you need it.
  • 🔌 Strong integration of TypeScript types for functions, data, and more.
  • ☁️ Simplified server logic for Callables, batch execution, and more.
  • 📱 High consistency between react and react-native.
  • ✅ Reliance on well adopted libraries:

1 — Define Realtime Connections

Firebridge mixes the power of Firebase’s Cloud Firestore and React hooks to help you express realtime platform logic in a very simple format. We give you a simple format for defining typed realtime connections to Cloud Firestore.

import { useDocument } from '@firebridge/web'
import { doc } from 'firebase/firestore'

// Your local Firestore instance
import firestore from '@/services/firestore'

const BOOK_ID = 'some-book-id'

const book = useDocument<Book>(
  // Books are stored at `books/{bookId}`.
  doc(firestore, 'books', BOOK_ID),

This format might look familiar. Under the hood on @firebridge/web, we’re using react-firebase-hooks with a few small extensions on the syntax. We’ve also implemented the same hooks for @firebridge/native.

We can also build our own custom hooks from this pattern. This makes it easy to share code throughout your application. For example, we can create a hook that will maintain a realtime connection to a book given its ID.

const useBook = (bookId: string) =>
  useDocument<Book>(doc(firestore, 'books', bookId), [bookId])

// and then
const book = useBook(BOOK_ID)

As you can see, useDocument has a dependency array similar to useEffect. By providing a value, we can ensure the ref is updated when the value changes. This is useful for when we want to maintain a realtime connection to a document that depends on a value that changes over time.

It’s also common to include the user’s ID as part of the path to a document. This makes writing Firestore security rules much easier. In this case, the ref can be provided as a function which accepts the current user’s ID as the first argument.

const useOwnReviewForBook = (bookId: string) =>
    uid =>
      // Reviews are stored at `books/{bookId}/reviews/{userId}`.
      doc(firestore, 'books', bookId, 'reviews', uid),

We can serve fairly complex reqirements by mixing this pattern with Firstore queries. For example, let’s maintain a realtime connection with the book’s three most recent reviews.

Since we’re creating reviews on the server, we can ask Firebridge to automatically include metadata like timeCreated in the documents. This makes sorting by creation date very easy.

import { query, collection, orderBy, limit } from 'firebase/firestore'
import { useCollection } from '@firebridge/web'

const useRecentReviewsForBook = (bookId: string, maxCount?: number) =>
      collection(firestore, 'books', bookId, 'reviews'),
      // Metadata can be included automatically by Firebridge.
      orderBy('metadata.timeCreated', 'desc'),

// and then
const recentReviewsForBook = useRecentReviewsForBook(BOOK_ID)

This is about as complex as it gets. We can use the same syntax to maintain realtime connections to collections, queries, and documents. Once you get used to this syntax, you’ll be able to build realtime connections to your data with ease.

2 — Cloud Callables

Sometimes we need to run an operation on the server before a write can occur. Let’s say when a user posts a review, we want to make sure that the review doesn’t contain any explicit content.

First, we can create a utility to save reviews to the database. We’ll use the firestoreSet utility from @firebridge/cloud to create a function that will save the review to the database. We’ll ask it to include metadata like a FirestoreTimestamp for the timeCreated to make querying easy.

// ... in our Cloud Functions project.
import { firestoreSet } from '@firebridge/cloud'

const setReview = firestoreSet<BookReview>(
  // We can simply use the bookId from the review to build the path.
  ({ data }) => `books/${data.bookId}/reviews`,
  // This will automatically include metadata like `timeCreated`.
  { addMetadata: true },

Now, we can write our callable function. In Firebase, a callable function will include the user’s authentication information in the request. We can use this to make sure we post the review on behalf of the correct user.

// ... in our Cloud Functions project.
import { callable, onCall } from '@firebridge/cloud'

export const onReviewBook = onCall(
  callable<OnReviewBookBody, OnReviewBookResult>({
    action: async (review, { auth }) => {
      // Check for explicit content in the body of the review.
      const isExplicit = await checkForExplicitContent(review.body)
      if (isExplicit) {
        return { success: false, message: 'Explicit content detected.' }

      // Post the review.
      await setReview(auth.uid, review)
      return { success: true }

Using the callable on the client side is as simple as using the useCallable hook. We can even include the same type definitions so that the request and response are typed correctly.

// ... back on the client side.
import { useCallable } from '@firebridge/web'

const useOnReviewBook = useCallable<OnReviewBookBody, OnReviewBookResult>(

// Now, we can use this hook to post reviews.
// For example:
const onReviewBook = useOnReviewBook()
onReviewBook({ bookId, body: 'This book is great!' })

3 — Frontend Experience

Now, we can bring it all together using our hooks and callable function to build a book page with just the right amount of abstraction. Everything is typesafe, and it’s easy to understand what’s going on just from reading the code. What’s more, we haven’t ruled out the full power of Firebase. If we need to, we can always drop down to the Firebase SDK to perform more complex operations.

const BookPage: FC<BookPageProps> = ({ bookId }) => {
  // Realtime Data
  const book = useBook(bookId)
  const ownReview = useOwnReviewForBook(bookId)
  const recentReviews = useRecentReviewsForBook(bookId)

  // Review Submission
  const onReviewBook = useOnReviewBook()
  const [draft, setDraft] = useState('')
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    // If the user has already reviewed the book, populate the draft.
    if (ownReview) setDraft(ownReview.body)
  }, [ownReview])

  const handleSubmitReview = async (body: string) => {
    // Post the review.
    const { success, message } = await onReviewBook({ bookId, body })
    if (!success) alert(message)

  return isLoading ? (
  ) : (

      <h2>Your Review</h2>
      <textarea type="text" onChange={e => setDraft(e.target.value)} />
      <button onClick={() => handleSubmitReview(draft)}>Submit</button>

      <h2>Recent Reviews</h2>
        {recentReviews?.map(({ id, body }) => (
          <li key={id}>{body}</li>

What’s more, when we go to writing our native app, the code will be alost entirely the same. Once you get started with these patterns, you’ll be able to build powerful and consistent developer experiences with Firebase on React Web and Native.