MIT licensed by Devon Tomlin
Maintained by [email protected]
This version can be pinned in stack with:firebase-hs-0.1.1.0@sha256:3801d678727c502f38297128ef4649c518aa74e1456faa2d6298f2eb6c5f7946,3102

CI Hackage Haskell License


What is firebase-hs?

A pure Haskell library for Firebase services:

  • Auth — JWT verification against Google’s public JWKs, with automatic key caching
  • Firestore — CRUD operations, structured queries, and atomic transactions via the REST API
  • Servant — One-liner auth combinator for Servant servers (optional flag)

Quick Start

Add to your .cabal file:

build-depends: firebase-hs

Verify a Token

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))

Auth

Verification Rules

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 exp must be in the future (within clock skew)
Issued at iat must be in the past (within clock skew)
Subject sub must be non-empty (becomes the Firebase UID)

Key Caching

Keys are fetched lazily on first verification, cached per Google’s Cache-Control: max-age, and refreshed automatically. Thread-safe via STM.

Error Handling

case result of
  Left (KeyFetchError msg) -> logError "Network issue" msg
  Left InvalidSignature    -> respond 401 "Invalid token"
  Left TokenExpired        -> respond 401 "Token expired"
  Left (InvalidClaims msg) -> respond 401 ("Bad claims: " <> msg)
  Left (MalformedToken _)  -> respond 400 "Malformed token"
  Right user               -> handleAuthenticated user

Firestore

CRUD Operations

import Firebase.Firestore

main :: IO ()
main = do
  mgr <- newTlsManager
  let pid = ProjectId "my-project"
      tok = AccessToken "ya29..."

  -- Create
  let fields = Map.fromList [("name", StringValue "Alice"), ("age", IntegerValue 30)]
  _ <- createDocument mgr tok pid (CollectionPath "users") (DocumentId "alice") fields

  -- Read
  let path = DocumentPath (CollectionPath "users") (DocumentId "alice")
  doc <- getDocument mgr tok pid path

  -- Update specific fields
  let updates = Map.fromList [("age", IntegerValue 31)]
  _ <- updateDocument mgr tok pid path ["age"] updates

  -- Delete
  _ <- deleteDocument mgr tok pid path
  pure ()

Structured Queries

Build queries with a pure DSL and (&) composition:

import Data.Function ((&))

let q = query (CollectionPath "users")
      & where_ (fieldFilter "age" OpGreaterThan (IntegerValue 18))
      & orderBy "age" Ascending
      & limit 10

result <- runQuery mgr tok pid q

Composite filters for complex conditions:

let q = query (CollectionPath "users")
      & where_ (compositeAnd
          [ fieldFilter "age" OpGreaterThan (IntegerValue 18)
          , fieldFilter "active" OpEqual (BoolValue True)
          ])

Atomic Transactions

Read-then-write operations that succeed or fail atomically:

result <- runTransaction mgr tok pid ReadWrite $ \txnId -> runExceptT $ do
  -- Reads within the transaction see a consistent snapshot
  d <- ExceptT $ getDocument mgr tok pid userPath
  let newBalance = computeNewBalance (docFields d)
  pure [mkUpdateWrite userPath newBalance]

Retry aborted transactions:

-- First attempt
result <- beginTransaction mgr tok pid ReadWrite
case result of
  Left (TransactionAborted _) ->
    -- Retry with the failed transaction ID for priority
    beginTransaction mgr tok pid (RetryWith txnId)

Firestore Value Types

Values mirror Firestore’s tagged wire format:

data FirestoreValue
  = NullValue | BoolValue !Bool | IntegerValue !Int64
  | DoubleValue !Double | StringValue !Text | TimestampValue !UTCTime
  | ArrayValue ![FirestoreValue] | MapValue !(Map Text FirestoreValue)

Note: integers are encoded as JSON strings ({"integerValue":"42"}), not numbers. The JSON instances handle this transparently.


WAI Middleware

Protect any WAI-based server (Warp, Scotty, Yesod, Spock) with Firebase auth. Enable with the wai cabal flag:

cabal build -f wai

Simple Gate

Reject unauthenticated requests before they reach your app:

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

With User Propagation

Store the authenticated user in the WAI vault for downstream handlers:

import Firebase.Auth.WAI (firebaseAuth, lookupFirebaseUser)

main = run 3000 $ firebaseAuth cache cfg myApp

myHandler req respond = case lookupFirebaseUser req of
  Just user -> respond (ok200 ("Hello, " <> fuUid user))
  Nothing   -> respond (err500 "unreachable")

