Inspiration

Most habit apps sell a quiet contract with yourself. Open the app. Tap done. Close it. Nobody saw it, so nobody cares if you skip tomorrow. We kept watching people start January strong and go silent by February, and we kept watching the exceptions: gym partners who text “you coming?”, study groups that wait at the library, group chats that roast the person who skipped, and the friend who actually buys lunch when they lose the bet.

That is the insight we designed around. Accountability to yourself is easy to negotiate with. Accountability to people you like is harder to fake. Verdly exists to put that social heat inside the product, without turning into a shame machine.

We also refused to make this a lecture about the planet or a guilt dashboard. The grove language (planting, roots, showing up) is how the product feels, but the job is any habit that needs witnesses: early mornings, training, writing, quitting something, showing up for a duo. If a friend can see you planted today, the habit has a chance. If they can react, rank you, and hold a real world stake over you, it has a spine.

What it does

Verdly is two products that share one identity: a serious solo habit home, and a social challenge arena that is the feature we would put in front of judges first.

Your grove (solo). You plant habits, check them off, and build streaks that belong to a real local day, not a UTC timestamp. Completions are stored as “this habit, this local date,” frozen at the moment you actually did it, so flying across a timezone does not rewrite yesterday. When life punches a hole in the run, streak shields can bridge a missed day without pretending you completed it. That distinction mattered to us: a shield is mercy, not a fake check. Premium (RevenueCat) unlocks unlimited habits, focus sounds, deeper analytics, shields, and a monthly coin drip. At seven days, at a loss, and when a shield could still save you, the paywall talks about that exact moment instead of a generic “go pro.”

Friends Grove and last planted. Your crew is not a follower count. The grove shows who planted today, who is still waiting, last active (only if both of you allow it), and small social moves like “saw you” witnesses. The home feed is people doing the work, not a content network. You can nudge, gift grove cosmetics with coins, and feel the room wake up when the first person plants.

Duo streaks. Two people lock the same daily bar. The day is not closed until both of you are in. Milestones (3, 7, 14, 21, 30, and on to a year) pay XP and have names on purpose: Week Warriors, Iron Pair, Year of Two. If one of you is stuck, swap a day lets a partner gift a completion that counts without a photo. It is cooperative accountability, not a second leaderboard.

Goals that last longer than a streak. Build, quit, or maintain. Confirmations, silent gaps, urge check ins, and copy that talks to the person you are becoming, not a calorie number. This is the slow layer under the daily fire.

Home screen widgets and Wrapped. Glance widgets put today’s habits on the launcher so the app does not have to be open to be honest. Wrapped turns a week of showing up into a story you can actually feel proud of.

The arena (the main event)

Create. Friends create a challenge: habit name, a real world stake as text (“loser buys everyone’s lunch”), duration (7, 14, 21, or 30 days), a daily clock, and a scoring mode. Invites are a 6 letter code, a QR card, and a link, saintnico.site/c/XXXXXX, that auto joins after sign in. Rivals show up in the same room.

Post daily. Every day, each player posts one proof photo before the clock. The feed is attendance: who Planted, who is still Waiting, how many showed up out of the roster. After the deadline you can still post, but XP drops (8 instead of 10 plus streak bonuses). Between midnight and 3am we do not let you pretend it is still “today”; we give a late window for yesterday at 5 XP with the streak frozen. That is how we kept the clock strict without being cruel at 12:07.

React. Friends react with weighted stickers. Hype family is +3, love family is +2, wow family is +1. You cannot hype yourself. You get 10 reactions per day per challenge, so the board cannot be farmed. Switching a sticker only moves the point delta.

Score. The creator picks how winning works:

  • Consistency Cup is longest streak.
  • Crowd Favourite is reaction points.
  • Hybrid is streak times 10 plus reaction points.

$$ \text{Hybrid} = 10 \times \text{streak} + \text{reaction points} $$

