A live feed of random emojis using Cloudflare, WebSockets and Redux.
As one of Cadell's friends, I want a live feed of emojis so I can send and receive emojis for some mild amusement.
As Cadell, I want to build a realtime application using WebSockets and Cloudflare because they sound cool and I haven't used them before.
As Cadell, I want to build a stateful application using Cloudflare's Durable Objects so I can consider using it for future projects, like Very Nested.
Cloudflare is pretty hot at the moment. They've announced a serious competitor to AWS S3 with R2, Cloudflare Pages has gone full stack, allowing you to deploy fullstack applications with ease, and a new kind of data store called Durable Objects, which sounds really interesting.
I've got a soft spot for stateful applications. They allow people to interact with each other and that can be a lot of fun. 12 years ago in high school, I made a terrible chat app powered by a LAMP stack and deployed on some sort of free hosting. It wasn't the nicest app but it worked and my friends and some fun using it and I had fun making it. The free hosting then went on to get hacked, leaking my severely re-used password to the world. Oh well. Since then, it feels like it's getting harder to build stateful applications but that's probably because my idea of what a stateful application is has changed. For example, I'm not in a rush to get back on some dodgy host.
PRAwN Stack is my most recent stateful application and I deployed it on AWS. I'm really happy with the result but setting up the AWS deployment was a journey to say the least. I think services like Netlify have spoilt me by making the deploy process incredibly easy to the point where it's actually fun. I've used it for multiple frontend applications and a few backend functions but nothing stateful because they don't have have their own data store. Instead, they recommend using another provider for data storage, like AWS, which largely defeats the purpose of using Netlify in my opinion. I can use PRAwN Stack for stateful applications going forward but what other options are there? Firebase springs to mind but it comes with significant trade-offs. I don't really know of any others. Vercel and Netlify have raised a lot of money recently, maybe they'll move into this space?
Cloudflare Pages going fullstack means Cloudflare already is. This is particularly exciting because, as discussed earlier, Cloudflare is pretty hot in general at the moment. They already have a powerful CDN for static assets, Workers for backend functions and Durable Objects for a data store. If they can tie that all together with a developer experience that's half as nice as Netlify's then I think that's going to be massive. All board the hype train!
It doesn't stop there because Cloudflare also supports WebSockets, allowing us to build realtime applications. The only thing better than a stateful application is a realtime stateful application, just look at Google Docs and Notion. We can use WebSockets, Workers and Durable Objects together to create stateful applications that updates live, in realtime. Glenn Maddern wrote Durable Objects in production which goes through their experience with this stack and it was a big inspiration for this project and write-up.
Diving in deeper, realtime applications are a thing all on their own. There's a range of solutions out there, each with their own trade-offs. CRDTs spring to mind and they're pretty hot at the moment too. I just read about SyncedStore, which uses Yjs, then there's Automerge, which might work better with Redux, and here's a fun article on the performance and optimisations of CRDT solutions. It goes deep. Figma found CRDTs were overkill for their use case because they have a central data store to act as a source of truth. They decided CRDTs were better suited for peer to peer communication and used them as inspiration instead.
I decided to do the same. I also have a central data store to act as the source of truth and I'm already familiar with Redux which seems perfect for realtime applications. In Redux, users trigger Actions which are then handled by Reducers to update a central data store. The data store is local but Actions already contain all the information required to update the data store so can't we just send them around to other data stores and build a realtime application? I found Logux, @localfirst/state and some others that seem to follow this idea but I couldn't see how they would work with Cloudflare so I decided to roll my own.
After all that background reading, the solution is quite simple. The frontend is a NextJS app with Redux, the API is a Cloudflare Worker and the data store is a Cloudflare Durable Object. The frontend creates a WebSocket connection with the Worker which then acts as a proxy for the Durable Object. When the connection is created, the Durable Object will send down the current feed of emojis and remembers the connection for syncing updates later on.
One interesting thing to note here is the WebSocket connection is stored in the Durable Object's memory rather than it's persistent storage. We can do this because the Durable Object is alive (and billed) for the entire duration of a WebSocket connection. More on this later.
Clicking the Add Emoji button creates a new Redux Action which a Reducer handles to update the local data store. The page is then updated to display the new emoji using React. Nothing groundbreaking here but here's the fun part. The Action is also sent through the WebSocket connection to the Durable Object which broadcasts it to other users, through their WebSocket connection. It also stores the emoji in it's persistent storage for users that join later on. Actions from the WebSocket connection are handled by Redux the same way local actions are - a Reducer updates the local store and the page is updated to display the new emoji.
This is all deployed using Cloudflare Pages which gives us a pipeline that builds and deploys our code when we push a new commit, similar to Netlify. I later learned Durable Objects need to be deployed manually but more on that later.
That's it! That's how I built a fun, realtime stateful application with Cloudflare.
Overall, this was a fun project. There were certainly rough parts but I'm really happy with the end result now that it's all working. Sharing Redux actions over WebSockets works well and the Durable Object code looks great.
Glen Maddern describes Durable Objects more as Stateful Workers in Durable Objects in production and I have to agree, Durable Objects are like Workers with persistence methods. But it gets even weirder with Websockets because we can actually store state on the Durable Object itself without using it's persistent storage. This is possible because the Durable Object is alive (and billed) for the entire duration of the WebSocket connection. This state is perfect for storing the active WebSocket connections and using them for broadcasting updates later on which is why the bulk of the logic is in the Durable Object instead of the Worker. The docs don't really highlight this functionality, maybe because it's only relevant when using WebSockets and Durable Objects together.
On a bit of a tangent, I also built an NPM package for my frontend components as part of this project. I've built NextJS apps with MDX and custom components before and wanted to split them out into a reusable package instead of copying and pasting them. The result is Cadell's Vanilla Components and Cadell's NextJS template. It was a bit harder than I thought it would be and wasn't strictly required for this project but I'm really happy with the result and it should make my next app much easier to build. I tried using tsdx for building the package but I couldn't get the packaged typescript types working couldn't work out why. Thankfully, I found tsup which pretty much worked out of the box. It uses esbuild instead of Rollup so that's cool too. tsdx introduced me to np for actually publishing to NPM and it works really well so I'm still using it. Sometimes everything happens for a reason.
My biggest challenge in this project was Cloudflare Pages. As discussed earlier, I chose Pages because I wanted to deploy full stack apps including a data store and have a nice developer experience. They just announced Cloudflare Pages Goes Full Stack and I was keen to ride the hype train.
Unfortunately, new products and hype trains are often a bit rough around the edges and that was certainly my experience here. I'm really happy with the result but it was a bit of journey to get here.
My biggest challenges with Pages:
None are deal breakers for me but they did subtract from the developer experience. Thankfully, most of them should be easy to fix.
There's no logs for the backend Functions deployed with Pages, or at least I couldn't find them or any documentation about them. This made it difficult to work out what was going wrong and so I had to write code to capture each error and send it back to the client where I could see it. This made for long feedback cycles because builds are slow even when you're only deploying code to try and find what's going wrong. Functions are based on Workers and they have logs so I suspect it's on the the works.
Using Durable Objects with Pages is poorly documented. This is really frustrating because using the two together was a big driver for using Pages and building this project.
The TL;DR is Durable Objects are deployed separately and manually and I've documented the process here.
Here's how I got there...
./durable_objects/downloadCounter.js. This looks fantastic because it suggests Durable Objects are deployed from a
durable_objects folder just like Functions are deployed from a
functions folder. Unfortunately, that's not the case.
durable_object folder doesn't work either. That's when I suspected it had to be deployed manually.
pages durable objects takes you to a page that sounds relevant but it's actually for Workers, not Pages. These are the Pages Docs for Durable Objects and they don't reveal much but it does confirm that we need to deploy our Durable Object manually.
Let's do that by jumping back to the README in the code example.
npm install; CF_ACCOUNT_ID="<YOUR CLOUDFLARE ACCOUNT ID>" npm run publish;
That doesn't work.
I've documented the actual process in my Durable Object package but it's basically:
So deploys are manual and it was a bit of a journey to work it out but the good part is deploys are really fast. It's closer to seconds than minutes and faster than some local environments I've used. I probably wouldn't even need a local environment when it's this fast. This was a welcome relief after slow Pages builds. It makes me think maybe we should use Wrangler instead of Pages but more on that later.
The first time you build a stock NextJS app in Cloudflare Pages it will fail. This is a small issue and it's easy to fix but it really hurts the developer experience, particularly when the same thing works in Netlify. It fails because the default Node version in Pages is too old for NextJS 12 and the solution I went with was to include a .nvmrc in the project to specify a Node version that works with NextJS 12. I think setting up a
.nvmrc file or something similar to specify a project's Node version is best practice anyway but new developers won't know this so it's going to be frustrating for them.
This isn't isolated to Cloudflare Pages but each build takes a few minutes, creating long feedback cycles. To make things worse, there seems to be a bug with the builds page where clicking a build doesn't actually take you to that build. It seems to have something to do with in-progress builds but I just ended up refreshing the builds page each time to get around it. This was particularly frustrating when most of your builds are just adding some error handling to try and work out what's going wrong because there's no logs for Functions, as discussed above.
I eventually became content with slow builds and added it to my "modern development is funny sometimes" list. Then I deployed my Durable Object with Wrangler and saw how fast that was and all my contentment was erased. "What if we could have cloud deploys AND fast feedback cycles!?". More on this later but if you like this idea then you might like this article I came across about a A magical AWS serverless developer experience which uses Serverless Stack (SST). Pages have since announced fast builds so that sounds promising.
The only other thing that's barely worth mentioning is Pages had some downtime where all my builds failed one day. Downtime happens, particularly in betas, but I don't get a lot of time for side projects like this and it was annoying to be blocked after building up the motivation to work on the project one day. Maybe local development is important after all? I tend to think so but apparently others aren't so sure.
The Functions API is slightly different to the Workers API meaning you have to unravel all the Workers examples you come across. Now that I'm finished, the differences aren't massive but it definitely adds some overhead when you're getting started. This is particularly noticeable when working with WebSockets because there's not a lot of documentation so you have to read through more code examples and adapt them. This is further amplified by the lack of Function logs and long feedback cycles discussed above. I also found Googling the Cloudflare Pages API sometimes takes you to Workers documentation which isn't relevant or helpful, adding further confusion.
You could make the case that it would be better to use Wrangler over Pages because:
On the other hand:
This was an awesome project. I'm really happy with the app, code and Cloudflare, particularly after the working out all the challenges. On the other hand, if I waited a few months, Cloudflare might polish things up and make it less challenging but then I would miss the hype train! Regardless, I hope Cloudflare can improve the Pages experience based on my feedback and create an awesome product in an exciting space. It probably took longer to write about my experience than it did to actually build the app but I'm working on my writing so that's okay. Feedback welcome!