UPDATE: Live Chat has been rebuilt from the ground up! Check out the new article.

Live Chat is a duro.me feature that lets you talk to me pretty much instantly. Before you read the rest of this article, go ahead, try it out.

Why? Because sometimes I want to ask a question, but I don't want to go through the whole process of drafting a good-looking email, logging into Twitter, finding my old Facebook login, etc... it's too much of a hassle. And that hassle is a lost opportunity! As a developer and a person, talking to anyone - literally anyone - can help you see things in a different way. And whether it's a spelling misake in a blog post or a critical bug in a product I've shipped, I want to know about it as soon as I can.

What can you talk about? Well, anything, for starters. But I'm particularly interested in what you're working on and what you're struggling with - maybe I could help.


This apparently ended up being really long. So, if you want, you can skip. No worries, I won't judge.


This is live. This is raw. And so is this post! I'm typing it as I develop the actual feature, so this should be interesting to look back on. I've never tried anything like this before.

4 AM - Obviously chat has been done before, a million times over, by pretty much every web dev at some point during their career. But this is different; namely, I have a budget of about five dollars, total, a total dev time limit of about a day, and you can only talk to one person: me. Let's get started.

4:45 AM - Starting with frontend. I've built the CSS/HTML for the header containing the online indicator, last online time, and email/Twitter links. I might end up using Vue or something to make the frontend dynamic.

5 AM - I've come to the realization that I'll also need an admin portal for this. Somewhat of an afterthought, but we go on!

5:30 AM - Redoing header a bit to make it look nicer on mobile. I've also got the chat body and footer in the right places.

6 AM - I've found Bubbly which makes is super easy to make chat bubbles. Thanks, leaverou!

6:13 AM - Taking a break for breakfast.

9:17 AM - Starting up again.

9:36 AM - I've figured out to and from message bubbles. Onto the footer.

10:23 AM - Refactored CSS so I could have different font sizes for things on mobile.

10:28 AM - Tested the site on my iPhone for the first time. Noticed a rendering bug with the sidebar, so dealing with that now.

10:32 AM - Thankfully was able to reproduce the issue on desktop Safari and found an easy fix. Safari was oddly respecting the 0 font size for sidebar elements when in mobile mode - the text is supposed to disappear. Well, it wasn't there, but its placement was still rendered, so the icons were pushed out of the center for seemingly no reason. I fixed this by wrapping the text in a span and just using display: none on it. Back to chat.

10:34 AM - The frontend styling is done, I think! Works well on mobile too. Next step: make it dynamic. I'm thinking of using React or Vue for this, and I'm leaning towards Vue because I can just drop it into the page without any custom tooling.