Standings have a podium, live scores, and “X score behind #1” so chase is visible. When ranks move, Cloud Functions push FCM: you took the lead, you climbed, someone overtook you. The arena is supposed to feel alive while you are not staring at it.

Trophies, badges, and titles. Showing up should leave something behind. Verdly has a tiered trophy cabinet of more than fifty trophies, and we deliberately never tell you the total. You find them by doing the work, not by chasing a checklist. Finishing an oath-backed challenge earns a stake trophy, so the coins you locked on yourself become something permanent instead of a number that vanishes.

Badges live where other people can see them. In an arena roster, the badge next to a name tells the room who has finished oaths before and who is still proving it. On the profile, the oath record is part of who you are, not a buried stat.

Titles are what you wear. Milestone names like Week Warriors, Iron Pair, and Year of Two, and the winner title an arena awards at the end, sit on your nameplate and profile card, so they show up everywhere you do. None of it can be bought. Trophies, badges, and titles are earned, and coins never buy them. That is why they mean something when a friend sees them.

Swear your consistency. Coins are recognition, never cash (there is no withdraw type in the economy on purpose). You can lock at least 10 coins and at most 50% of your wallet on yourself.

  • Finish the challenge and the lock converts to trophy XP (1 per coin, 2 with Premium, 500 XP cap on free).
  • Miss and you forfeit: half of the lock is redistributed to people still in as a consistency bonus.

New wallets get a 20 coin first oath so the 50% cap does not make the minimum stake impossible. That is skin in the game without gambling regulation theater.

Proof, report, leave. Photos upload compressed. You can report a proof. Leaving a tribe costs 50 XP. Ending the arena writes a result snapshot: winner title, final board, top posts. We wanted a closing ceremony, not a doc that just goes isActive: false.

How we built it

We built a native Android app in Kotlin 2.0 and Jetpack Compose (Material 3, Navigation Compose) because this product is motion, lists, photos, and rank changes, and we needed that to feel immediate.

Identity and data. Identity is Firebase Authentication (Google Sign In and email). Challenge documents, members, completions, reactions, and activity live in Cloud Firestore. User progress (XP, grove fields, inbox, presence) lives under users/{uid} with rules that let friends read a public slice and nobody else rewrite your wallet.

Proof images. Proof images go to Firebase Storage at proofs/{uid}/challenges/{challengeId}/.... Before upload we decode, cap the long edge at 1024px, and JPEG compress at 70%. Coil paints the feed. Invite cards and QR use ZXing. Share cards are bitmaps we export, not screenshots of Compose.

