Build in public · May 29, 2026 · 15 min read

Building EqualHome Solo: Web, iOS, Android, and the Decisions That Tied Them Together

An app that makes domestic labor visible and fair in couples. Built solo across web, iOS, Android, and a backend on Cloud Run, currently in App Store and Google Play review. Notes on the product, pricing, and retention decisions that mattered most when the unit of value isn't one user but two.


Most consumer apps optimise one user at a time. The signup is single-player, the home screen is personal, the retention curve is computed per account. That model works because for most products the user is the customer, the user is the consumer, the user is the locus of value.

Couples apps break this. The unit of value is two people who have to coordinate, not one person who has to convert. Every decision you make about onboarding, retention, pricing, and notifications shifts under your feet once you accept that.

EqualHome is a couples app that makes household labor (tasks plus mental load) visible and fair. Web (live), iOS and Android (currently in App Store and Google Play review). Solo build, three surfaces, a backend in production on Google Cloud Run. 16 commits, 28 REST endpoints, 5 cron jobs, 0 TypeScript errors at every CI gate.

This article is not the shipping log. It is the post-mortem on the design and product decisions that took roughly three months from idea to App Store submission, with a focus on the ones that only make sense once you accept the multi-user unit.

The product question that justifies the work

Couples fight about housework. They fight more about the housework one of them does not see. Mental load (remembering doctor appointments, knowing what is in the fridge, deciding what is for dinner) is invisible labor: the hardest to recognise, the hardest to share. There are productivity apps for individuals, shared calendars for families, chore wheels for kids, but nothing that takes the equity question seriously for the two adults who actually run a household.

That is the gap.

The product hypothesis: if a couple can see the imbalance, and if the system gives both of them a way to agree on what to rebalance, the conversation shifts from blame to repair. The hypothesis is not that we will solve household equity. It is that we will create a shared object both partners can point at, which is currently missing from the household.

Decision 1. Anonymous-first onboarding

Most apps with a couple or household model gate the experience behind a signup wall and a partner invite before anyone sees value. EqualHome inverts this.

You can explore the entire app in anonymous mode. Firebase anonymous auth gives you a UID. You can browse the categories, see the Balance Score, log a few tasks. The “save your account” prompt comes after you have used the product for at least one cycle, and when it does come, it preserves the UID through Firebase’s linkWithCredential. No data loss on conversion.

Why this matters in numbers I can defend internally: in couples apps, asking for both a personal account AND a partner invitation before any value is shown roughly halves activation. Anonymous-first removes both walls and lets the product earn the signup.

The interesting downstream consequence: anonymous users still receive push notifications, still see the daily prompt, still get suggestions. The product behaves like a real product even when there is no profile attached yet. That decision flows from the product thesis (the value is in the daily logging, not in the account), but it has architectural consequences across every endpoint.

Backend work this required: a separate auth middleware, verifyTokenOnly, that validates the Firebase ID token without requiring a Firestore profile to exist (because the create-profile route cannot require a profile). The original middleware, requireAuth, was too strict and blocked exactly the route it should not have blocked. One of those bugs that is obvious only once you have written the second middleware.

Decision 2. The two-threshold invitation

When the unit of value is a couple, only half the people are sitting in front of the app at any given moment. The other half is being invited, often through a channel the inviter chose instinctively, often through a phone they are looking at while in the same room as the inviter.

EqualHome’s invitation has to clear two thresholds the inviter rarely thinks about. It has to look serious enough that the partner takes it seriously. It also has to be short enough to share over the channel real couples actually use. In our target geography that channel is WhatsApp, followed by SMS.

Three primitives:

  1. A long, secure token (64 characters) used in deep links. Cryptographic, unguessable, single-use.
  2. A short code (8 characters) used in the URL, equalhome.app/j/XKFM2Q9P. SMS-friendly, copy-pasteable, easy to type if the link gets mangled by a messaging app.
  3. A dedicated landing page for the invited partner, with the partner’s name pre-filled in the welcome copy.

The 64-character token is technically superior. It is also socially impossible to forward over SMS. The short code exists because of that one fact. When the flow has two real thresholds (technical and social), engineering one and ignoring the other ships a worse product than engineering both at 80%.

Decision 3. The Balance Score, and the vote that follows

The product’s central artifact is a Balance Score. Two circles, one per partner, breathing in counter-phase animation on the home screen, with the percentage split underneath. The visual itself is the message: this is what the system sees, can you both look at it together.

But a score is not a feature. A score is an invitation to do something about it.

The follow-up is the weekly insight. Every Sunday, an LLM (Claude Sonnet on the backend) generates a one-paragraph read of the past week’s data with one proposed action, always framed positively (“partner X could take on Y”), never blame-framed (“partner X is not doing enough”). Critical guardrail: the prompt is sanitised against injection (real users with real names write hostile-looking things sometimes), and the LLM’s response is validated against a Zod schema before it reaches the app. The model is not allowed to return arbitrary shapes. Prompt cannot guarantee output. Code can.

The proposed action surfaces as a card on the home screen. Both partners can vote. If both vote “ok”, an assignment is created for the coming week and shown as a banner on each partner’s home screen for the duration of the cycle.

