BSD-3-Clause licensed by Devon Tomlin
Maintained by [email protected]
This version can be pinned in stack with:firebase-hs-0.3.0.0@sha256:3f5bd69b0270362bb1d0b85ff0be957278d18e9e2c9f7eb1c32fb1e2898ec447,3408

firebase-hs

CI Hackage License

Firebase for Haskell:

  • Auth: Firebase ID token (JWT) verification against Google’s public keys, with RS256 via crypton and automatic key caching
  • Firestore: CRUD, structured queries, and atomic transactions over the REST API
  • WAI / Servant: auth middleware and an auth combinator, each behind an optional cabal flag

Full API documentation lives on Hackage.

Install

build-depends: firebase-hs

The web integrations are off by default; enable the ones you use:

cabal build -f wai      # Firebase.Auth.WAI
cabal build -f servant  # Firebase.Servant

Auth

import Firebase.Auth

main :: IO ()
main = do
  cache <- newTlsKeyCache
  let cfg = defaultFirebaseConfig "my-project-id"
  result <- verifyIdTokenCached cache cfg tokenBytes
  case result of
    Left err   -> putStrLn ("Auth failed: " ++ show err)
    Right user -> putStrLn ("UID: " ++ show (fuUid user))

Build one KeyCache at startup and share it across threads; keys refresh automatically per Google’s Cache-Control header.

A token is accepted only if every one of these holds:

Check Rule
Algorithm RS256 only
Signature Must match a Google public key
Issuer https://securetoken.google.com/<projectId>
Audience Must equal your Firebase project ID
Expiry / issued-at exp in the future, iat in the past, within clock skew
Subject sub non-empty (becomes the Firebase UID)

Roles live in custom claims, set by the Admin SDK’s setCustomUserClaims:

if hasClaim "admin" user then handleAdmin user else refuse

Firestore

import qualified Data.Map.Strict as Map
import Firebase.Firestore

main :: IO ()
main = do
  fs <- newFirestore (ProjectId "my-project") (AccessToken "ya29...")

  let path = DocumentPath (CollectionPath "users") (DocumentId "alice")
  _ <- createDocument fs (CollectionPath "users") (DocumentId "alice")
         (Map.fromList [("name", StringValue "Alice"), ("age", IntegerValue 30)])
  _ <- updateDocument fs path ["age"] (Map.fromList [("age", IntegerValue 31)])
  doc <- getDocument fs path
  print doc

Build one Firestore handle and share it; it holds a pooled connection manager. Access tokens expire, so swap in a fresh one with withToken rather than rebuilding the handle.

Queries compose with (&); subcollections are addressed by path:

import Data.Function ((&))

result <- runQuery fs $
  query (CollectionPath "users")
    & where_ (fieldFilter "age" OpGreaterThan (IntegerValue 18))
    & orderBy "age" Ascending
    & limit 10

Transactions read with the transaction ID and return the writes to commit. On any failure the transaction is rolled back, and losing a contention race surfaces as TransactionAborted:

result <- runTransaction fs ReadWrite $ \txnId -> runExceptT $ do
  doc <- ExceptT (getDocumentInTransaction fs txnId path)
  pure [mkUpdateWrite (fsProject fs) path (applyDebit 100 (docFields doc))]

WAI

import Firebase.Auth (newTlsKeyCache, defaultFirebaseConfig)
import Firebase.Auth.WAI (requireAuth)
import Network.Wai.Handler.Warp (run)

main :: IO ()
main = do
  cache <- newTlsKeyCache
  let cfg = defaultFirebaseConfig "my-project-id"
  run 3000 (requireAuth cache cfg myApp)

firebaseAuth additionally stores the verified FirebaseUser in the request vault for lookupFirebaseUser to read downstream.

Servant

import Firebase.Auth (newTlsKeyCache, defaultFirebaseConfig)
import Firebase.Servant (firebaseAuthHandler)
import Servant.Server (Context (..))

main :: IO ()
main = do
  cache <- newTlsKeyCache
  let cfg = defaultFirebaseConfig "my-project-id"
      ctx = firebaseAuthHandler cache cfg :. EmptyContext
  runSettings defaultSettings (serveWithContext api ctx server)

Build and Test

cabal build all -f wai -f servant --enable-tests --ghc-options="-Werror"
cabal test

BSD-3-Clause. Maintained by Gondola Bros Entertainment.

Changes

Changelog

