Mobile App Development · Case Study

Bad Habits App — Building a Full-Stack Gamified Habit Tracker from Zero to App Store in 6 Weeks

By Alex — JBA Agency Published March 21, 2026 12 min read
432
Automated Tests Passing
6 wks
Zero to App Store
8.0/10
Final Engineering Score
19
Database Tables
5
Languages Supported
<200ms
p95 API Response Time

The Challenge: Quitting is Boring — So We Made It Brutal

Most habit-tracking apps fail for the same reason diets fail: they're too kind. They give you streaks that reward you for not messing up and gentle nudges when you do. Bad Habits takes the opposite approach. It weaponizes social pressure, dark humor, and financial accountability to help people quit their vices — smoking, gambling, doomscrolling, fast food, and 17 other trackable categories. If you relapse, your circle knows. If you cheat, your streak is gone. If you want to hide your damage, you'll have to spend coins to do it.

JBA Agency was tasked with building the entire product from scratch: a React Native mobile app, a production-grade backend API, and an admin dashboard — all delivered in a single continuous development cycle across 6 engineering phases. The constraint was real: zero to App Store-ready in approximately six weeks, without compromising on security, data integrity, or the complex gamification mechanics that make the app's core premise work.

"432 automated tests across 6 engineering review phases caught 130+ issues before production, including SQL injection vectors and race conditions — resulting in an 8.0/10 final engineering score." — Engineering review summary, BadHabits.app

The core tension was architectural: how do you build a social leaderboard app where users compete publicly — sharing streak data, wagering coins in duels, forming accountability circles — while enforcing strict privacy boundaries that ensure no one can see another person's individual check-in details? The answer required privacy enforcement at the database query level, not just the application layer.

The Concept & Target Audience

Bad Habits targets people who've tried and failed at quitting something using willpower alone. The app's thesis is that shame, competition, and financial stakes are stronger motivators than achievement badges. The 21 vice categories span 7 domains: substance (smoking, alcohol, vaping), food (fast food, sugar, overeating), digital (doomscrolling, social media, gaming), gambling, shopping, transport (rideshare overuse, impulse travel), and entertainment. Users can also define custom vices with per-unit cost tracking.

The financial accountability layer is core to the experience. Every check-in calculates a real-money cost — cigarettes smoked, bets placed, coffees bought — and aggregates it into daily, weekly, and monthly dashboards. The Shame Receipt feature generates client-side receipts with equivalences: "You spent enough on cigarettes this month to buy 47 lattes." It's designed to sting. That's the point.

Architecture & Tech Stack

Frontend: React Native with Expo SDK 52

The mobile client is built on React Native with Expo SDK 52, using TypeScript throughout. State management uses Zustand for global stores — lightweight, no boilerplate, and compatible with React Native's async rendering model. Persistent local state uses MMKV, the fastest key-value storage available on React Native (C++ backed, synchronous reads, orders of magnitude faster than AsyncStorage).

Animations are handled by react-native-reanimated, which runs on the UI thread rather than the JS thread — critical for the gamification interactions (coin earn animations, duel countdown timers, achievement unlock sequences) that need to be buttery smooth even when the JS thread is busy. Navigation uses Expo Router with a file-based routing structure. The UI follows a dark brutalist design language: heavy typography, high-contrast accents, and deliberately confrontational microcopy that matches the app's irreverent tone.

Android home screen widgets update the user's current streak and daily cost every 30 minutes — a persistent, impossible-to-ignore reminder that sits on the home screen even when the app isn't open. This was built using the react-native-android-widget package with a background task scheduler.

Backend: Fastify API on Node.js

The backend is a Fastify API — chosen over Express for its schema-first request validation (via @fastify/ajv-compiler), significantly faster throughput, and first-class TypeScript support. Every route has a typed request/response schema, which means malformed payloads are rejected at the framework level before they reach business logic. This alone eliminated an entire class of injection vulnerabilities caught in early review phases.

The API exposes 20+ routes covering authentication, habit management, check-ins, the coin economy, duels, social circles, leaderboards, subscription management, and admin operations. Real-time features — duel status updates, circle notifications, streak alerts — are delivered via Server-Sent Events (SSE), which works reliably across both iOS and Android without the overhead of WebSocket connection management.

The API is deployed on Railway alongside a React + Vite admin dashboard. Railway's environment-based deployments made staging/production parity straightforward, and its built-in PostgreSQL and Redis add-ons kept infrastructure management minimal during the compressed delivery timeline.

Database: PostgreSQL with Drizzle ORM

The data layer is a 19-table PostgreSQL schema managed with Drizzle ORM. Drizzle was chosen over Prisma for its SQL-first philosophy — generated queries are transparent and predictable, which matters when you need advisory locks, row-level locking, and atomic transactions at the application layer without ORM magic obscuring what's actually executing.