This is a couples-native interaction primitive. Not a notification you tap to dismiss. Not a chore wheel you set up alone. A symmetric, low-stakes vote that creates a binding-for-a-week change. The assignment expires automatically at the end of the week, no escalation, no guilt. The product does not keep score of broken assignments. The product creates the space for the conversation, and lets the couple choose to recommit or move on.

Decision 4. Retention is a backend decision, not a mobile one

The retention spec for EqualHome was set on the backend before a single mobile screen was built.

The product insight: if logging requires reflection, people drop after three days. The product question: how do you make the daily log effortless, daily?

The decision: a daily push notification at roughly 8pm local time, asking “What did you do today?” Tapping the notification opens a one-tap multi-select sheet with three to five suggested tasks specific to that partner, that day of the week, that time of year. Confirm in one tap, the day is logged.

The infrastructure to make this work lives on the backend, not the app:

This is the architectural shape of retention. The mobile app is a thin renderer. The state, the schedule, and the intelligence live where they can be updated without an App Store release. That choice is not free (it requires real backend work) but it is correct for a product where retention IS the product.

Decision 5. Pricing the couples loop

The freemium gate decision came up twice and almost went the wrong way both times.

Version 1: locks everywhere. Weekly insight gated. History gated. Assignment voting gated. €4.99/month or €49.99/year, -16% on the annual. RevenueCat for the entitlement, App Store and Play Store for the rails.

The problem: the assignment voting IS the couples loop. If you gate it, you have gated the thing that makes the product work for couples, in a couples product. Half the time you will have one partner paid and one partner free, and you have broken the loop for both.

Version 2 (shipped): gate revenue, keep engagement free.

Free, for both partners, forever:

Premium (€4.99/month or €49.99/year):

The couples loop (vote on assignment, balance the load this week) stays free for both partners regardless of subscription status. Either partner can pay for the deeper insight, the long memory, the shareable report. Nobody is locked out of the loop because their partner did not pay.

This is a couples-native pricing decision. It would be wrong in a single-player product, where gating the engagement loop is the standard freemium move. It is correct here, because gating the engagement loop in a couples product is the same as gating the product itself.

Implementation detail that mattered: the mobile reconciles premium status from two sources, the backend isPremium field AND RevenueCat directly, taking the union with upgrade-only logic. The first version of this naively trusted whichever source replied last, and downgraded an active premium subscriber to free when a backend webhook arrived late. Three angry users, one urgent fix, one rule for life: subscription state is upgrade-only on the client until you have explicit evidence of a cancellation.

The contract bug that came back three times

One war story, because every shipping log needs at least one.

The mobile app reads name, the backend serves nameFr. The mobile reads glyph, the backend serves emoji. The mobile reads enabled, the backend serves isActive. The mobile reads plan and isPremium, the backend serves subscriptionStatus.

Each mismatch caused a different visible bug: empty categories on the home screen, missing icons in the task picker, the paywall shown to a paying user. Each one was found, “fixed” in the mobile with a one-line hardcoded mapping, and forgotten. Until the next field rename caused it to happen again.

After the third occurrence, the architectural answer: a response mapper layer on the backend that translates internal model names to the explicit mobile contract, plus stable IDs (deterministic slugs, not random Firestore document IDs) so the mobile can reference categories by name across releases. Now the mobile contract lives in one file. When it changes, the build fails. The mapper became the single source of truth for what the mobile is allowed to see.

Ad-hoc field renames at the API layer are technical debt that compounds linearly with surfaces and quadratically with releases. Pay it once with a mapper layer, or pay it every release.

The 30-vulnerability audit

Short section, but worth its own heading because the work is usually skipped on solo projects.

Before the apps went into review, I ran a systematic security audit of every endpoint against a checklist of common vulnerabilities. The result: 30 issues identified. 4 critical, 6 high, 10 medium, 10 low. All closed before submission.

Some examples, picked because the lesson generalises:

The point is not that any of these are exotic. The point is that most solo products ship without ever having looked. Looking is the decision.

What is in v1, and what is not

Honest scope at submission.

Shipped:

Not yet:

The reason “not yet” lives second on this list, not first: it makes clear that the scope cuts were deliberate and the work that remains is process work, not architecture work.

What this taught me

Three things I will carry into the next product.

1. When the unit is multi-user, friction compounds twice. Onboarding friction halves the funnel twice (once per partner). Notification fatigue compounds twice. Trust compounds twice. Pricing decisions that look correct in single-player products break in multi-user ones for reasons that are not visible on a single-user metrics dashboard. The hardest part of designing a couples product is keeping both partners in scope on every screen.

2. Retention is an infrastructure choice, not a feature. The decision to send the daily prompt at the right local time, with the right suggestions, idempotently, was made in the backend. The mobile app renders the result. This is how retention should be architected when the work is hard, because the mobile is the surface most likely to be wrong and the backend is the surface most easily fixed.

3. Contract layers earn their cost in week three. The mapper layer felt like overhead in week one. It is the architectural fix that stopped the bug from coming back in week three. Every consumer-facing shipping log I have written has a version of this lesson. The first time you ship, you can skip the mapper. The third time, you cannot.


Try EqualHome