March 24, 2024

On the last day of 2023 we hopped on a plane to London. We've been here two and a half months now. Michelle and I lived here in 2009 and we've come back again with kids. I guess you could call this a kind of digital nomadism, except we're doing it the wrong way around moving to a country with a higher cost of living. We've already had some wonderful experiences exploring the UK and Italy, and we're looking forward to more! I feel very lucky to be able to do this with my family.

50c84a0ccb1cd1a42402903e2e291632.png

2023 was a good year for me in software development. Here's a recap of things I shipped. Most of these things are built with ClojureScript.

Open Source

I started the year building a new open source Pocket Operator sync app called PO Sync (source code on GitHub). It's a ClojureScript web app which I ported to iPhone and Android native using Cordova (Capacitor is what I'd use today). I did this to get warmed up for what I thought was going to be a year of shipping small music apps. More on that later.

In March I open sourced Roguelike Browser Boilerplate (source on GitHub). Sales of the boilerplate fell after I did this but I don't mind. It feels better to have this available as an open source project.

At the start of February I open sourced TweetFeast (source code here) in response to the catastrophic failure of that micro-startup. I thought releasing the source might be a way to salvage something from the experience. Hilariously TweetFeast actually went on to generate some revenue after it "failed" which was a big learning experience for me. More on that below.

At the end of February my open source web game Rogule went viral and started to get thousands of players per day. That kicked off after I listed it on /r/webgames and then it was picked up by Hacker News, re-tweeted by the GitHub twitter account, and got some press in Japan. I wasn't really sure what to do about going viral so I just watched and made a few small updates to fix things throughout the year.

What's interesting is Rogule had sat dormant for one full year with only a few players per day. There was no change to the software when it went viral. I learned a big lesson from this which is sometimes you don't need to add more code or features to make something work, you just have to get it in front of the right people. In other words, marketing. I've got some big plans for Rogule but haven't been able to give it much attention lately.

In the middle of 2023 I shipped livereload.net which is a fully client side web development live reloading utility. This is similar to Slingcode but more stripped back. Neither of these utilities get very much use but I learned a lot and honed my craft so all good.

In August I released a little utility called aish which is an AI shell command helper. I use this quite a lot on the command line. It has saved me many hours of looking things up when doing sysadmin type of things. Building this simple bash script was one of my first forays into ChatGPT's API. Interfacing with a proprietary 3rd party API is not my favourite thing and I'm looking forward to a future where it's easy to install and run local open source GPTs on our laptops and servers.

At the very end of the year I re-released SVG Flipbook. It's an online app for doing Inkscape animation. A couple of years ago I tried to commercialize this but it didn't work out. I decided to remove the website and just point people directly to the open source version of the app.

Throughout the year I put out several releases of Sitefox, a backend web framework for ClojureScript in the pattern of Django, Rails, etc. I really didn't want to build Sitefox but I had to. I'm all in on full-stack ClojureScript now and I needed something to replace Django. Biting the bullet and building it in the end turned out to be a good decision as just about everything I built this year was built on top of Sitefox. Even if nobody else uses this project it has saved me so much time as to be more than worth it.

Finally this year I open sourced a couple of libraries I am using to build apps for dopeloop.ai. The first is a small collection of music making UI elements for ClojureScript and the second is a collection of webaudio functions for ClojureScript. Both of these are fairly specific to the work I am doing on Dopeloop but I wanted to have them out there as a reference for anybody else building music stuff with cljs.

Micro-SaaS bootstrapping

In 2023 I contined my adventures in bootstrapping small online business projects. I learned a ton and managed to double the revenue from these projects over the previous year.

revenue-2023-annual.svg

The projects averaged $700 MRR overall and I managed to do my first $1k AUD month at the end of 2023. This trend has continued into 2024 and I've managed to do $1k MRR in January and February. I am not sure if I'll be able to double the annual revenue again in 2024 but that's my goal.

revenue-2023.svg

With the $1k MRR milestone it feels like I've unlocked something fundamental about how online businesses work. I think I've got a grasp now on the full suite of skills you need as a solo developer to ideate, build, launch, market, and sell software online. NOw bUy mY eBoOk!!! Just kidding.

Anyway, here is some detail about where the revenue came from in 2023.