"19-table PostgreSQL schema with atomic transactions, advisory locks on check-ins, and row-level locking on circle joins — zero data integrity issues in production." — BadHabits.app engineering documentation

Redis handles session storage, rate limiting state, and real-time pub/sub for SSE fan-out. The combination of PostgreSQL for durable state and Redis for ephemeral/real-time state is a proven pattern that kept the backend stateless and horizontally scalable from day one.

Core Features Deep-Dive

Gamification Engine: Streaks, Duels, Achievements & Bribes

The gamification layer is the product's core differentiator. It's built on four interlocking mechanics, all backed by atomic database transactions to prevent exploits:

Privacy-First Social Circles

Social circles are the accountability mechanism. Users can join public circles (discoverable by geolocation or category), create private circles with invite codes, or form family pairs — the only relationship type with full transaction visibility. For every other relationship, the privacy boundary is absolute.

"Privacy boundary enforced at the database query level — circle leaderboards use LEFT JOIN with COALESCE SUM to return only aggregated totals, making it architecturally impossible to leak individual check-in details." — BadHabits.app security architecture notes

This wasn't a UI-level decision. The leaderboard query aggregates check-ins into totals before they ever reach the API response serializer. There is no code path through which an API consumer could receive a list of individual transactions for another user's non-family account — the query simply doesn't produce that data. Privacy is a property of the schema and the query, not a filter applied on top of raw data.

Shame Receipts

Shame Receipts are generated entirely client-side — no server round-trip required. The Zustand store holds the user's check-in history and per-unit cost configuration. The receipt generation logic computes daily, weekly, and monthly totals and converts them into relatable equivalences using a configurable equivalence table (coffee price, average meal cost, streaming subscription price, etc.). The result is shareable as an image via the native share sheet, generated with react-native-view-shot.

Subscription & Coin Economy

Monetization uses a dual model: a subscription tier (30-day free trial, monthly/annual plans) and a coin economy with IAP coin packs. RevenueCat handles both — its unified entitlement API abstracts Apple and Google billing differences, and its server-side webhook validates purchase events before granting entitlements, preventing receipt spoofing.

Coin packs as in-app purchases were a specific security focus. The Phase 3 engineering review identified an exploitable endpoint where coin grant amounts were derived from client-provided values rather than server-side price table lookups — a classic parameter tampering vector. This was fixed by mapping product IDs to coin grant amounts entirely on the server, with the client sending only the RevenueCat product identifier.

Engineering Quality & the 6-Phase Review Process

The project was structured into 6 engineering phases, each capped by a formal review that produced a written findings report, a severity-bucketed issue list, and a phase score. The review process is JBA Agency's standard quality gate for full-stack product builds — it's what allows a compressed timeline without compressing quality.

1

Phase 1 — Database Schema & Core Models

19-table schema reviewed for normalization, constraint coverage, index strategy, and foreign key integrity. Missing cascade rules and nullable columns that should have been NOT NULL were caught and corrected before any application code was written against them.

2

Phase 2 — Authentication & Check-In Engine

JWT refresh token rotation, serialized token refresh to prevent race conditions on concurrent requests, advisory locks on check-ins. The TOCTOU race condition on streak calculation was identified and fixed here — a check-in submitted twice in rapid succession would have awarded a streak day twice without the advisory lock.

3

Phase 3 — Coin Economy & Payments

RevenueCat webhook validation, coin purchase endpoint parameter tamper fix, atomic coin transaction implementation, duel wagering logic. SQL injection vectors in dynamic query construction were identified and replaced with parameterized queries throughout.

4

Phase 4 — Social Circles & Leaderboards

Privacy boundary verification at query level, row-level locking on circle join operations (preventing simultaneous joins from exceeding circle capacity limits), invite code entropy review, and geolocation-based circle discovery radius validation.

5

Phase 5 — Frontend, Offline State & Widgets

MMKV persistence strategy, Zustand hydration order, offline queue for check-ins submitted without connectivity, Android widget update reliability, and i18n key parity across all 5 language files reviewed for missing interpolation variables.

6

Phase 6 — End-to-End & App Store Readiness

