
A technical deep dive
At it’s core, QuestKeeper is a production-ready task manager done right. It’s a well-orchestrated system built to balance performance, scalability, and an engaging user experience. Here’s a look at the tech stack and architecture powering QuestKeeper, and a peek behind the 25,000 lines of code.
Frontend
I love Typescript. I love React. I despise React Native.
Flutter + Flame
The QuestKeeper app is built using Flutter for a seamless cross-platform experience. To add a dynamic and interactive touch, the Flame game engine integrates gamified elements, allowing users to engage with animated characters and a pixel-art environment. State management is handled with Riverpod, ensuring efficient and responsive UI updates.
A note on state management
State management is surprisingly difficult to do right. There were times where I was actively trying to figure out what to be doing and where. While Riverpod does make it simple in some areas, it was pretty daunting to get into, especially using Flutter after three years. This guide helped simplify the basics, which let me iterate from there.
Backend
I didn’t want to pay for a backend if the project didn’t grow. On the other hand, I wanted it to be scalable, partly to learn more and partly just in case.
Enter Cloudflare Workers + Supabase
I’m a huge proponent of open source BaaS. Firebase was the obvious choice at first glance, but knowing how hefty the cost can be with Firebase at scale detered me for the most part. On the other hand, options like Appwrite and Supabase have been getting better and better. I initially built out part of the app in Appwrite, particularly due to the ease of handling notifications through Appwrite. However, Appwrite’s abstraction over Maria was confusing to say the least. On the other hand, Supabase was built on top of PostgreSQL, a RDBMS I’m already familiar with, and so Supabase was a no-brainer for me.
While Supabase has a robhust mobile SDK, I didn’t stick too long with all the DB calls from the app/SDK itself. Why? Social features! Things like friends list and points systems would be difficult to do securely without a proper backend, and so almost all database calls were moved to dedicated APIs/microservices running on Cloudflare Workers.
Workers seems like an odd choice though. When talking about serverless, CF isn’t typically the one that comes to mind. Part of my choice was wanting to learn more about microservice architecture, and part of it was morbid curiosity. Workers doesn’t have the best docs, nor the best community support. However, building a system that is insanely fast and built to scale became trivial because of Hono + Drizzle + Workers.
Microservices and Communication
QuestKeeper follows a microservices architecture, with distinct services handling tasks, social interactions, notifications, and gamification. These services communicate through HTTP requests, with plans to integrate queue-based messaging for improved scalability should the app scale. Webhooks and Firebase Cloud Messaging (FCM) handle real-time notifications.
Optimizations and challenges
Significant time went into the optimizations that this app benefits from. And also Firebase Cloud Messaging (FCM).
Optimizations
Performance is tough. While seemingly simple of an app, there’s a lot that’s being done under the hood to improve performance. In no particular order, here’s a list:
- HonoJS, the express-alternative for edge runtimes: HonoJS provides a lightweight and efficient routing solution optimized for edge environments, reducing latency and improving request handling.
- The
cache
API on Cloudflare Workers: Workers expose acache
API within the runtime to retain information on the machine it’s running on. This is extremely beneficial for global usage, as cached information is physically close to the user, preventing unnecessary round-trips to the Supabase database in the U.S. - Drizzle ORM: Using a type-safe ORM designed for speed ensures optimized database interactions while maintaining strong typing and security.
- Optimistic rendering: UI updates are performed optimistically, assuming success before confirmation from the backend, making interactions feel instantaneous and reducing perceived latency.
Challenges
Push notifications are obscenely difficult, and I don’t know why. Google has Firebase Cloud Messaging as part of their Firebase services. FCM is the only way to send push notifications to users on Android. iOS has APNs, however to simplify notification management, FCM handles sending to Apple devices too. Unfortunately, there aren’t many third party services that handle FCM effectively, at least on edge runtimes. I looked into OneSignal at some point, however their data-collection policies, and the fact that they were on multiple ad-block lists, made it a no-go for me personally.
Scheduling notifications
Aside from that, FCM doesn’t natively support scheduling push notifications. I ended up with my own custom and VERY rudimentary implementation of a push notification scheduler, which boils down to throwing everything with a “sendAt” column in a table in Postgres, then running a cron-job every minute to see if a notification has to be sent out, and sending a request to my notifications microservice.
Yeah… not ideal 💀.
In the future, I would re-work this possibly to using a persistent Redis database (similar to Upstash), and listening for “expired”/“timeouts” and sending a push notification based on that. It’s a bit more elegant, but still not a fantastic system.
Why not local notifications??
Yes, handling notifications locally is significantly easier. However, with my goal to have this truly cross-platform, local notifications would not have cut it unfortunately.
What’s Next?
Future plans include enhancing social features, refining the quests system, and potentially introducing AI-driven task recommendations. QuestKeeper continues to evolve, driven by user feedback and technical advancements.