0.3.0.0

Fixed

  • Percent-encode every caller-supplied URL component. Project IDs, collection paths, document IDs, and updateMask field paths were interpolated raw, so a value containing ?, #, %, &, a space, or any non-ASCII character produced a malformed request or injected query parameters. Subcollection / separators are preserved; the segments between them are encoded.
  • FirestoreValue covers every type Firestore stores. bytesValue, referenceValue, and geoPointValue had no representation, so a document containing any of them failed to decode outright and getDocument returned InvalidResponse for data Firestore considers perfectly valid.
  • Non-finite doubles travel in proto3 JSON’s string spellings. DoubleValue holding NaN or an infinity previously encoded as null or "+inf", which Firestore rejects, and a stored "NaN", "Infinity", or "-Infinity" failed to decode, making any document that held one unreadable.
  • runQuery posts to the queried collection’s parent resource, so a subcollection path (users/abc/posts) now queries that subcollection. The full path used to be sent as the from collection ID, which Firestore rejects; only top-level collections were queryable.
  • A query result whose document fails to decode is reported as InvalidResponse instead of being silently dropped, which misreported what the query matched.
  • integerValue strings outside the Int64 range are rejected. The digits were read straight into Int64, so an out-of-range value silently wrapped instead of failing.
  • {"doubleValue": null} is rejected. The null previously fell through to aeson’s Double parser, which reads JSON null as NaN.
  • A cache refresh no longer overwrites a newer key set installed by a concurrent verification.
  • runQuery and runQueryInTransaction classify their error responses. Firestore’s streaming endpoints frame an error as a single-element JSON array, which the parser did not read, so a query that failed with an error response surfaced as NetworkError "HTTP <status>" instead of DocumentNotFound, PermissionDenied, TransactionAborted, or a specific API error.
  • A query stream that fails after emitting some results reports the error instead of returning the partial page as a success. The trailing error entry was dropped, so a truncated result set looked complete.
  • TimestampValue encodes at most nanosecond precision. A UTCTime carrying sub-nanosecond digits formatted to more than the nine fractional digits Firestore accepts, which it rejects on write.

Breaking Changes

  • Firestore operations take a single Firestore handle in place of the Manager -> AccessToken -> ProjectId prefix every one of them repeated. Build it with newFirestore, and refresh an expiring token with withToken rather than rebuilding the connection pool.
  • FirebaseUser gained fields, so positional construction no longer compiles. Pattern matches on fuUid, fuEmail, and fuName are unaffected.
  • commitTransaction and the runTransaction callback take the new opaque Write type in place of raw Aeson.Value, with mkUpdateWrite and mkDeleteWrite constructing it. Hand-assembled write JSON no longer typechecks.
  • Firebase.Firestore.Internal.queryUrl is replaced by runQueryUrl, which takes the queried collection so it can address the parent resource.
  • KeyCache is now backed by IORef rather than TVar, and Firebase.Auth re-exports it as an abstract type. Construct it with newKeyCache or newTlsKeyCache.
  • Dropped the stm dependency: the cache is a single reference with no composed transactions, which atomicModifyIORef' covers.
  • Dropped the mtl dependency from the servant flag. Firebase.Servant now builds its 401 on the Handler newtype and transformers, which does not depend on which MonadError re-export a given servant version ships.
  • Firebase.Servant no longer exports extractBearerToken or authErrorToBody. Both were duplicates of the Firebase.Auth.WAI versions; use Firebase.Auth.Internal.bearerToken and Firebase.Auth.authErrorMessage.
  • A response body that is JSON but not shaped like a Firestore error now reports NetworkError "HTTP <status>" instead of FirestoreApiError status "" "unknown error".