Full 432-test suite run, EAS Build configuration review, App Store metadata, privacy manifest requirements (Apple's required reasons API declarations), and final performance profiling. Phase 6 produced the 8.0/10 final engineering score.

"The app tracks 21 vice categories across 7 domains — substance, food, digital, gambling, shopping, transport, entertainment — with custom vice support and per-unit cost calculation." — BadHabits.app product specification

Security & Privacy by Design

The 130+ issues caught across 6 review phases weren't all critical — but several were. The security findings that would have caused the most damage in production included:

Privacy was enforced structurally, not procedurally. The distinction matters: a procedural privacy control (checking permissions in a route handler) can be bypassed by a bug, a missing middleware, or a new route that forgets to apply the check. A structural control (a query that literally cannot return individual records for non-family relationships) cannot be bypassed without rewriting the query. This is the class of security architecture that distinguishes production-grade applications from prototypes.

Internationalization & Offline-First

The app supports 5 languages — English, Romanian, Spanish, German, and French — using i18next with react-i18next bindings. Every user-facing string is keyed; there are zero hardcoded copy strings in the component tree. The Phase 5 review specifically audited i18n key parity across all 5 language files, checking for missing keys, missing interpolation variables (e.g., {{count}} present in English but absent from the German file), and plural form handling differences across languages.

"Full offline-first architecture using MMKV for local state persistence, serialized token refresh to prevent race conditions, and Android home screen widgets that update every 30 minutes." — BadHabits.app technical overview

Offline-first was a product requirement, not an afterthought. Users track habits in airports, on runs, and in areas with poor connectivity. The architecture handles this through an offline check-in queue: check-ins submitted without connectivity are stored in MMKV and synced to the API when connectivity resumes, with deduplication logic on the server to handle the case where a check-in was submitted twice (once offline, once after connectivity was restored but before the sync acknowledged). Zustand stores hydrate from MMKV on app launch, meaning the UI is fully interactive before any API call completes.

Results & Key Metrics

The final engineering review at the end of Phase 6 produced a structured scorecard across eight dimensions: schema integrity, API correctness, security posture, test coverage, performance, internationalization completeness, App Store compliance, and code quality. The aggregate score was 8.0/10 — with the primary deductions in test coverage gaps for edge-case duel resolution scenarios and one outstanding UI accessibility item for screen reader labels on the bribe selection modal.

432
Automated tests passing
130+
Issues caught pre-production
20+
API routes
95%+
Crash-free session target
<200ms
p95 API response time
21
Vice categories tracked

The sub-200ms p95 API response time was achieved through a combination of Fastify's low overhead, connection pooling via pg with a configured pool size matched to Railway's instance limits, Redis-cached session lookups (no DB query on authenticated requests), and index-optimized queries on the hot paths (check-in creation, leaderboard fetch, coin balance read).

Lessons Learned

Phase-Gated Reviews Are a Force Multiplier, Not Overhead

The instinct on compressed timelines is to treat review gates as optional — something to do after shipping. The BadHabits build demonstrates the opposite. Issues caught in Phase 2 (like the advisory lock on check-ins) would have been architecturally expensive to retrofit in Phase 5. Issues caught in Phase 3 (like the coin purchase exploit) would have been a production security incident. Each phase review paid for itself in avoided rework.

The Gamification Layer Is the Hardest Part to Get Right

Streaks, duels, coins, and achievements are interconnected state machines. A user wins a duel, earns coins, those coins cross an achievement threshold, which triggers another coin reward, which may trigger another achievement. Each transition must be atomic, idempotent, and observable (the user needs to see what happened and why). Designing this as a set of atomic database transactions with explicit event logging — rather than application-layer state — was the right call and simplified debugging significantly.

Privacy Architecture Must Be Decided at Schema Design Time

Retrofitting privacy constraints into an existing schema is painful. The decision to enforce circle privacy at the query level — rather than through permission checks in route handlers — was made during Phase 1 schema design. By the time the leaderboard routes were implemented, the query pattern was already established. Teams building social apps should treat privacy boundaries as schema constraints, not application logic.

RevenueCat Significantly Reduces Subscription Complexity

Managing Apple and Google billing directly involves handling receipt validation, subscription state machines, proration, refunds, and grace periods — separately, for two platforms, with different APIs and edge cases. RevenueCat unifies this behind a single entitlement API and handles the platform-specific complexity. For a 6-week build, this was a non-negotiable dependency. The webhook validation pattern (verify signature, check event type, look up user, grant or revoke entitlement atomically) is straightforward and well-documented.

If you're building a gamified mobile product, a habit-tracking app, or any full-stack React Native application, the JBA Agency engineering process — from schema design through App Store submission — is available as a managed build engagement. See the FAQ for how engagements are scoped, or explore the Fractional AI Officer model if you need ongoing technical leadership rather than a one-time build. You can also read how the same phase-gated quality process applies to SaaS product builds.

About the Author

Alex is the founder of JBA Agency, a web development and AI solutions agency. He operates as a solo agency powered by AI agents, delivering production-grade digital products — mobile apps, SaaS platforms, APIs, and web applications — with a structured engineering process that catches issues before they reach users. Alex has built full-stack products across health & wellness, fintech, professional services, and SaaS verticals. Read more at jbagency.ro/about.