Hosted Gitea

c1e49f993cd0f10d0bfc9b99c1ab35b3.png

Hosted Gitea is a hosting service for Gitea, an open source git code sharing web app. Hosted Gitea is an alternative to GitHub for people who want to get away from big corporation run code hosting. In 2023 I made 172 commits which included many improvements to security, stability and user experience. Probably the biggest user-facing change was introducing tiers for larger machines with more disk space etc.

The MRR grew by 200% during the course of the year. This basically comes down to search traffic. I don't do much marketing for this site, mostly just making improvements to the service.

Sfxr Pro

a46e78c5cfbaed150e37cd64e4cbc585.png

Jsfxr Pro is an online app for generating retro sound effects. It also grew 200% last year and continues to grow this year too. I am thinking about adding this app to the dopeloop.ai suite of apps. The way it would work is everybody with a Sfxr Pro subscription or a Melody Generator subscription would get full use of the other apps as well. It's a bit of work to make this happen but hopefully I'll get to it later in the year.

TweetFeast

e82c70380ed75bad76cc6321b2119ac8.png

TweetFeast was a micro-SaaS for downloading tweets and follower data.

I learned a really valuable lesson with TweetFeast. A few months after shutting down the app I started thinking about it again. There had been changes at Twitter. I saw that there was still traffic coming to the site from the "download followers" search term, which meant people still wanted to export their follower lists. So I spent a day ripping out all of the other functionality except the follower downloads, and applied for a new API key.

Hilariously, people started to pay for it and months later after "failing" it became a profitable app, reaching $100 MRR for a couple of months. All up it made $600 USD before further changes to Twitter APIs caused it to break again. This was a big surprise to me. I learned there may still be latent potential in time invested in failed business ideas. There's always the possibility I missed something the first time around so I shouldn't give up too easily.

Later I tinkered with an AI tweet generator but that never really took off. I'll probably sit on this domain name for a couple of years until I think of something else to do with it.

Transcript generator

f4463156b6051937d20ea863c963720e.png

I thought of the idea for Transcript Generator a while back when I first started tinkering with these new GPTs. There is a lot of hype around AI, and LLMs have many issues, but one of the things they are good at is summarizing and editing existing existing text. I record YouTube videos periodically and I thought it would be interesting to generate articles from those videos.

I sat on this idea for a while and then I realized it could be something useful to other people bootstrapping online businesses. Creating content to rank well in search engines is a difficult part of bootstrapping so this could be helpful. After I started looking at the tech I realized it would not be hard to bang this together and ship something. So I did that and shipped the first version at the start of September. Then I left it alone for a while to see what would happen.

The visitor numbers went in the right direction and so near the end of the year I decided to spend "just a week" shipping a paid subscription version. The transcript download features would be free but users would pay for the AI features like article generation.

Of course it took me longer than a week (programmer estimate!) but I finally shipped the paid version in early 2024. You'll have to wait to find out what happened but basically there are now customers paying for this utility.

Dopeloop (Melody Generator)

d701e63896acc140d4c26a8f97d92c7a.png

The smartest thing I did all year was shipping a Pro version of Melody Generator.

At the end of May I was making updates to the audio engine and general improvements. I had been vaguely thinking about a paid version of Melody Generator but something was holding me back. At the end of 2022 what had looked like the smartest idea was building music apps for Android and iOS and I thought that was the right way to go.

What finally pushed me over the line was this thread from Danny Postma where he talks about how much traffic you need to make a sustainable online business. He was talking about search results per day in the hundreds, but the term "melody generator" had thousands, and I was already capturing a good proportion of that traffic. What's more my numbers were going up.

So I abandoned my idea of working on on native music apps and started working on a Pro subscription version of Melody Generator. I shipped it on July 27th and immediately started seeing sales. It's not huge yet by any means but I can see a clear path forward to good healthy numbers with Melody Generator and the dopeloop.ai suite of online music apps.

What is wonderful about this is I found a way to avoid building native apps. I've always been uncomfortable with shipping software for the walled garden ecosystem of app stores. I don't think it's good for developers or for users. The open web is much better. The problem is that it's easier to reach paying users through an app store. Now that I've got the web app revenue above what I was making in the app stores though, it also makes economic sense to avoid them. Much better to ship pure web apps that people can use without installing anything.