Added

  • getDocumentInTransaction and runQueryInTransaction read inside a transaction. The reads see the transaction’s snapshot and Firestore verifies at commit that nothing they read has changed, which is what makes runTransaction a transaction rather than an atomic batch write; reads without the transaction ID never did either.
  • Token claims are no longer discarded. FirebaseUser now carries fuEmailVerified, fuPicture, fuAuthTime, fuSignInProvider, and fuCustomClaims, with hasClaim and lookupClaim to read them. Custom claims are how Firebase expresses roles, so without them a verified token could establish identity but not authorize anything.
  • 401 responses from the WAI middleware and the Servant handler carry the WWW-Authenticate: Bearer challenge RFC 6750 requires.
  • listDocuments lists a collection. collectionUrl had described itself as “used for listing” since the first release with nothing to use it.
  • BytesValue, ReferenceValue, GeoPointValue, and the GeoPoint type.
  • mkUpdateWrite and mkDeleteWrite build the writes commitTransaction and runTransaction expect. mkUpdateWrite was referenced by the README and Haddock but never existed, leaving no supported way to construct a write.
  • Firebase.Auth.Internal: base64url padding and bearer-token extraction, shared by the verifier and both web integrations.
  • authErrorMessage renders an AuthError as a client-safe body.
  • documentResourceName, the percent-encoding helpers, splitCollectionPath, documentInTransactionUrl, and the pure response decoders (decodeBody, decodeDocumentList, decodeQueryResults, decodeTransactionId) are exported from Firebase.Firestore.Internal.

Changed

  • Bearer scheme names are matched case-insensitively, as RFC 7235 requires.
  • Network calls catch only HttpException. Anything else, asynchronous cancellation in particular, propagates instead of surfacing as a KeyFetchError or NetworkError result.
  • Widened bounds: http-client-tls < 0.5 (resolves the Stackage report in issue #1), containers < 0.9, aeson < 2.4, time < 1.17, crypton < 2. Lowered base to >= 4.18, admitting GHC 9.6.
  • http-client >= 0.7.13, the first release whose Show Request redacts the Authorization header. An HttpException rendered into NetworkError text therefore never carries the access token.
  • CI builds the wai and servant modules, which no job previously compiled, and adds a GHC compatibility matrix, an sdist build, and an ASCII-only source check.

0.2.0.0

Breaking Changes

  • Replaced jose JWT backend with direct crypton RS256 verification
  • Dropped jose, lens, memory dependencies for a significantly lighter dependency tree
  • JWKSet (from jose) replaced with internal JwkSet/JwkKey types in Firebase.Auth.Types
  • License changed from MIT to BSD-3-Clause

Added

  • JwkSet and JwkKey types for Google’s public key representation
  • Manual JWT parsing with base64-bytestring (no jose overhead)
  • RS256 signature verification via crypton (Crypto.PubKey.RSA.PKCS15)

Changed

  • Bumped GHC to 9.8.4, base >= 4.19
  • Widened stm bounds to < 2.7
  • Widened text bounds to >= 1.2 && < 2.2
  • Compatible with modern Haskell crypto stack (crypton)

Removed

  • jose dependency (source of memory/ram incompatibility)
  • lens dependency (only used for jose’s API)
  • FirebaseClaims, HasClaimsSet internal types (replaced with direct JSON parsing)

Unchanged

  • Public API for verifyIdToken, verifyIdTokenCached, newKeyCache, newTlsKeyCache
  • FirebaseConfig, FirebaseUser, AuthError, KeyCache types
  • WAI middleware (Firebase.Auth.WAI)
  • Servant combinator (Firebase.Servant)
  • Firestore client (all modules unchanged)
  • All 46 tests pass

0.1.1.0

Fixed

  • Import throwError from Control.Monad.Except (mtl) instead of Servant.Server re-export for compatibility with newer servant versions
  • Add explicit mtl dependency under servant flag

0.1.0.0

Initial release.

Auth

  • Firebase ID token (JWT) verification against Google’s public keys
  • RS256 signature validation via jose
  • JWK-based key fetching with Cache-Control: max-age caching
  • STM-backed KeyCache for thread-safe concurrent verification
  • Full claims validation: issuer, audience, expiry, issued-at, subject
  • Configurable clock skew (default 300s)

Firestore REST API Client

  • CRUD operations: getDocument, createDocument, updateDocument, deleteDocument
  • Structured query DSL with composable builder pattern
  • Composite filters for complex query conditions
  • FirestoreValue ADT with custom JSON instances matching Firestore’s tagged wire format

Atomic Transactions

  • beginTransaction, commitTransaction, rollbackTransaction for manual control
  • runTransaction for automatic begin/commit/rollback with callback
  • TransactionMode sum type: ReadWrite, RetryWith, ReadOnly

WAI Auth Middleware (optional, wai flag)

  • requireAuth, firebaseAuth, lookupFirebaseUser

Servant Auth Combinator (optional, servant flag)

  • firebaseAuthHandler, extractBearerToken, authErrorToBody

Internal

  • Pure URL builders and error parsers in Firebase.Firestore.Internal
  • 41 pure tests