The problem
Most habit trackers are not built for learning. They don’t connect a habit to the goal behind it, and their streaks are either so strict that one missed day ruins them or so loose that they mean nothing.
Who it is for
Self-learners and developers who set a goal, such as learning a new language in 60 days, and check in every day. The app is in public beta.
What I built
- Goals with linked habits, notes and study resources.
- Habits to build or to quit, on daily, weekday or weekly schedules.
- Streaks, XP, levels and a dashboard of completion rates.
- Rest days with a weekly limit, and push notification reminders.
- Google sign-in, an admin panel, and an installable web app.
Architecture
A single Spring Boot application organised by feature. Features talk to each other only through their services, never through another feature’s repository. Flyway manages every database change.
Key decisions
Calculate progress, don’t store it
Streaks, statistics, XP and goal progress are all calculated from check-ins when they are requested. Nothing is stored that could fall out of sync with the history.
Past days follow the rules of that day
A day counts as complete if it met the target that applied on that day. Raising a target later never changes your past results. Changing a schedule starts a new streak but keeps your XP.
Other people’s data simply does not exist
Every query is filtered by the signed-in user. If you request someone else’s habit, you get “not found”, which reveals nothing about whether it exists.
How I solved it
Adding Google sign-in created a subtle security risk called account pre-hijacking. Someone could register with your email address and a password before you ever signed in with Google, and keep access to the account. Now, when a Google account is linked to an account whose email was never verified, that account’s password and sessions are removed. This integration test proves it:
@Test
void linkingAnUnverifiedAccountRemovesItsPasswordAndSessions() throws Exception {
// someone signed up with this address without owning it (pre-hijacking)
register("victim@gmail.com");
google("t3", new GoogleIdentity("sub-victim", "victim@gmail.com", true, "Victim"));
signIn("t3", null).andExpect(status().isOk());
login("victim@gmail.com").andExpect(status().isUnauthorized());
}Google sign-in tokens are verified on the server against Google’s public keys, and a Google account without a verified email is refused.
What’s next
- Automatic check-ins from GitHub, so a “commit every day” habit is ticked by real activity.
- A custom domain, so account emails stop landing in spam.
- A paid Pro plan, once there is demand for it.
