I built a SaaS in 24 hours

created on 2024-04-23

This weekend, I participated in TribeHacks IX at my university, William & Mary.

My submission, FocusGuardian, is a service that allows you to define rules that will block any service you choose. Are you a chronic late night scroller? Block Instagram and TikTok after 10PM. Are you guilty of checking Hacker News constantly during your classes or at work? Block it M-F, 9-5.

Since it's DNS based, it can be blocked at any time, on all of your devices, and will truly act as a network block. The reason why I built this instead of using existing solutions like Apple's Screen Time or Opal, or browser extensions, are as follows:

  • I need something that works across all of my devices, as I am just as likely to consume on my laptop as I am my phone
  • I need something that can't be easily bypassed. At this point, I have been trained to bypass Apple's Screen Time prompts by clicking "ignore" every day, it's almost muscle memory. So I want something that won't stop me from opening the app but rather break its functionality, so I'm more aware of the block.

The possibility of scheduling the blocks also serves to put myself in a routine - if I know I will have an hour of social media at the end of the day, I won't constantly check it throughout the day. In my experience that's better than just limiting it to one hour for the whole day, because I could quickly burn through it, and there's no schedule I'm forcing myself into.

Inspiration

I thought to myself, what if I just program a DNS server to do this? That sounds like it'd work - just return a 0.0.0.0 response if it needs to be blocked. I could parse some rules to decide live whether a DNS request should be permitted or blocked, then just pass it up to a recursive DNS resolver if it's allowed.

I would need to make it DNS over HTTPS, though - I could NOT buy an IPV4 address for every user, so DNS over UDP and TLS were out of the question. DoH, however, allows you to set whatever path you want for the query, meaning you could make the endpoint https://dns.focusguardian.org/USER_ID/dns-query, and it would be specific to one user's rules and schedules.

Selling Points

My idea distinguishes itself from existing solutions by addressing some limitations and offering unique features:

  • Cross-Device Compatibility: Unlike some existing solutions such as Apple's Screen Time or browser extensions, FocusGuardian operates seamlessly across all devices, including laptops, smartphones, and tablets. This ensures consistent distraction-blocking functionality regardless of the device being used.
  • Enhanced Blocking Mechanism: Traditional solutions often rely on prompts or restrictions that users can easily bypass. In contrast, FocusGuardian employs a DNS-based blocking mechanism that effectively prevents access to specified services.
  • Flexible Scheduling: By allowing users to schedule distraction-blocking rules based on specific times and days, FocusGuardian promotes the establishment of healthy digital habits. This flexibility enables users to tailor their usage restrictions to their individual routines and preferences, ultimately fostering more productive and balanced lifestyles.

I'm not sure how differentiated I am in reality from the existing market (such as Opal), but hey, I spent my 24 hours coding, not doing market analysis. :)

Frontend

So, I got started! I whipped up a Vue/Vite/TailWind frontend in a few hours, with tabs for information on how this can be beneficial to parents, students, etc. I'm NOT a frontend developer (last year, my hackathon submission was hand-written HTML fragments being returned by a Rust backend and it looked... subpar). But, in the past year, I've been building Vue frontends for side projects, and I'd like to say I'm passable by now.

Screenshot of the frontend

I wanted a landing page to be done first, since that's what gets looked at for the hackathon. For my own sake I spent the vast majority of my time on working on the actual functionality but making it look pretty is really what gets you points.

Backend

Next - the backend. I started writing writing some sample code that parsed DNS over HTTPS requests in Rust. I was nose-deep in the RFC and implementing the two possible methods that could be used. At first, I simply proxied to Quad9's DoH endpoint, but that quickly proved too slow - it adds the entire latency of another HTTPS request. I switched to a local resolver issued over UDP.

I tried adding the DoH configuration to Firefox to test, and success! I could see the requests being proxied and Firefox worked as expected.

Apple cannot adhere to spec, ever.

I used this site to generate a DoH profile for my phone. But alas, even when my DNS resolver follows spec, Apple does not work. You need to add support for POST/GET requests, where GET requests could have a dns parameter or a name and type parameter - where dns is a base64-encoded DNS packet, or name is a domain and type is a DNS packet type. This involves manually constructing a DNS packet if they decided to send it as the latter case.

