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
MIT License · Gondola Bros Entertainment