Yesterday I open sourced Gobaith, a mental health tracker I initially created and used many years ago. As a side goal with this release, I looked at three thoughts: 1) Why do developers dislike global state?, 2) Is it possible to get a “you compile it, it works” development experience with TypeScript?, and 3) tool scaling vs human scaling.
Design principles
Here’s some of the design principles I followed, in terms of software architecture. Making something that works with deeply private data has different concerns from regular web apps.
Adaptability
Everyone's condition is their own. What each person wants to track will be different as a result, and it should be as easy as possible to adapt to their needs.
Simplicity of journaling
For data to be useful, it has to exist. Therefore in this app, I've made it as easy as possible to enter a day's record so that it does not take up much time from the day.
No analysis provided
You, your doctors, and your support network are the only ones who understand your exact condition. Therefore this app does not provide any analysis or commentary on your symptoms. It does provide multiple ways to view and interact with the data, but any meaning from the data is up to you to decide.
No AI
AI can be used to discuss medical issues, however the person you should really be talking to are licensed professionals. As a result, I have not build AI into this project. It would be trivial to do so if you wanted to, but again, if you are having serious symptoms you should talk to a medical professional.
Additionally, all code in this repo was written by me, a human.
Data ownership
Your tracked data is yours. Therefore, this app does not limit you from having access to it in any way. There are multiple ways to export and import the data. Additionally, the data is not shared with any 3rd parties, and there are no telemetrics of any kind.
No notifications
Many apps provide notifications specifically for a tracker app. I instead built the habit over time by filling the tracker at the same time I take my medications, which I already had a notification for.
Avoid dependencies
A core tool that you use every day should not need constant maintenance to keep running. Therefore this app uses minimal dependencies. The frontend itself only uses Chart.js, and two CSS frameworks (Pico and Pure.css). The backend only uses Express.
Tool scaling vs human scaling, and some background
Many years ago, I realized I had symptoms of a fairly serious mental illness. At the time, I didn't know exactly which one. I just knew that I had symptoms that didn't match the way my friends and families lived their daily lives.
As a developer, I put my solution hat: to get diagnosed, it may take years before the psychiatrists find both a cause and a treatment plan. Years at this point was too long.
All the advice I found about the diagnosis process highlighted something core to getting diagnosed: historical data. Before receiving treatment, that would basically be symptoms. After treatment begins, that would also include medication.
The tools I found were great, but not what I needed. I needed something a little more flexible, and to be able to fully own my data. They also were only available on mobile, but I wanted it on my laptop too.
So the journey began to write my own. Since I wanted to get it done quickly, I went for as basic Html, CSS and JavaScript as I could. No build processes, global state, and no real render loop. It worked. I used it daily for a couple of years at least, showing my psychs and friends and family where I was at.
Several years passed, the idea of releasing the code so that it might help other developers who were in my situation kept coming up in mind. As is typical, my priorities changed, pushing the release further back each time. But recently, I started meeting more developers in my situation, pushing the idea of a release to the top of my todo pile.
A person doesn’t scale. Code does. Code can help more people than a person. A person however might be able to help more deeply than code. Empathy and understanding goes a long way, and currently code can’t quite truly do that. It’s a classic quality vs quantity problem. Gobaith (the tracker) being open source can help more people, and help them to get more understanding of their own situation.
The same thing happens in work environments, too. If an employee is the only one who knows how to do something, and has frequent requests from others to do so, then that person becomes a bottleneck. However, if the knowledge is shared, or automated via a self-service application, it frees up the developer’s time as well as reducing turnaround for other employees.
Global state and the big re-write
Originally, the tracker relied entirely on global state, and updated the DOM directly in the callback for any events triggered. Because I wanted to build it quickly, I avoided all build systems and clever libraries. There’s no need for an update loop when the DOM is directly modified.
But why do people dislike global state? Global state makes it hard to understand the flow of an application. Global state is useful, because of that lack of structure. Quick and simple, with code living closely to the UI. However, since my main concern with releasing the mood tracker was data loss, I needed that structure and enforced flow.
I figured that I could rewrite this old, brittle JavaScript into TypeScript. With an extra goal: I would not run either the old code nor the new code in the browser until I had completed the rewrite. That’s the experience that was possible with Elm: “you compile it, it runs”.
The idea is that through enough type safety, it’s possible to have a program pieced so well together that there are no or few logical errors. It’s hard to achieve that in impure languages, but I thought I’d see how far I could get. Type errors are given before evaluation (i.e at compile time).
My action plan was:
Untangle state, with a clear render loop.
Give types for everything.
Store data different (i.e no local / Electron / Cordova storage).
Split the JavaScript into multiple files.
Modern web apps are often built around render loops. A minimal render loop doesn’t require a full shadow/virtual DOM. A simple render loop might define:
A function called when state is changed, typically called render.
A function called to map between two states, typically called update.
Individual functions that return content to render with events.
An entry point which initialises any needed state and triggers for the first render.
There’s not much magic to this, though I did introduce a function for use in template string literals. While upgrading the code, there were a bunch of places that an object might’ve been given instead of a string, and therefore the wrong value. To enforce the right values being given in the template literal, I implemented a function that would ensure at the type level that variables within a template literal were the right type.
Which catches some errors like using objects or non-strings:
Instead of needing to run code to see [Object object] popping up in rendered code, the error is pushed to the type level. The more you can rely on the types, the less you need to actually run code.
Originally, the tracker was cross platform via Cordova (mobile) and Electron (web). Cordova was outdated even 6 years ago when I first made the tracker, so I decided to drop support for it this time around. The question then becomes how do people use the tracker on their phone?
I decided to use service workers with IndexedDB for state management. While IndexedDB is a little less reliable than fully stored locally, it would provide some way of storing data regardless of which platform the app is used on. The architecture ends up looking like this:
Service workers remain a little annoying to get consistent. Particularly since many PWA features are locked behind https, which is difficult to test locally. A lot of documentation of behaviour is pretty lacking, so even specifications don’t really match up with how different browsers have implemented it. Still, I like the idea conceptually.
Hope
It took a few evenings to get the rewrite done, and once I’d migrated everything over to TypeScript with separate files, I started ruthlessly cutting out unused functions or features. Finally it got to the point where the code compiled successful, so I opened the browser and was greeted by a fully functioning app.
The remaining fixes were mostly to do with styling. I’m not a designer, nor do I focus on design much. So I leaned heavily on CSS frameworks (Pico and Pure).
I think that TypeScript is powerful enough these days to move most debugging to the type level. No fancy types needed, just good structure, enforced by well-typed functions.
Here’s a few screenshots of the app (with demo data):
![](https://substackcdn.com/image/fetch/w_474,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc5c67b-d816-4d4b-8ca1-c74968a64a81_1086x1029.png)
![](https://substackcdn.com/image/fetch/w_474,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb28ddc9-c08f-4fc0-8c18-f57c6bc176ec_825x1013.png)
![](https://substackcdn.com/image/fetch/w_474,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff44b8953-2e66-4616-b48f-5dea5afadb5b_1144x730.png)
![](https://substackcdn.com/image/fetch/w_720,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50bdb286-cb77-47a6-9662-0121558373a2_1115x1030.png)
![](https://substackcdn.com/image/fetch/w_720,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb24124c7-9867-4b23-8568-8ed514909c7d_526x301.png)
I hope that this app might be useful to other developers. Or perhaps provide inspiration. It’s designed to be very flexible, but requires programming knowledge to adapt it to your needs. It helped me find a path through a stressful, chaotic time. Check out the Github repo for more info.