For two years in a row I’ve taken some time off at the end of the year to rest and relax and, most importantly, to work on personal projects. I’ve begun calling it my annual holiday sprint. This past year I ended up building two apps. One is a writing app built on omg.lol. It deserves its own blog post, but that’ll be another time.

The other thing I built was a home-cooked collaboration with my brother, Matt. On a whim, we decided to build our own real-time chat application—a Discord knockoff we call BrainWave Chat.

The name, BrainWave Chat, isn’t going to appeal or make sense to most of you. And it shouldn’t, because it’s based on an inside reference. That in itself should give you some insight about the application. The name as well as the functionality of this app is specific to me and Matt. We created the app just for us; our own dedicated chat tool.

If you just want the TL;DR, jump to the list of features.

Screenshot of BrainWave Chat. Several test messages are shown along with emoji reactions. One message is a meme gif that says "nerd alert." The last message is the shrug emoticon.
The BrainWave Chat user interface in dark mode

Why build yet another chat app in 2024?

You may be thinking, what’s the point in building a chat app when so many good options already exist? We certainly thought this at the project’s outset, though admittedly not for very long. It’s true, though. There’s SMS, iMessage, Facebook Messages, Snapchat, WeChat, Discord, WhatsApp, Telegram, Signal, and Slack—and that’s just the stuff I thought of off the top of my head without any research. I’m sure ChatGPT could vomit out a list of a hundred more options.

I don’t know if these are good reasons, but here are the factors that pushed us to go ahead with the project.

Privacy and the small web

Matt and I bounced around a few apps, but the latest one was Discord. We love its UI. But we don’t love that it doesn’t offer much privacy. I recently saw this video that levels some pretty big accusations against Discord. Specifically, the idea that the Chinese government has access to our data by way of Tencent, the China-based company that invests in Discord.

I didn’t do much to fact-check that accusation but it’s not like believing that big companies are harvesting data and sharing it with governments is a tin-foil-hat-level idea.

We were inspired by home-cooked apps and small web ideas. It’s fine to make your own thing just for you. And you can absolutely build software without pulling in a ton of dependencies and wiring up complex infrastructure. We’re so used to reading about the software practices of large companies that it’s easy to forget that a lot of (most?) software projects don’t need to be built at that scale.

Accessibility

What can a website do? Turns out, a lot. In my MagnoliaJS talk about home-cooked apps I discussed several websites that I made just for me in order to make something that was more accessible and usable for me. This was another motivation for us. As much as we like the Discord UI, it has some browser inconsistencies and other issues that don’t work well for how we use our computers. More on that later.

Features

When you build something for yourself it means that you can make it do whatever you want. There were some specific features we wanted that Discord didn’t have. Also more on this later.

Practice and fun

Lastly, we had time off and we wanted to build something together. We had some tools we wanted to use and an architecture we wanted to push to the limit. Sometimes you do something just to do it.

What makes a good chat app for us

I’ve already said a couple of times that we like a lot of things about Discord.

  • Nice-looking. I really don’t care for chat bubbles and fortunately there are none to be found in Discord.
  • Doesn’t presume you have a phone. It’s hard for me to use phones because of my disability. I’m a desktop native and anything that forces me to use my phone for initial setup is annoying to me.
  • Discord really nails the fun factor. It has lots of goodies and delightful interactions.
  • Speaking of which, reactions are fun. Especially when you go beyond standard emoji and use the custom or animated ones. They’re a great way to micro dose memes.
  • GIFs, or as they are correctly called, GIFs, are a must-have feature of a chat app at this point. Though I prefer that they be collapsible.
  • Toggle-able desktop notifications for new messages. No-brainer.
  • Easy to share images and video. I share a lot of screenshots. I need this feature or it’s a dealbreaker.
  • I use different computers in different lighting environments so being able to switch between dark and light modes as needed is a must. Discord also has multiple themes now which I think is kinda fun.
  • Video calls. This feature always works pretty well for us and we’ll probably stick with Discord for video chat for the time being.