Maybe I was just sleep deprived at the time but this implies Apple doesn't adhere to the DoH spec. That's okay, I handled that case and Apple was working perfectly!

Now to write some of the actual rule/scheduling/user code.

User database

I have used Pocketbase in the past - it's an absolutely magical self-hosted FOSS Firebase alternative. It handles user authentication, OAuth (sign in with all the services), tables with auth rules, etc. I was able to very quickly get started with it, by using two tables - manual vs scheduled rules - each one associated with a user and list of services.

I modified the Rust backend to grab from the database periodically, then parse the list of schedules/services into a database. The backend then would first check on any DNS requests if the user has an active rule for that service. If so, it would modify the returned IP to be 0.0.0.0 (this is generally the agreed-upon way - NXDOMAIN responses could have the OS use a backup DNS server which would be bad).

Service list

I didn't want to manually collect a long list of services and their associated list of domains, but I had remembered that the AdGuard Home server I run does have the option of blocking services, so they must have a list of these services and domain names associated with each one. And indeed, they do! Buried in the source code is a list - so I had my code parse that and build a list of services supported for my website. (I manually added Hacker News since it wasn't there and that's my worst procrastination tool)

Frontend pt 2

Now, to write code that will allow me to add rules on the fly.

Screenshot of the UI that allows adding a rule by name, service, and time/day

After a lot of searching about Vue forms, I finally have a component to add rules! After integrating with PocketBase, it's as easy as:

await pb.collection("scheduled_rules").create(bodyParams)

I wrote some code to handle the case where the rule isn't scheduled but rather manual, and we were off to the races.

Mobile App

Upon making the desktop site, I realized: I was definitely not good at making responsive Vue sites. The create rule component notably was extremely poor when viewed on mobile. I researched some solutions for creating mobile apps in a cross-platform manner and found the Ionic Framework which served me very well. I got a simple proof of concept working quickly, and it looked decent.

Demo of the mobile app, with a similar UI to the desktop app, but with a menu along the bottom

At this point, I had a working website, a demo mobile app written in Ionic, and instructions on how to add FocusGuardian for every common platform (Windows, Mac OS, Linux, Firefox, Chrome, iOS, iPad OS, etc). But, this wasn't cool enough. For those manual rules, is it really the best decision to have to open your computer to visit the site to enable it? I wanted a hardware solution that would allow me to enable/disable my rules without using a browser.

Hardware prototype

I had so many old pieces of hardware laying around, from ESP32s and RPi Zeros. None of those would be enough for a consumer-facing prototype, but I had one secret weapon - a LilyGo T-QT.

Image of the T-QT, a very small ESP32 device with a screen and two buttons

This bad boy is incredibly small, yet has buttons and a screen! That's all you need, really. Can we go back to the blackberry's now?

So, I got to coding - Surely I could learn LVGL in the 12 hours left.

I began by simply modifying the sample program, a duck waddling gif, and added "text". That worked okay.

Now, to mock rendering the rules. We will write code to actually parse from the database, but below, I will fake it.

static char* rules[] = {"instagram", "hn"};
static bool* enabled[] = {false, true};
static int current_rule = 0;
static int max_rule = 1;

// In refresh_debug
lv_obj_t *debug_label = lv_label_create(src);
String text;
text = "Rules\n";
for (int i = 0; i < max_rule; i++) {
  if (i == current_rule) {
    text += "> ";
  } else {
    text += "   ";
  }
  text += '[';
  if (enabled[i]) {
    text += "X";
  } else {
    text += "   "; // no monospace fonts, sorry!
  }
  text += "] ";
  text += rules[i];
  text += "\n";
}
lv_label_set_text(debug_label, text.c_str());
lv_obj_align(debug_label, LV_ALIGN_TOP_LEFT, 0, 0);

// If the toggle button is pressed
if (enabled[current_rule]) {
  enabled[current_rule] = true;
} else {
  enabled[current_rule] = false;
}
refresh_debug();

// If the scroll button is pressed
current_rule = (current_rule + 1) % max_rule;