Invites. The 6 letter code is not the Firestore document id. We generate A–Z codes, write challenges/{autoId} and invites/{CODE} in one batch, and joiners get the invite mapping. App Links (https://saintnico.site/c/... with autoVerify) and verdly://challenge?code= land in the same parser. People without the app hit a static join page that shows the code, opens the app, or sends them to Play (com.saintnico.verdlyhabits).

Streaks. Streak math for personal habits is a local StreakEngine over Room, with WorkManager sync and a DayChangeCoordinator that re evaluates on calendar day, timezone, and boot. Shields are a ledger: they cover a gap, they do not increment the run. Challenge streaks recompute from the completions map (ISO local dates) with stored memberStreaks as a cache we distrust when it says 0 against real history.

Scoring. Scoring is one contract in Kotlin (STREAK, REACTIONS, HYBRID) and the same formula in Cloud Functions (Node 20, firebase-admin, firebase-functions v2) so FCM rank alerts match the board the user sees. Reactions and check ins run in Firestore transactions so two phones cannot double spend points. The client still applies an optimistic sticker so the UI does not wait on the round trip.

Coins and Premium. Coins, oaths, and gifts are Room first (challenge_stakes, wallet, transactions), then synced to users/{uid}/wallet. Forfeit and convert are idempotent against challenge missedDays and archive state. RevenueCat plus Play Billing own Premium entitlements. Glance widgets snapshot habits. Lottie and Konfetti are the ceremony layer (planted, converted, podium), not the product.

What we didn't build. We did not ship a Python CV microservice. Honesty on the photo is the crew, the report button, one proof per day, and a clock you cannot quietly skip. That was a product choice: social contract first, model later.

Challenges we ran into

Joining a private room without listing the whole collection. Firestore evaluates list queries against the query, not the document you hope exists. If only members can list challenges, a joiner’s whereEqualTo("inviteCode") is permission denied even when the challenge is sitting there. We learned that the hard way. The fix was a get-able index: invites/{CODE}. Join became: resolve code, get challenge by id (signed in users may get by id), then an update that is only allowed if you are adding yourself to members. Legacy challenges still try the old query and fail closed.

Links that survive WhatsApp, Play, and “I don’t have the app.” A code in a screenshot, a pasted URL, an App Link, and a custom scheme all had to become the same six letters. We wrote one normalizer (trim, letters only, take 6, path segment after /c/, query code=). The website is a handshake: show the code, try verdly://, fall through to Play. Auto join after login was the last mile. If that fails, the product feels broken even if Firestore is fine.

What “today” means. Ranking the world on UTC midnight punishes night people. Using “now” for both posting and history punishes travelers. Personal streaks freeze localDate at write in the zone the user was standing in. Challenge proofs key off ISO local dates and LocalTime.now() on the device for the daily window. The 3am grace exists because “I posted at 12:40 for yesterday” is a real human sentence. Late is not full XP and does not extend the streak. After deadline is reduced XP. We would still rather move the clock to the server next, because a client clock can be wrong on purpose.

Hype that cannot be farmed. If reactions are free, Crowd Favourite is a popularity contest with alts. Transactions: one sticker per reactor per proof, 10 proofs per day per challenge, no self react, point delta on swap, memberReactionPoints in the same write. Proof keys are {uid}_{date} so we know owner and day. Cloud Functions still label the original three keys (fire, heart, lightning) while the client has a larger sticker set. We papered that with meta.points on activity. It is a seam we know about.

Oaths without becoming a casino. People asked for money on the line. We put coins in and then wrote a hard constraint: no withdraw, no cash out. Finish becomes trophy XP. Fail feeds the group. First oath grant of 20 coins exists because floor(10 * 0.5) cannot stake 10. Premium doubles XP per coin on convert. The copy is “swear” and “plant,” not “wager.” That was product and legal taste in the same file.

Solo streaks that survive reinstall and silent nights. Room schema moved through migrations; we open the database at launch so a mismatch dies in a recoverable place, not in the first Flow collector. Pending ops queue work for when the network comes back. Widgets read a snapshot file so the home screen is not a live Firestore listener. None of that is glamorous. All of it is why the grove still feels true on Monday morning.

Accomplishments that we're proud of

We shipped this to Google Play (package com.saintnico.verdlyhabits, version 1.0.8). Create an arena, share a code or QR or link, rival installs or already has the app, they land in the same challenge, they post a photo, the attendance row updates, stickers move Hybrid scores, the podium and “behind #1” update, and a push can tell you that you just got overtaken.

We are proud of the attendance feed more than any badge. “2/5 showed up” is a sentence that changes behavior. We are proud that Hybrid is a real formula (streak * 10 + reaction points) with deterministic tie breaks (points, then streak, then uid), duplicated in Cloud Functions so notifications cannot disagree with the UI.

We are proud of the oath loop that converts or forfeits without returning spendable coins, and of the invite architecture that respects Firestore rules instead of opening the whole challenges collection to the internet.

We are proud that the same app is a daily solo grove: StreakEngine, shields, duo lock, goals, Glance, Wrapped, trophies, grove gifts, referrals at saintnico.site/r/....

Habits are easier to keep when someone's watching. Verdly makes sure someone is.

What we learned

Nobody is in the room. People don't fail habits for lack of advice. They fail because nobody is watching. When even a small crew can see the proof, completion stops being a private mood. That's why the arena feed shows attendance (“2/5 showed up”) instead of a personal stats page.

A day is a cultural object. A streak belongs to your real local day, not a UTC timestamp. We store completions as “this habit, this local date,” frozen at the moment you act, so flying across a timezone can't rewrite yesterday. Once a streak feels unfair, trust is hard to win back.

Strict clocks need gentle edges. A daily deadline only works if it's real, so late posts drop from 10 XP to 8. But between midnight and 3am we open a late window for yesterday at 5 XP with the streak frozen, so 12:07 doesn't punish someone for being human. Strict about the rule, kind about the edge.

Make farming boring instead of impossible. Stickers are weighted (hype +3, love +2, wow +1). You can't hype yourself, and you get 10 reactions per day per challenge. Switching a sticker only moves the point delta. You can't stop every gamer, but you can make gaming the board not worth the effort.

Verification doesn't have to be a model. One photo, one day, a clock, a cap on hype, a report button, and friends who know your face are enough to keep the social contract. Better checks can come later. In v1, we chose not to fake them.

Security rules are product design. Private arenas and shareable invites pull in opposite directions. Instead of loosening privacy for everyone, we built invites as their own thing, so a link can get someone into one challenge without exposing anyone else's.

The same rule in two places will drift. Hybrid scoring ($$10 \times \text{streak} + \text{reaction points}$$) has deterministic tie-breaks: points, then streak, then uid. We duplicated that logic in Cloud Functions so a push saying “you were overtaken” can never disagree with the leaderboard on your screen.

Mercy has to be labeled honestly. A streak shield bridges a missed day without pretending you completed it. Swap a day lets a duo partner gift a completion that counts without a photo. Both are visible acts of mercy, not silent edits to the record, which is why they don't cheapen the streaks around them.

Coins work when they're identity, not an ATM. Skin in the game means XP, titles, and half of your forfeit going to the people still in. Nothing in the economy can be withdrawn, on purpose. New wallets get a 20-coin first oath so the 50%-of-wallet cap never makes the 10-coin minimum impossible.

Endings deserve a ceremony. When an arena closes, we write a result snapshot: winner title, final board, top posts. We wanted people to feel it finish, not watch a database flag flip to isActive: false.

What's next for Verdly Habits

  • No cash out, ever. That is a promise in the coin model.
  • Server-authoritative clock. We want the daily clock and proof window authoritative on the server, so a moved device clock cannot cheat the arena.
  • Team versus team arenas. Campus, office, and friend groups as sides, building on the same proof and scoring engine.
  • Optional proof assistance. On device or cloud checks that the photo is new, a person is in frame, or it is not a duplicate of yesterday, with humans still able to report. Captions can be suggested later. The feed should stay people, not generated sermons.
  • Arena streak recovery. A freeze you earn by showing up for others, matching personal shields, not a mute button on failure.
  • iOS. When the Android contract is boringly solid: invites, clocks, oaths, grove.

The north star stays the same. Make showing up visible. Make flaking expensive in pride, not in cash. Make the crew the product.

Built With

  • Kotlin
  • Jetpack Compose
  • Material 3
  • Navigation Compose
  • Firebase Authentication
  • Google Sign-In
  • Cloud Firestore
  • Firebase Storage
  • Firebase Cloud Messaging
  • Cloud Functions (Node 20, firebase-admin, firebase-functions v2)
  • Room
  • DataStore Preferences
  • WorkManager
  • Glance
  • Coil
  • Lottie
  • Konfetti
  • ZXing
  • Gson
  • RevenueCat
  • Google Play Billing
  • Play Services Location
  • saintnico.site (App Links + invite landing)

Built With

Share this project:

Updates

Submission history