But there are things we don’t like about Discord that we knew we could address with our own bespoke app.

  • Privacy. This is a big one. We can host our app on an affordable VPS1 using boring old PHP and MySQL. Now we control our data. Boom.
  • The desktop version of Discord really wants to be used in its downloadable desktop app form. It will run in the browser, but can be finicky compared to the desktop app. But I don’t love having another always-on Electron app2 running as it hogs a fair amount of resources.
  • I mentioned browser inconsistencies. What I found was that it works best in Chromium-based browsers. I ran into more glitchiness when using it in Firefox. I haven’t tried Safari much so I can’t comment on that.
  • Scrolling. This is a big one. Matt and I have to scroll by clicking and dragging scroll bars. Discord shrinks its scrollbars down and they’re hard for us to use. We counteract this on most websites by using a browser extension called ScrollAnywhere. Unfortunately, it struggles with Discord’s infinite scrolling—a UI pattern I despise.

We also like the idea of adding weirdly specific features in the future. We haven’t yet added too many of these, but I can brainstorm a few right now:

  • We type mostly using onscreen keyboards because we’re mouse-centric. We could make our app more mouse-friendly by adding any number of shortcut buttons: canned messages, go-to reactions or GIFs, a Morse code sender!
  • Some kind of advanced text-prediction that saves us some keystrokes (err, mouse clicks).

Building the app

Okay now we’re to the fun part! Matt is a PHP curmudgeon so he was tasked with building the backend of the application whilst I would build out the frontend simultaneously.

PHP Islands

We decided to build an API out of plain PHP. But not even a reasonable one that directs all requests to a single entry point and employs a routing mechanism—you know, like every single backend framework ever. No. Every endpoint is just a PHP file. You want to create a message? “Right this way,” I say as I direct you to /api/create-message.php. It imports a handful of shared functions from elsewhere and does one thing, you can probably guess what that is.

We started calling it PHP Islands Architecture because we had just read Chris Ferdinandi’s article of the same name. But this is not that. Chris described it thusly:

The tl;dr: the site is nearly entirely static, pre-rendered HTML. Rather than using JavaScript to render the interactive and dynamic bits, I use islands of server-powered PHP.

What we were doing was smashing tons of loosely related islands together to form a monstrous archipelago. And our app’s frontend; well it is technically pre-rendered HTML, but it makes heavy use of JavaScript to hide and show things. So I’m not sure why we hijacked that name or why I’ve prattled on about it for this long. Sorry.

Polling versus websockets

Pretty much every article about writing a real-time chat app will tell you that you need websockets. They are all correct. Polling is a terrible way to keep a chat client up-to-date.

We chose polling. Why? Because we didn’t immediately know how to latch websockets onto PHP, we didn’t have much time to learn, and we had already decided we didn’t want to use NodeJS (the only other server-side language either of us are any good at). And anyway it’s built for two users. How bad could it be?

Real bad. I probably spent more time fixing the polling function than anything else. It would have been nice to know if a message was sent or a previous one was updated (or reacted to, etc) instead of continually asking the server, “What’s new home skillet?” and having to sort through the resulting pile of shit.

But we got there and it’s working pretty well now. I’m guessing this could scale up to about a dozen people but I feel like it might get weird beyond that. That’s my un-researched wild-ass guess.

Ain’t nobody got time for [a build step]

We were in a hurry because we had about one week of free time to get this thing usable before it was time to go back to our day jobs. We weren’t going to have a ton of time to maintain this thing and we wanted it to be able to run by itself for a long time without intervention. That meant using as few dependencies as possible. We didn’t even bother with Composer.

On the front-end, we skipped having a bundler. The handful of libraries and utils I used—I just downloaded their minified versions from a CDN and checked them right into git. Dusts hands. The only thing I used npm for was to install prettier, the code formatter.

Single page app with Alpine.js

