Inspiration

Chores are the most predictable fight in a household. Parents repeat themselves, kids tune it out, and everyone loses. The insight I kept coming back to: kids don't need more reminders — they need a loop worth coming back to. Duolingo did it for languages; nobody had done it properly for family habits, and the apps that tried felt like spreadsheets with a mascot bolted on.

There was a second thing I wanted to fix: allowance is where most kids meet money for the first time, and it usually happens as loose cash with zero visibility. If chores are already tracked, the wallet should be part of the same story.

Shipaton was the push to stop sketching it and ship it.

What it does

Chore Quest turns household chores into quests, with two very different apps sharing one codebase.

For parents: create chores with categories, recurrence, assignment and optional photo proof; an approvals inbox to accept or reject completions; an agenda view of the week; a rewards shop you fill yourself; family challenges; an analytics dashboard with a north-star metric (approved chores per week) and a kid leaderboard; and an allowance/wallet system that pays out for completed work and keeps a running balance per kid.

For kids: a quests tab with everything they can do today, XP and levels (floor(sqrt(xp/100))) with rank titles, badges, streaks with a streak freeze, a shared family pet that grows as the family keeps up, a shop to spend XP or allowance on rewards parents approve, and their own wallet.

Kids get in with a PIN from a profile picker; parents with email/password, Google or Apple. A QR code invites a second parent or joins another family. The whole thing works offline against a local cache and syncs when it can.

Free covers the core loop with a small ad slot. Chore Quest Pro ($6.99/month, $49.99/year) lifts the family limits, unlocks the pet, allowance, analytics and challenges, and removes ads.

How we built it

One Kotlin codebase for Android, iOS and desktop: Kotlin 2.4 + Compose Multiplatform 1.11, Material 3, MVVM with Koin, coroutines/StateFlow. ~27k lines of Kotlin across ~171 files, and essentially all of it — every screen, the paywall included — is shared. There are no bundled image assets: every visual is emoji, Compose Canvas (progress rings, the pet aura, confetti) or a Compottie Lottie, so it renders identically on every target.

The backend is NestJS + Prisma + PostgreSQL with JWT auth, bcrypt-hashed passwords and PINs, and the XP/level/streak math mirrored from the client so both sides agree. The app talks to it over Ktor and falls back to its offline cache when it can't.

Monetization is RevenueCat via purchases-kmp, behind a BillingManager façade so no RevenueCat type leaks into common code (desktop gets a no-op). A single entitlement, "Chore Quest Pro", gates everything; the paywall reads the current offering and never hardcodes a price. Ads run through AdMob on the free tier.

Around that: Firebase for push (FCM) and crash reporting (Crashlytics + CrashKiOS), Google Credential Manager and Sign in with Apple, qrose for QR invites, full es/en localization through Compose Resources, R8 and a signed release pipeline.

Challenges we ran into

The paywall that worked everywhere except production. My first App Store submission was rejected under guideline 2.1(a) — the reviewer tapped Subscribe and nothing happened. In debug the RevenueCat Test Store key returned three packages, so it looked fine; the production offering was literally packages: [] because the products had never been configured in the stores. Fixing it meant configuring subscriptions on both platforms by API, and App Store Connect fought me the whole way: prices can't be created through subscriptionPrices (they go as a PATCH on the subscription), price points paginate five at a time, a single US price leaves the subscription in MISSING_METADATA forever unless you push all 175 territories through equalizations, and you separately need subscriptionAvailabilities. Play had its own rule — only the newest regionsVersion is accepted, and creating with one region gives every other region NO_ELIGIBLE_OFFER.

You can't test a purchase from a sideloaded build. Play Billing requires the signature to match what Play distributes, so a locally-signed release always fails. Everything has to go through the internal track, which turns each test into a full upload cycle.

Error states turned out to be a product surface. I rewrote the paywall to distinguish three outcomes: packages loaded, the store answered with nothing, and the query itself failed. Showing "check your connection" when the store simply offers nothing is a lie — and it's exactly what got me rejected.

Shipping iOS out of a KMP monolith. Framework wiring, iPad's four required orientations, the deployment target, an icon that still had an alpha channel, PrivacyInfo. Plus a toolchain that bites: AGP 9 breaks the monolithic setup, Compose Multiplatform 1.11 dropped iosX64, and kotlinx-datetime 0.8 moved Instant/Clock into kotlin.time.

A rejection for a missing EULA link in the store description, even though the app already showed it in-app.

Accomplishments that we're proud of

Two real mobile apps from one Kotlin codebase, with no "Android app plus an iOS port" — the paywall, the pet animation, the analytics dashboard and the offline cache are the same code on both.

Doing it without a single image asset, so the art direction survives every screen density and platform for free.

Full Spanish and English localization, offline-first behavior, and a monetization stack that is actually correct end to end: entitlement, offering, packages, both stores, 173–175 regions, and a paywall that tells the truth when the store has nothing to sell.

And the unglamorous one: surviving the App Review loop and coming out with a build that holds up.

What we learned

Monetization is infrastructure, not a screen. I built the paywall UI early and assumed the store side would be a checkbox; it was the single largest source of work in the project.

A test store will happily hide a broken production configuration. Verify the real offering with the production key before you submit — one curl against /v1/subscribers/{id}/offerings would have saved me a rejection.

In Kotlin Multiplatform, the shared code is the cheap part. The cost lives at the platform edges — auth, billing, push, ads — and the win comes from designing narrow expect/actual seams so the edges stay small and the product logic stays in commonMain.

Review queues are the long pole in any Shipaton-shaped timeline. Submit something imperfect early just to learn what the reviewer will say.

What's next for Chore Quest

The suggestion engine is curated and rule-based today, behind an explicit AI seam on the server — swapping in a live model is a server-side change the client already handles (responses carry their source). That's next: chore and reward suggestions actually tailored to a kid's age, history and what they keep skipping.

On monetization: RevenueCat remote paywalls so I can A/B test copy and pricing without a release, plus a web funnel for parents who convert on the couch, not in the app.

On product: co-parent and split-household support, a school-year mode, home screen widgets, and a Galaxy Store build. And re-engagement done properly — the app's whole value is the daily loop, and today it only nudges through basic push.

Built With

Share this project:

Updates