Blog system on Cloudflare Workers, powered by Servant and Miso, using GHC WASM backend

TL;DR

I’ve been implementing some bridge between Servant and Cloudflare Workers to write my own Blog system with GHC WASM backend.

Overview

Recently I have implemented Humblr, a humble clone of a subset of Tumblr, with GHC WASM backend.

And here is the working example:

https://gohan.konn-san.com

(DISCLAIMER: It hosts photos of my cookings in the last ten years or so and doesn’t contain any technical articles :wink:)

This blog system consists of two sides: frontend and backend, which are ALL WRITTEN IN HASKELL!

Frontend is implemented with Miso:

https://haskell-miso.org

… and the backend is written with my libraries for writing Cloudflare Workers in Haskell, which is a (bunch of) extension of my previous post.

In both ends, I extensibly use Servant framework for routing (in both back- and frontends) and static-link generation. I will describe the details below.

DISCLAIMER: I am not working in Web industry, the descriptions and designs below can be wrong and suboptimal. Correct me if I’m on a wrong track!

Backend

The backend consists of five Workers:

  1. Router: parsing HTTP request, do some authorisations if needed, and invokes appropriate dedicated Workers for both frontend and internal REST API. It also takes care of caching. Powered by Servant!
    • Currently, ~995 KiB after compression.
  2. Database: Communicates with Cloudflare D1 (some kind of distributed read replica of SQLite) to manage articles/tags/image metadata, etc.
    • Currently, ~758 KiB after compression.
  3. Storage: Manages article attachments on Cloudflare R2 object storage. It also issues signed URL with expiry to the original image hosted on R2, which will be used by Images worker described below.
    • Currently, ~722 KiB after compression.
  4. Images: Serves user-facing images communicating with Storage backend. It uses Transformation mechanism of Cloudflare Images to delete metadata and resize photos for several purposes.
    • (indeed, Cloudflare Images provieds Storage service, but not available with Free Plan :slight_smile: )
    • Currently, ~664 KiB after compression.
  5. SSR: handles Server-Side Rendering. Currently, only article pages are pre-rendered at the server-side to provide link cards and save REST API calls.
    • Currently, ~977 KiB after compression.

They are communicating with each other with JavaScript RPC mechanism of Service Bindings. Only Router worker is exposed to the public internet.
I once implemented them as a single, monolithic worker, but it significantly exceeds the 1000KiB of size limit in Free Plan. So I separated them into five individual workers.

I polished ghc-wasm-earthly (the name is rather inaccurate - the bunch majority of this monorepo is now dedicated to JavaScript FFI and Cloudflare workers. Perhaps I should re-organise the repo in near future) so that it contains the bindings to Cloudflare D1, R2, KV and can generate custom Service Bindings described above.

I’ve been also developing a simple adapter of Cloudflare Workers for Servant:

Porting Servant to Cloudflare Workers is not a novel idea - indeed, Tweag guys (especially @TerrorJack) had already done this in the Asterius era:

So why the new implementation? The Tweag implementation of Servant on Workers uses wai abstraction to port existing web apps to Workers. They implemented the conversion functions between Wai’s Request/Response and those of Cloudflare Workers.

This approach still work well if the service is closed under Haskell world. Unfortunately, if we want to communicate with Cloudflare’s Edge computing services, such as R2, the object storage, Static Assets, etc., this approach is not enough. Why? Well, the workers should complete the entire computation within roughly 10ms. This might seem relatively limited, but this doesn’t count the waiting time communicating with external services, and manipulating header / cloudflare-related options generally won’t take much so long. Heavy burden can be delegated to the wide variety services of Cloudflare, and all we have to do is call them in order and returns the response body as-is. The runtime API of Cloudflare Workers serves contents as a ReadableStream objects. The point is that, if the ReadableStream remains unmodified, no additional time is consumed.

The last part becomes problematic when one uses WAI-based approach, as it needs to convert between ReadableStreams in JavaScript land and WAI Responses in Haskell World. That’s why I have to implement independent servant-cloudflare-workers library, which is forked from servant-server to directly serving ReadableStreams.

I also had to implement simple version of servant-auth and its implementation for client and workres.

The reason is that it has a dependency to crypton or cryptonite ecosystem, which cannot be built with WASM backend for the time being, and I also want to use Cloudflare Zero Trust as yet another authentication method. Fortunately, Cloudflare Workers provides standard SubtleCrypto API for cryptography, so it is not too hard to rewrite the logic.

Frontend

As mentioned above, I extensively used Miso:

https://haskell-miso.org

Miso is a Haskell SPA framework based on The Elm Architecture, in which we describe the app as the model, view function and transition function.
It also provides Isomorphism, or Server-Side Rendering, mechanism. The SSR backend mentioned above uses this feature indeed.

In Frontend, we must communicate with Backend’s REST API. To do so, I also implemented servant-client-fetch, which is built upon servant-client-core and uses Fetch API as the backend via JSFFI.

Miso also uses Servant as the routing mechanism for frontend (i.e. the decision procedure of the internal model based on window URL), which makes the life with the API much, much easier as we can share API definition across frontend, backend, static link generation and API invocation!

(Random) Concluding Thoughts

I think this can be compelling evidence that we are at the point of being able to use Haskell to implement practical web apps with Cloudflare Workers.

As a concluding remarks, I share some random thoughts and lessons below.

  • Having Servant in our ecosystem is really a killer feature (as always)!
    • We can use the type-level API definition as the single-source of truth for all the API-related things!
      • Perhaps we can use its OpenAPI integration to teach Cloudflare API Shield about the API schema.
    • In addition, its Generics feature to treat endpoints as records together with OverloadedRecordDot extension works perfectly.
    • It would be good to have servant-auth with a little more lightweight dependencies.
  • Writing Frontend in Miso is a Joy!
    • The Elm Architecture feels really functional, and its performance is good enough (at least for my personal usage).
    • It is rather inconvenient that its router mechanism doesn’t allow new instances of HasRouter - it cannot handle, for example, Auth and/or our original Image middlewares. To mitigate this, I have to separate REST API as independent type and pay attention to the actual full-path.
    • It would be much better if there is official document on SSR (Isomorphism) feature.
  • Lessons and Possible Enhancements
    • Service Bindings can return a direct value and/or promise. We are currently assuming the output is the direct value, but it would be good to have one.
    • Router worker still has relatively large binary size. There is much more room for improvements here.
      • The first implementation of Handler monad is composed of transformers, but now it is just a (hand-rolled) RIO (or effectful). This reduced the binary a few ten KiB.
    • (I am not good at desiging Frontend UI :slight_smile: )

Many thanks and kudos to all the pioneers. Happy Haskelling!

18 Likes