The frontend is found at /chat.php (which I keep accidentally pronouncing as ChatGPT 😩). We use PHP to generate most of the HTML then we drop Alpine on top of it to provide all the interactivity.

Alpine deserves its own article, and I plan to write one at some point. It’s refreshing in many ways but also kind of odd—it encourages writing JavaScript expressions, or sometimes whole code blocks, directly inside of HTML attributes. It’s similar to the Tailwind philosophy. And I love hating on Tailwind so it’s hard to justify why I enjoy Alpine.

Alpine can quickly add functionality anywhere which means I often end up with random JS code everywhere. But it gets you moving quickly and doesn’t require an annoying build step. So it was the perfect tool for this project.

Iteration and dogfooding

We did little planning and a lot of iteration. The beginning was the hardest because the frontend and backend couldn’t work without each other and we were building them simultaneously. But as soon as we reached critical mass of functionality, our duties split up nicely. Eventually both of us worked on both sides, which was a nice change of pace.

We forced ourselves to use the app as soon as we could feasibly do it. That gave us immediate feedback and pointed us, quite painfully at times, toward what we should tackle next. It was a whirlwind but it was a ton of fun.

The long tail of fixes

After our vacation was over, we had a functioning app. I’m proud of that accomplishment for sure. But we also had a huge list of stuff left over that we still wanted to do. Fortunately, it is much easier to accomplish a quick win on a functioning system than to build the system in the first place. I have found this to be true with most of my side projects. Build the MVP in a mad dash, then slowly improve it over time.

Features

Here is a snapshot of the app’s features as of press time.

  • Real-time text chat with markdown support
  • Light and dark mode
  • Emoji support (including custom/animated) via shortcode and picker
  • GIF support via Tenor
  • Message editing
  • Syntax highlighted code blocks
  • Image and video embedded attachments (stored on Backblaze B2)
  • Downloadable file attachments protected behind login
  • Reactions (standard emoji and custom)
  • Inline (Discord-style) replies
  • Open graph previews
  • Message notifications/sound
  • Unread message favicon status
  • Active user indicator
  • User-is-typing indicator
  • Pinned messages
  • Message search
  • The OG Discord font, Whitney
  • Can render ¯\_(ツ)_/¯ properly

Roadmap

At this point, we have shipped most of the features that we really wanted. Most of the leftover stuff are small UI tweaks. The biggest thing left is giving each message a permalink. I would love to be able to link to specific messages.

We’ve already started using pinned messages as a way to have shared “notes.” I wouldn’t be surprised if the next phase is to bolt on another application that lets us have collaborative notes.

We designed the app for us, but it does technically support n users and we coded things in such a way that they could potentially be open sourced. Maybe we’ll do that at some point. If anyone is actually interested in this, shoot me an email.

Lessons learned

This project went mostly better than I thought it would. Most of my lessons learned were good things—like confirming that a project with few dependencies and no build step could succeed. I learned that Matt and I can work together quickly and efficiently and to make a lot of progress in a short amount of time. And have fun doing it!

My biggest lesson learned in terms of what not to do is not to use polling for a realtime application. I mean it is working fine for this use case. But it is painfully clear that websockets are a much better fit for something like this. But even then, I am better at polling now. The next time I need to do it, hopefully I can avoid some of the pitfalls.

Sometimes I do stop and wonder whether these kinds of endeavors are worth it. Shouldn’t I have been working on something more important? Like a SaaS or something that can make some money. Or an open source project that would benefit society. Or perhaps something that doesn’t involve computers at all.

I don’t have a good answer. All I know is that it sure was fun and I love using it.


If you would like to comment on this article, send me an email.


  1. We opted for the managed VPS, Cloudways (referral link). It gives us the old and reliable LAMP stack we’re using without needing to do much server maintenance. 
  2. I say this as a person who sells an Electron app for possibly the most overkill use case possible—rolling dice. All I can say is I’m sorry and I will try to do better next time.