firebase-hs
Firebase Auth, Firestore, and Servant integration for Haskell
https://github.com/Gondola-Bros-Entertainment/firebase-hs
| Stackage Nightly 2026-07-30: | 0.3.0.0 |
| Latest on Hackage: | 0.3.0.0 |
firebase-hs-0.3.0.0@sha256:3f5bd69b0270362bb1d0b85ff0be957278d18e9e2c9f7eb1c32fb1e2898ec447,3408Module documentation for 0.3.0.0
firebase-hs
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
updateMaskfield 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. FirestoreValuecovers every type Firestore stores.bytesValue,referenceValue, andgeoPointValuehad no representation, so a document containing any of them failed to decode outright andgetDocumentreturnedInvalidResponsefor data Firestore considers perfectly valid.- Non-finite doubles travel in proto3 JSON’s string spellings.
DoubleValueholding NaN or an infinity previously encoded asnullor"+inf", which Firestore rejects, and a stored"NaN","Infinity", or"-Infinity"failed to decode, making any document that held one unreadable. runQueryposts 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 thefromcollection ID, which Firestore rejects; only top-level collections were queryable.- A query result whose document fails to decode is reported as
InvalidResponseinstead of being silently dropped, which misreported what the query matched. integerValuestrings outside theInt64range are rejected. The digits were read straight intoInt64, so an out-of-range value silently wrapped instead of failing.{"doubleValue": null}is rejected. The null previously fell through to aeson’sDoubleparser, which reads JSON null as NaN.- A cache refresh no longer overwrites a newer key set installed by a concurrent verification.
runQueryandrunQueryInTransactionclassify 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 asNetworkError "HTTP <status>"instead ofDocumentNotFound,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.
TimestampValueencodes at most nanosecond precision. AUTCTimecarrying 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
Firestorehandle in place of theManager -> AccessToken -> ProjectIdprefix every one of them repeated. Build it withnewFirestore, and refresh an expiring token withwithTokenrather than rebuilding the connection pool. FirebaseUsergained fields, so positional construction no longer compiles. Pattern matches onfuUid,fuEmail, andfuNameare unaffected.commitTransactionand therunTransactioncallback take the new opaqueWritetype in place of rawAeson.Value, withmkUpdateWriteandmkDeleteWriteconstructing it. Hand-assembled write JSON no longer typechecks.Firebase.Firestore.Internal.queryUrlis replaced byrunQueryUrl, which takes the queried collection so it can address the parent resource.KeyCacheis now backed byIORefrather thanTVar, andFirebase.Authre-exports it as an abstract type. Construct it withnewKeyCacheornewTlsKeyCache.- Dropped the
stmdependency: the cache is a single reference with no composed transactions, whichatomicModifyIORef'covers. - Dropped the
mtldependency from theservantflag.Firebase.Servantnow builds its 401 on theHandlernewtype andtransformers, which does not depend on whichMonadErrorre-export a given servant version ships. Firebase.Servantno longer exportsextractBearerTokenorauthErrorToBody. Both were duplicates of theFirebase.Auth.WAIversions; useFirebase.Auth.Internal.bearerTokenandFirebase.Auth.authErrorMessage.- A response body that is JSON but not shaped like a Firestore error now
reports
NetworkError "HTTP <status>"instead ofFirestoreApiError status "" "unknown error".
Added
getDocumentInTransactionandrunQueryInTransactionread inside a transaction. The reads see the transaction’s snapshot and Firestore verifies at commit that nothing they read has changed, which is what makesrunTransactiona transaction rather than an atomic batch write; reads without the transaction ID never did either.- Token claims are no longer discarded.
FirebaseUsernow carriesfuEmailVerified,fuPicture,fuAuthTime,fuSignInProvider, andfuCustomClaims, withhasClaimandlookupClaimto 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: Bearerchallenge RFC 6750 requires. listDocumentslists a collection.collectionUrlhad described itself as “used for listing” since the first release with nothing to use it.BytesValue,ReferenceValue,GeoPointValue, and theGeoPointtype.mkUpdateWriteandmkDeleteWritebuild the writescommitTransactionandrunTransactionexpect.mkUpdateWritewas 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.authErrorMessagerenders anAuthErroras a client-safe body.documentResourceName, the percent-encoding helpers,splitCollectionPath,documentInTransactionUrl, and the pure response decoders (decodeBody,decodeDocumentList,decodeQueryResults,decodeTransactionId) are exported fromFirebase.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 aKeyFetchErrororNetworkErrorresult. - 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. Loweredbaseto>= 4.18, admitting GHC 9.6. http-client >= 0.7.13, the first release whoseShow Requestredacts theAuthorizationheader. AnHttpExceptionrendered intoNetworkErrortext therefore never carries the access token.- CI builds the
waiandservantmodules, 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
joseJWT backend with directcryptonRS256 verification - Dropped
jose,lens,memorydependencies for a significantly lighter dependency tree JWKSet(from jose) replaced with internalJwkSet/JwkKeytypes inFirebase.Auth.Types- License changed from MIT to BSD-3-Clause
Added
JwkSetandJwkKeytypes 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
stmbounds to< 2.7 - Widened
textbounds to>= 1.2 && < 2.2 - Compatible with modern Haskell crypto stack (
crypton)
Removed
josedependency (source ofmemory/ramincompatibility)lensdependency (only used for jose’s API)FirebaseClaims,HasClaimsSetinternal types (replaced with direct JSON parsing)
Unchanged
- Public API for
verifyIdToken,verifyIdTokenCached,newKeyCache,newTlsKeyCache FirebaseConfig,FirebaseUser,AuthError,KeyCachetypes- WAI middleware (
Firebase.Auth.WAI) - Servant combinator (
Firebase.Servant) - Firestore client (all modules unchanged)
- All 46 tests pass
0.1.1.0
Fixed
- Import
throwErrorfromControl.Monad.Except(mtl) instead ofServant.Serverre-export for compatibility with newer servant versions - Add explicit
mtldependency underservantflag
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-agecaching - STM-backed
KeyCachefor 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
FirestoreValueADT with custom JSON instances matching Firestore’s tagged wire format
Atomic Transactions
beginTransaction,commitTransaction,rollbackTransactionfor manual controlrunTransactionfor automatic begin/commit/rollback with callbackTransactionModesum 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