Later in 2023 I also worked on a new style guide for Dopeloop apps. I also got a new app called Beat Generator pretty far along. I'm not yet ready to launch that one yet so keep it under your hat. I'm excited to finish it and ship in 2024!

Tinkering

Aside from these open source and commercial projects I also tinkered on a whole bunch of pie in the sky tech. Algorave stuff generating impulse tracker modules, Bluetooth LE sync for music apps, PNG metadata hyjinx, generating game assets with AI, a tiny browser game engine, a joplin blogging plugin, and a variety of ClojureScript experiments.

Hopefully this year I'll find some time to write up the results of those experiements where they could be more widely useful.

Plans for 2024

Now that we're a bit more settled in London I've been thinking about the year ahead. Work has continued apace on my existing projects and client work of course. My goal is really to not start anything big and new but to drill down into growing these projects. I want to focus on growing dopeloop.ai and the "online sound making apps" side of things as a whole.

I also have a weird idea about a way to ship open source web apps with a one-time license for pro features. Shipping web based software means people don't need to install any app, either client or server, and making it open source means they can avoid various bad failure modes (such as companies shutting down or selling user data). Making money doing this means it becomes a sustainable activity that it's easy to find time and energy for.

Some people are surprised to discover that even the Free Software Foundation is in favour of selling to make projects sustainable.

Many people believe that the spirit of the GNU Project is that you should not charge money for distributing copies of software, or that you should charge as little as possible—just enough to cover the cost. This is a misunderstanding.

Actually, we encourage people who redistribute free software to charge as much as they wish or can.

For me that is a kind of holy grail. To build free and open source software and to make money doing so. I think the world would be a better place if more people were able to do this and it's something I continue to aspire towards.

Anyway, for the short term I am plan to continue focusing on what is working. Thanks for reading, and have a great 2024!

Oct. 10, 2023

tl;dr: have some CSS animations to make your browser games juicy!

particles.gif

My favourite game engine is the browser. You get so many batteries included when you use the browser as your runtime. Sprites, animation, sound, mouse, keyboard, touch, gamepad, fonts, text handling, localization, concurrency, networking, 2d, 3d, and a weird XML based scene-graph called "The DOM". The list goes on.

I'm not even talking about canvas based games. These days when I build games like Rogule, Asterogue, and Smallest Quest, I sometimes use <canvas> but I always use the DOM.

One cool thing I've discovered about using the DOM as your 2d game's scene graph is you can offload a lot of CPU intensive effects to a high performance declarative graphics language called CSS. This frees you up to write far less code, and do the more interesting stuff with game logic in your procedural code.

Smallest Quest

It was Juice It or Lose It that inspired me to put more juice into my games. The talk makes the case that "game feel" is a major part of what makes games fun. Game feel is a mixture of animation and sound in response to interactivity and in-game events. It's the difference between a flat game and one that pops.

When I started building Asterogue (a solo-developed space based sci-fi roguelike) I got to wondering - is CSS good enough for video games? I was using web tech to build the game already. Could I do all of the visual effects using only CSS animations?

Asterogue game play

I put together this collection of juicy CSS game-feel animations as a test and I've been building on them ever since. Feel free to use them in your own browser games!

https://chr15m.github.io/juice-it/

Before I made Asterogue I would have used JavaScript to manually script sprite animations. Manually coding animations takes a lot of time and effort. It's also not very performant. That means the game ends up less juicy than it should be. Why not speed the process up using the domain specific animation language built right into browser?

boing.gif

It turns out CSS is absolutely good enough for a large class of browser based 2d games.

In the end I was able to build and ship a juicy graphical roguelike in about 1.5 months. I used Electron and Cordova to build the Asterogue binaries for desktop and mobile. I used plain old CSS animations for game feel. I saved on code using pure JavaScript with just one library (rot.js). Asterogue is only 2k lines of code which helped a lot with debugging and development speed. Browser based debugging tools are also absolutely fantastic during game development.

I would highly recommend this path to anybody making 2d games. The browser is a killer game engine.

screenshake.gif

Have fun!

Sept. 10, 2023

Today I put together a small test repo to check how much space is saved when replacing React with Preact in a ClojureScript project.

