Porting a feature between two apps: what I hardened, what broke anyway
I run a second app, a sports-scheduling tool with its own bill-splitting feature that's been live for a while, real users and all. When MyFinCup needed the same kind of thing — split a group dinner, settle a trip — I didn't design it from scratch. I ported it.
Reusing something proven sounds like the safe path. The math is already correct. The screens already work. Real people already found the sharp edges. All true. It's also not the same as copying the code and swapping the colors. Some decisions that were fine for a scheduling app were genuinely wrong for a finance app. And even with a working original to copy from, I still shipped one bug that had already been found and fixed once, in the very app I was copying from.
What I deliberately hardened
The original version trusts its application code to only ever ask for data scoped to a group you actually belong to — reasonable, as long as every future line of code remembers to check. I didn't carry that trust forward. Every table behind MyFinCup's Split feature checks group membership at the database level itself, not just in the screens that display it — so a future feature (built by me, or by an AI with no memory of this decision) can't accidentally show someone a group they were never part of, just by forgetting to add a filter.
I also deliberately left something out. The original app lets an admin search every user by name to find who they're looking for — reasonable for a sports team, where finding your teammate is the whole point. I didn't bring that into a finance app. A budgeting tool that lets anyone search a directory of every other user's name is a real privacy tradeoff a scheduling app just doesn't carry, and I made that call on purpose rather than by accident. Members start as typed names; a real account only links in once someone's actually sent an invite and chosen to claim it.
Four bugs an audit and a real test run caught before anyone saw them
Before calling Split done, I didn't just eyeball it against the original — I drove the whole thing end to end against the real database, the same way a real person would use it, and checked each step actually did what it claimed.
That caught the "add members" picker suggesting people who were already in the group — technically working, entirely useless, since every suggestion was, by definition, someone already there. It caught the "invite" button generating a brand-new link every time someone tapped it, instead of returning the one already sent, so nobody could tell which link was actually "the" link anymore. It caught the fact that if someone picked the wrong name off an invite (an easy mistake in a group of five similar names), there was no way to undo it — not for them, not for me — so I added a way to release a wrongly claimed spot. And it caught a double-tap on "mark as paid" creating two settlement records for the same debt, because the button stayed clickable the entire time the request was still in flight.
The one that had already been fixed once — and slipped through anyway
That last bug is the one that actually stung. The double-tap problem wasn't new — it was the exact same bug I'd already found and fixed once before, on the exact same "mark as paid" action, in the original app I was copying from. Porting the feature carried over the fix for the settlement math faithfully. It did not carry over the lesson about the double-tap, because that lesson never lived in the code — it lived in my memory of a bug report from a different project, and memories don't come along for a copy-paste.
I only caught it because my test pass specifically tried tapping the button twice, quickly, and checked that exactly one record got created — instead of just checking that tapping it once worked. Fixed now with a small lock that disables one specific debt's button the moment its own request starts, so marking two different debts paid at the same time still works fine.
What still broke anyway
Even after all of that, a real user found something neither the audit nor the test pass caught: the invite page only offered email-and-password sign-in, and this particular setup requires confirming your email before you can finish creating an account. Someone clicking an invite cold has no confirmed inbox yet, and no way to finish joining without leaving the app to go check email first — exactly the wrong experience for the one screen where a good first impression matters most. Adding a one-tap Google sign-in option fixed it, and surfaced a smaller, second problem behind it: signing in that way always sends you back to the home screen, not back to the invite you started from, so the app now has to remember where you were headed and quietly send you back there once you're signed in.
The honest takeaway
Reusing a proven feature buys you correct math and a UX that's already been shaken out by real people — genuinely valuable, not nothing. It doesn't buy you a pass on rethinking every assumption for the new context, and it doesn't automatically carry over the lessons baked into bugs the original already learned the hard way — unless something more durable than the code itself travels along with it. The code moved over cleanly. The scar tissue didn't, until I wrote it down and made sure it did.
More in this series

Why I rebuilt a budgeting app from scratch (instead of using YNAB or Mint)
YNAB and Mint couldn't answer 'am I overspending right now?' So I built an app that could.

Inside the "Shared Journey": how MyFinCup lets two people trust one dashboard
One dashboard, two people, no mode switcher. How the database — not the app — decides who sees what.

Keeping 40+ AI coding sessions from turning into a Frankenstein app
Color tokens are the easy 20%. Here's what actually kept 43 AI sessions looking like one app.

The 404 that only strangers could see
It worked every time I tested it, and failed for everyone else. Here's why that's not a coincidence.