This was pretty good! I could simulate "scrolling through" the rules (updating where the > character was) with the right button, and use the left button to toggle the currently selected rule. Now for actually integrating with the database.

HTTPS on the ESP32

Getting HTTPS working on the ESP32 was really the most difficult part of this project. I ended up simply disabling TLS certificate validation. It's still encrypted, but anyone who can control the connection could MITM by presenting any TLS certificate. Unfortunate, and obviously not ideal, but this will be just fine for my one-day-long hackathon. I'm well aware of the security implications, and will be iterating on this to find the best solution if I bring this to fruition.

Parsing

JSON parsing was pretty easy - I mostly just used the ArduinoJson library. It became pretty easy to parse responses into the global rules array (once I switched to using Strings!).

Demoing the hardware

Approximately 10 minutes before the deadline, I filmed my demo for the project, including the hardware.

Summary of my tech stack

  • Rust-based DNS backend (handles processing DNS requests, keeping up-to-date with newly created rules, and generating Apple MobileConfigs)
  • Vue-based frontend
  • Ionic-based mobile app demo (still being worked on)
  • Go-based user database (PocketBase + custom Go)
  • C++-based software on the embedded hardware

Juggling it all had me accidentally writing Rusty let x in JavaScript, C++'y const * in Go, etc. I don't think this stack is too much; it fulfills my needs at exactly the right complexity, and none of it seems over- or under-engineered for my needs. But just having 24 hours of switching between all of them to become proficient enough to get out a fully-working prototype hurt my head a lot.

Reviews of my tech stack

  • Rust: 10/10. I'm very experienced in Rust, so I'm biased and well over the learning curve for a simple project like this. Rust's performance-strong characteristics reassure me I didn't leave any performance on the table by not choosing another language.
  • Vue: 9/10. Really good dev experience, plenty of libraries for most of the things I wanted to do.
  • Ionic: 7.8/10. Pretty good, though I didn't get this working in time, I could easily freshen it up in a weekend.
  • PocketBase/Go: 10/10. PocketBase makes it incredibly easy to turn a proof of concept web app into a SaaS while saving me from all of the pain of user authentication, OAuth, databases, etc.
    • MailPace for transactional emails: 10/10, awesome and dead-simple to integrate with Pocketbase for all of the sign-up/reset emails (and I wrote the Rust library for it, so if I need to email from the backend, I can do so easily!).
  • C++: 5/10. I have experience working with ESP32 as a platform but it was my first time learning LVGL. Definitely harder to pick up and start hacking with, but I also was down to a few hours. Maybe I disliked it more because I was closer to the deadline when I started working on it?

Results

The results of this hackathon were three-fold.

  • I was very happy to find I could bang out a functional, ready-for-launch SaaS in 24 hours, that could expand into B2C hardware (irrespective of how good the idea is - truthfully I am unsure about demand, so this was more of a technical challenge)
  • I was doubly happy to get the hardware working - I want to work on a proper first iteration, on a larger device, and get all of the kinks out.
  • I won best hardware hack!

All in all, a very fun experience!

What's next for FocusGuardian

I am launching FocusGuardian as a fully featured SaaS today, in a pilot program.

Until June/July, I won't add any code that checks for premium status - so if you want to try out how premium feels, you may sign up and utilize those unlimited rules and ad-blocking without paying.

But if you want to help me recoup some of the costs of the server, domain, hardware, the Celsius's consumed while being built, etc, and feel like you get enough value from it, feel free to click upgrade on the pricing page.

Free plan allows for two scheduled rules, and two manual rules. As well as unlimited DNS requests/devices/etc.

Paid plan allows unlimited rules, ad blocking, requesting additional services for blocking, and first access to a hardware client, should I turn that into a full project (provided interest is there).

I'm starting out the paid plan at $5/mo, $55/yr (with a discount to $2.50/mo, $27.50/yr - see pricing page).

Feedback

If you have feedback about absolutely anything, send me an email at admin @ the domain (either one). Do you think you'd be interested in paying for a product like this eventually? Or would you use the hardware solution? I would greatly appreciate if you signed up today and started using it, and providing feedback!

Send me a letter!

You can visit FocusGuardian here!