I used npm init shadowfront prtest to get a basic project up and running. This creates a simple one page Reagent app with a button you can click.

(ns prtest.core
  (:require
    [reagent.core :as r]
    [reagent.dom :as rdom]))

(defonce state (r/atom {}))

(defn component-main [_state]
  [:div
   [:h1 "prtest"]
   [:p "Welcome to the app!"]
   [:button {:on-click #(js/alert "Hello world!")}
            "click me"]])

(defn start {:dev/after-load true} []
  (rdom/render [component-main state]
               (js/document.getElementById "app")))

(defn init []
  (start))

I made a build to check the size of the resulting js binary. Then I uninstalled react and react-dom and installed preact@8 and preact-compat. Then I updated shadow-cljs.edn to add the following clause into the :app build:

:js-options {:resolve {"react" {:target :npm :require "preact-compat"}
                       "react-dom" {:target :npm :require "preact-compat"}}

This asks shadow-cljs to alias those React modules to the Preact compatibility layer throughout the whole stack.

I ran make to build the project before and after the change and got the following results:

  1. With React = 292k
    $ du -hs build/js/main.js 
    292K    build/js/main.js
  1. With Preact = 172k
    $ du -hs build/js/main.js 
    172K    build/js/main.js

A 41% size reduction (120k) for a simple one page app seems pretty good. Most of the remaining 172k would be ClojureScript core and libraries such as Reagent.

Update: shadow-cljs lets us generate a build report. Here's a build report with React and then Preact:

React ClojureScript build report

Package Weight %
react-dom @ npm: 18.2.0 128.22 KB 45.8 %
org.clojure/clojurescript @ mvn: 1.11.60 115.71 KB 41.4 %
reagent @ mvn: 1.1.0 22.93 KB 8.2 %
react @ npm: 18.2.0 6.49 KB 2.3 %
scheduler @ npm: 0.23.0 3.96 KB 1.4 %
org.clojure/google-closure-library @ mvn: 0.0-20230227-c7c0a541 1.12 KB 0.4 %
Generated Files 932 0.3 %
src 490 0.2 %

Preact ClojureScript build report

Package Weight %
org.clojure/clojurescript @ mvn: 1.11.60 115.61 KB 71.4 %
reagent @ mvn: 1.1.0 22.94 KB 14.2 %
preact-compat @ npm: 3.19.0 9.24 KB 5.7 %
preact @ npm: 8.5.3 8.18 KB 5.1 %
preact-context @ npm: 1.1.4 2.55 KB 1.6 %
org.clojure/google-closure-library @ mvn: 0.0-20230227-c7c0a541 1.12 KB 0.7 %
Generated Files 931 0.6 %
prop-types @ npm: 15.8.1 801 0.5 %
src 490 0.3 %

June 23, 2023

Hello! Today I am very excited to announce a thing I've been tinkering with for the past month or so. You can find it at https://livereload.net.

It's a simple online utility that enables live reloading web development for your local HTML/JS/CSS projects. It's easy to use and you don't have to install anything. Just drag your web project folder onto the window and your index page will show up. When you edit the files on your local machine they will live-reload in the browser and you'll see your changes immediately.

That is basically all there is to it. I've found live reloading to be so useful in my own development and I wanted to make it easy for anybody to get this feature without complicated command line build tooling. I discovered a browser filesystem feature that allows polling for file changes and realized I could use it for this, and so I did.

So there you have it. If you have any feedback do let me know. Enjoy!

screenshot.png

March 26, 2023

tl;dr: you can generate very small (less than 1k) JS artifacts from ClojureScript with some tradeoffs. I worked out a list of rules to follow and made the cljs-ultralight library to help with this.

Photograph of a glider in the air

Most of the web apps I build are rich front-end UIs with a lot of interactivity. Quite often they are generating audio in real time and performing other complicated multimedia activites. This is where ClojureScript and shadow-cljs really shine. All of the leverage of a powerful LISP with its many developer-friendly affordances (editor integration, hot-loading, interop, repl) brought to bear, allowing me to quickly iterate and build with fewer bugs.

On many projects I find myself also needing a small amount of JavaScript on a mostly static page. An example would be a static content page that has a form with a submit button that needs to be disabled until particular fields are filled. It seems a bit excessive to send a 100s of kilobyte JS file with the full power of Clojure's immutable datastructures and other language features just to change an attribute on one button.

In the past I resorted a tiny bit of vanilla JS to solve this problem. I have now discovered I can use ClojureScript carefully to get most of what is nice about the Clojure developer experience and still get a very small JS artifact.

Here's an example from the Jsfxr Pro accounts page. What this code does is check whether the user has changed a checkbox on the accounts page, and shows the "save" (submit) button if there are any changes.

(ns sfxrpro.ui.account)

(defn start {:dev/after-load true} []
  (let [input (.querySelector js/document "input#update-email")
        submit-button (.querySelector js/document "button[type='submit']")
        initial-value (-> input .-checked)]
    (aset submit-button "style" "display" "none")
    (aset input "onchange"
          (fn [ev]
            (let [checked (-> ev .-target .-checked)]
              (aset submit-button "style" "display"
                    (if (coercive-= checked initial-value)
                      "none"
                      "block")))))))

(defn main! []
  (start))

This code compiled to around 500 bytes. It has since been updated to do a bunch of different more complicated stuff and today it compiles to 900 bytes. I'll talk about some of the special weirdness and language tradeoffs in a second, but first here is the shadow-cljs config I used.

{:builds {:app {:target :browser
                :output-dir "public/js"
                :asset-path "/js"
                :modules {:main {:init-fn sfxrpro.ui/main!}}
                :devtools {:watch-dir "public"}
                :release {:output-dir "build/public/js"}}
          :ui {:target :browser
               :output-dir "public/js"
               :asset-path "/js"
               :modules {:account {:init-fn sfxrpro.ui.account/main!}}
               :devtools {:watch-dir "public"}
               :release {:output-dir "build/public/js"}}}

The first build target :app is for the main feature-rich app which does all the complicated stuff. I am fine with this being a larger artifact as it does a lot of things for the user including realtime generation of audio samples.

The second target :ui creates a file called account.js which is just 900 bytes. It gets loaded on the accounts page which is statically rendered. The reason for two completely separate build targets is otherwise the compiler will smoosh all of the code together and bloat your artifact size. It is easiest just to keep both codebases completely separate.

When compiling I found it useful to have a terminal open watching the file size of account.js so I could see real time when the size ballooned out and figure out which code was making that happen.

So what tricks do we have to use in the code to get the artifact size down? Here is a brief list of rules to follow to stay small. If you break any of these rules your artifact size will balloon.

  1. Do not use any native Clojure data types. Don't use vec or hash-map or list for example. Instead you have to use native JavaScript data structures at all times like #js {:question 42} and #js [1 2 3]. That also means you will have to use aget and aset instead of get and assoc. It means we are dropping immutablity and other data type features.
  2. Do not use Clojure's = operator. I know that sounds mad but what you can use instead is ClojureScript's coercive-=. This function does a native-style surface level JavaScript comparison. This means you have to give up the value based equality comparison you can use on deeply nested datastructures in Clojure.
  3. Do not use certain built-ins like partial. Other clever built-ins like juxt are probably going to be bad for file size too. As far as I can tell it's anything that uses immutable Clojure types under the hood. For the specific case of partial you can use #(...) instead to do what you need.
  4. Use js/console.log instead of print.
  5. Use (.map some-js-array some-func) instead of (map some-func some-js-array)

Generally as far as possible you should stick with native JavaScript calls and data types.

If all of this sounds onerous remember that the idea here is to only do this in situations where you have a small codebase giving the user some small amount of interactivity on a web page. So that's the tradeoff. You still get LISP syntax, editor integration, hot-loading, repl, and lots of other nice Cloure stuff, but you have to forgo immutable datastructures and language features like partial.

I have created a small library called cljs-ultralight to help with the UI side of things. It uses browser calls and returns JS data types. You can use it to perform common UI operations like attaching event handlers and selecting elements, without incurring too much overhead.

The library applied-science.js-interop also works with these techniques. Require it like this: [applied-science.js-interop :as j] and you can use j/assoc! and j/get and friends. Note if you use j/get-in or other functions that take a list argument, you should instead pass a JavaScript array which works well.

Also note that @borkdude has a couple of very interesting projects under way in this space. Check out squint and cherry for more details.