Servant

Enable with the servant cabal flag:

cabal build -f servant

One-liner auth for any Servant server:

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)

The handler extracts the Bearer token, verifies it against Google’s keys, and injects a FirebaseUser into your endpoint — or returns 401 with a descriptive error.


API Reference

Auth

verifyIdToken       :: Manager -> FirebaseConfig -> ByteString -> IO (Either AuthError FirebaseUser)
newKeyCache         :: Manager -> IO KeyCache
newTlsKeyCache      :: IO KeyCache
verifyIdTokenCached :: KeyCache -> FirebaseConfig -> ByteString -> IO (Either AuthError FirebaseUser)
parseCacheMaxAge    :: ResponseHeaders -> Maybe Int

Firestore

getDocument    :: Manager -> AccessToken -> ProjectId -> DocumentPath -> IO (Either FirestoreError Document)
createDocument :: Manager -> AccessToken -> ProjectId -> CollectionPath -> DocumentId -> Map Text FirestoreValue -> IO (Either FirestoreError Document)
updateDocument :: Manager -> AccessToken -> ProjectId -> DocumentPath -> [Text] -> Map Text FirestoreValue -> IO (Either FirestoreError Document)
deleteDocument :: Manager -> AccessToken -> ProjectId -> DocumentPath -> IO (Either FirestoreError ())
runQuery       :: Manager -> AccessToken -> ProjectId -> StructuredQuery -> IO (Either FirestoreError [Document])

Transactions

beginTransaction    :: Manager -> AccessToken -> ProjectId -> TransactionMode -> IO (Either FirestoreError TransactionId)
commitTransaction   :: Manager -> AccessToken -> ProjectId -> TransactionId -> [Value] -> IO (Either FirestoreError ())
rollbackTransaction :: Manager -> AccessToken -> ProjectId -> TransactionId -> IO (Either FirestoreError ())
runTransaction      :: Manager -> AccessToken -> ProjectId -> TransactionMode -> (TransactionId -> IO (Either FirestoreError [Value])) -> IO (Either FirestoreError ())

WAI Middleware

requireAuth        :: KeyCache -> FirebaseConfig -> Middleware
firebaseAuth       :: KeyCache -> FirebaseConfig -> Middleware
lookupFirebaseUser :: Request -> Maybe FirebaseUser

Servant

firebaseAuthHandler :: KeyCache -> FirebaseConfig -> AuthHandler Request FirebaseUser
extractBearerToken  :: Request -> Maybe ByteString
authErrorToBody     :: AuthError -> LBS.ByteString

Full Haddock documentation is available on Hackage.


Build & Test

cabal build                              # Build library
cabal test                               # Run all tests (40 pure tests)
cabal build --ghc-options="-Werror"      # Warnings as errors
cabal build -f wai                       # Build with WAI middleware
cabal build -f servant                   # Build with Servant combinator
cabal haddock                            # Generate docs

Changes

Changelog

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’s polymorphic verifyJWT with custom claims subtype
  • JWK-based key fetching with Cache-Control: max-age caching
  • STM-backed KeyCache for thread-safe concurrent verification
  • Strict JWKSet evaluation on fetch (zero deferred parse cost on hot path)
  • Convenience newTlsKeyCache constructor
  • 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 (query, where_, orderBy, limit)
  • Composite filters (compositeAnd, compositeOr) for complex query conditions
  • FirestoreValue ADT with custom JSON instances matching Firestore’s tagged wire format
  • Document type with automatic JSON decoding from Firestore responses

Atomic Transactions

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

WAI Auth Middleware (optional)

  • requireAuth — simple gate middleware, rejects unauthenticated requests
  • firebaseAuth — vault-based middleware, propagates FirebaseUser to handlers
  • lookupFirebaseUser — retrieve authenticated user from WAI vault
  • Gated behind wai cabal flag (default off)
  • Works with Warp, Scotty, Yesod, Spock, and any WAI-based framework

Servant Auth Combinator (optional)

  • firebaseAuthHandler — one-liner Firebase auth for Servant servers
  • Gated behind servant cabal flag (default off, zero extra deps)
  • Pure helpers: extractBearerToken, authErrorToBody

Internal

  • Pure URL builders and error parsers in Firebase.Firestore.Internal (fully testable)
  • Redacted Show instances for AccessToken and TransactionId (no credential leakage)
  • 40 pure tests: value roundtrips, document decoding, URL construction, query DSL, transaction encoding, error parsing