10:50 AM - Interesting predicament. Both Liquid (preprocessed by Jekyll) and Vue (processed by the user's browser) use {{ double brackets }} to specify expressions that should be rendered. If I put that in my HTML, Liquid will pick it up and fail to process it since it's meant for Vue and just leave the result empty. Thankfully, there's an easy fix: the delimiter property.

10:55 AM - I accidentally solved the problem in a better way. In an attempt to show you the {{ double brackets }} thing in the last update, I realized that Liquid ate that, too, and any attempts to escape the expression didn't work. Googling how to fix that so I could show you Liquid rendering expressions in a Liquid-rendered page led to this: Outputting Literal Curly Braces in Liquid Templates , and thus the {% raw %} tag (yes, I had to escape that too). Ultimately what this means is I can just put my whole page inside a raw tag and not have to worry about Liquid accidentally rendering it and breaking my Vue app.

11:20 AM - I wanted the ability to drop links in messages and have them get rendered as clickable links. I found a Vue library named vue-linkify, but was having trouble getting it working, until I found this guy's tiny solution.

11:24 AM - Finished the Vue app, minus the part where it talks to the backend. That's what I'm working on when I come back to this.


10:23 AM - Had to work on a separate project for a bit. Back to chat.

11:30 AM - Fighting against API Gateway to get a WebSocket API off the ground. I've managed to get it to execute a Lambda and return the result, so I should be able to implement the rest of what I need with this one function.


9 AM - I've made a DynamoDB table to hold conversations. I'm planning on using a UUID as the primary key, and an incrementing number for the sort key. The idea is that you can send a message containing a UUID and my Lambda function will append it to the bottom of the list. Then, when you need to load the conversation list at the beginning of the session, I can return the list of all messages with one Query call. I'm going to try playing with the AWS CLI to mock up the Dynamo actions I want, and then it should be pretty easy to translate those to Node parameters.

9:04 AM - After doing more research into how I could auto increment sort key, I learned that apparently Dynamo doesn't like auto incrementing numbers and that there are better solutions, like a UUID based off of timestamps. That's fine; after thinking it over for a bit I realized I can just use the timestamp itself as my sort key (since time is auto incrementing) and achieve the sa,e result. I don't even need to include a timestamp field now, since the sort key will function as that. I'll remake the table with UUID/timestamp keys instead (I wish there was a way to rename them without tearing down the whole table, but oh well, I didn't have anything in it anyways).

9:48 AM - Currently playing with API Gateway and Lambda to see if I can get connection IDs into Dynamo like the example. Because I want to fit everything into one function, I'll be doing all my work in the inline Lambda editor.

10:07 AM - Want a way to link a connectionID with a UUID in onConnect, but there's no way to specify a message before first connecting to the WebSocket.

11:22 AM - Bought some pizza dough and now back to this. Rather than dealing with connect/disconnect events, I've decided to just have everything get processed through message events. I can include the connectionID with every message stored, and then I'll always have the most recent connectionID available to use (for my admin backend). This means that connectionIDs in previous messages will become stale as soon as the connection is dead, resulting in a lot of useless values stored, but hey, storage is cheap.

11:35 AM - I figure people might want an option to get notified whenever I respond if they navigate away from the page. I looked into web push notifications, but iOS Safari doesn't support them, so that's a dead end. I'm signing my domain up for AWS SES (Simple Email Service) so I can send emails from my Lambda whenever I reply to someone who has their email configured and their WebSocket connection is closed. I guess I'll need to modify the frontend to support this too.

1:33PM - Now exploring AWS SES for the first time. Verified my domain and filed a support ticket to get my account out of sandbox mode so I can actually send emails to real people.

2:30 PM - Getting into deep specifics with Dynamo now, I've come to the conclusion that a frontend web GUI (think phpMyAdmin) to execute queries in would be a huge boon to development times. Oh well, maybe for another time.

2:53 PM - I've cracked the code and figured out how to store user emails and nicknames (what I'm calling metadata). This was a bit confusing at first because I wanted it all to go into one message with a timestamp of -1, but that message may not exist yet if this was the first time the user was changing their metadata. The query parameters I'm using now will allow that key to be automatically made and then have values inside it set if that's the case.

6:37 PM - Took some breaks in between, but I spent the better part of 3 hours painfully learning that single quotes are not valid JSON. This kept breaking JSON.parse(), leading to me trying to figure out WTF happened to Lambda that it suddenly couldn't parse incoming event bodies. Oh well, that's software dev.

6:40 PM - I've got message sending & email/nickname changing down. I'm going out to get sushi; when I come back I'll figure out retrieving message history and my admin portal (and also link the frontend to all of this somewhere between there). I really want to get this done today because it's blocking my progress on the rest of the site. Given that I've paved enough ground to figure out this much, the rest of it should be a breeze (famous last words).


11:40 AM - I've successfully gotten the first messages sent through the frontend. At some point yesterday I also realized I won't need a list-messages action because I can store them locally along with the UUID. If you clear local storage and lose the UUID, it makes sense to lose the conversation history as well.

12:45 PM - I found a bug (well, not really, but it's an aesthetic thing) where if you get too many messages they scroll off your screen. I fixed this already with overflow: scroll, but as it stands you have to manually scroll down when this happens. I need to automate that.

12:51 PM - From the client, you can now send and receive messages, change your nickname, and change your email. The client is effectively finished (barring one or two features), now I'm going to figure out how the admin portal works. The features remaining are the "last online" info bar at the top and testing if receiving messages works (I haven't yet because I have no admin backend to send messages from).


11:18 AM - Decided to build the admin portal under /andi. You can't interact with it, of course, but you can check out the source code. It's secured with a local, client side password.

2:08 PM - After coding for a fewi hours, I took some time to draw out what I wanted the backend to look like. I've also decided to scrap the andi specific page because there wasn't enough difference to justify two seaprate codebases, and just do everything on the one frontend file.

5:36 PM - I've finally gotten a reply to show up in the admin portal from a user session. A bunch of other stuff happened in between then; hastily trying to wrap up loose ends and get everything into a shippable state.

6:45 PM - Abandoning the timestamp: 0 metadata storage method. Opting to just send all of it with every message - storage is cheap, retrieval/processing is expensive.

7:47 PM - Finally locking down the backend. Only doing frontend work now, as I think I've got everything needed for both client side and admin side portals sans email sending functionality.

10:42 PM - Calling it a night. Admin portal needs work.


10:46 AM - Rewriting frontend and backend.

2:25 PM - Done.

A wild ride, right? Sometimes it ends up like this. What was supposed to take a few hours ended up taking 6 days. But hey, it got done in the end. And that is all that matters.


So yeah, I built my own chat portal using AWS. If there's interest I can also do a technical writeup on how everything works together to make Live Chat, since it was all built more or less from scratch. Let me know using the live chat! (see how handy this is now?)

Tagged #announcement.

Want more where that came from? Sign up to the newsletter!

Powered by Buttondown.