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!

38 Likes

Yeah I’ve struggled to find much info about this either. It sounds really cool, and I want to know more about how it works and how to use it! In general, Miso could do with better documentation. The Haddocks are fairly sparse, for example, and have some weird formatting and typos. Fortunately, its API is conceptually very simple so it’s mostly easy enough to figure things out for oneself.

Anyway, I’ve always liked Miso but been very wary of GHCJS, so it working so well with WASM feels like an exciting new dawn. I’m quite hopeful that once the process becomes smoother (right now, getting to a Hello World requires installing some extra tools and copying a boilerplate JS shim), we can advertise it as the obvious choice for all the people who like Elm but no longer see it as a serious option.

3 Likes

Agreed! It’s simplicity is really comfortable but there is still room for documentation of advanced features to be improved. As for the SSR feature in my impl., it just uses the ToHtml instance for Views (the class itself comes from lucid) and Router mechanism, which are provided by Miso by default.
I realised my usage of SSR feature seems rather non-standard one though - I made the backend to serve a complete pre-rendered HTML, with internal state embedded as a JSON. It seems that the standard usage is to serve an HTML with pre-rendered head, and then fetch a body content pre-rendered separately from another endpoint.

I also used routing mechanism differently - most of the existing examples seem to use routers to determine Views from URLs, but I used it to route Actions. This is because I need some extra API call to actually build a page content because URL itself doesn’t contain all the needed information.

Anyway, it would be much nicer if some kind of usage example showcase that lists best practices of Miso.

Indeed! It would be good for everyone if tooling for WASM backend becomes more simplified. It seems that people engaged in GHC’s cross-compilation backends (including WASM and JS) tend to use Nix as a canonical build system, but it would be much nicer if we can just use cabal and ghcup.

3 Likes

I’m not sure exactly what the long term plan looks like, but as I understand it this is very much a goal. Nix is a temporary crutch (and not actually necessary, but probably the easiest way for now, at least for those already familiar with Nix).

2 Likes

Thanks a lot for building cool stuff using the ghc wasm backend! I’ll add the humblr project to the end-to-end test scripts in ghc-wasm-meta, so future ghc-9.10/ghc-9.12 updates of wasm backend will build humblr in CI. Shall there be upstream breaking changes, it’ll be caught as early as possible.

You might also want to remove the TH-related cabal flags in cabal-wasm.project too. The announcement blog post is not published yet, though the ghc-9.10 flavour provided by ghc-wasm-meta is built from my non-official branch which includes backports for Template Haskell & ghci support! It’s already used by miso examples and ormolu playground, and I’m curious if your projects can also take advantage of it.

It seems that people engaged in GHC’s cross-compilation backends (including WASM and JS) tend to use Nix as a canonical build system, but it would be much nicer if we can just use cabal and ghcup.

Hey, I can’t speak for the JS backend team, but I don’t consider Nix as “canonical” for my work! ghc-wasm-meta does provide nix flakes for nix users, but the non-nix installation scripts should work for end users as well. And I don’t use nix when working on ghc wasm backend fwiw.

There are some potential improvements about our toolchain distribution that I have in mind and will gradually implement in the future:

  • Set up self hosted github runner to build bindists of ghc wasm backend release branches, on platforms other than x86_64-linux.
  • Maintain a ghcup channel in ghc-wasm-meta directly, so you can use ghcup to install up-to-date builds of ghc wasm backend.
  • Is it possible to completely get rid of the curl | sh workflow in ghc-wasm-meta? Maybe we can just bundle all the tools like wasi-sdk, nodejs, etc into the ghc bindist itself.
5 Likes

I think the big reason people use Nix for stuff like this is because these builds require somewhat complicated configuration, have additional dependencies, and sometimes need creative tweaks (patching deps, scripts, external interpreter, etc) to work smoothly.

Cross-compilation to Windows is another case where you can’t just ghcup and it’s easier to just use haskell.nix because it captures all that is needed in code.

Nix is just the best way to distribute such a thing. There’s a lot of essential (not accidental) complexity when trying to build complicated software, so it can be nice to use a tool like Nix to wrangle it instead of just providing a README that tells you how to set up your computer. Eliminating “it works on my machine” is appealing to an open source project issue tracker too :grin:

But as the JS/wasm backend stabilizes, I’d imagine Just Using It via ghcup would become more viable.

2 Likes

This is all built upon your work on WASM backend! Thank you!

Wow, now my code becomes a test-case!? As I’m planning to use Humblr for a long term, this would help me tremendously. Thank you again!

That’s really great news! I was aware that your TH support got back-ported to GHC 9.12, but it would be great if I can use TH also with GHC 9.10! I’ll definitely try it!
As the source plugin makes HLS somewhat fragile, it would make situation better if we can use TH for conditionally generating FFI declaration! The availability of TH also help me unrolling worker endpoints along some Enum types to reduce the binary size safely!
In addition, if the availability of GHCi means that of Compiler Plugins, it would help me a lot! If this is the case, I can use my type-checker plugin to solve constraints type-level naturals. Also, I want to inline type families incorporating call to other typefams generated by Generic deriving. I also have developed such type-inlining GHC plugin, so if Compiler Plugins are available in WASM backend, I can use these!

Thank you for pointing it out. Indeed, I wrote my Dockerfile to use with Earthly based on the contents of Nix and non-nix installation scripts. It uses ghcup to install compiler under the hood, so if there is bindist for platforms other than x86_64 linux, GHCup + the installation script should also work.

Good to hear that! Just bindists for multiple platforms (for me, aarch64-darwin would be enough :slight_smile:) should help people tremendously! I’m looking forward to see that future. Thank you again!

1 Like

if the availability of GHCi means that of Compiler Plugins

To use GHC plugins for cross GHC, you need to build the plugin as an “external static plugin”. I’ve not verified if it works for wasm backend yet.

1 Like

Thanks! I will try to build plugins as static external ones, following the pointer you provided and GHC document.