5 min read

Inside the "Shared Journey": how MyFinCup lets two people trust one dashboard

Here's the requirement that sounded simple and wasn't: my wife and I needed to look at the same dashboard.

Not "sync our data" after the fact. Not two separate accounts we compare over dinner. The same running balance, the same calendar, the same category breakdown — no matter which of us opens the app, and no matter which of us actually logged a given expense. No "Couple Mode" toggle. No screen that asks whose data you want to see. One truth, two people looking at it.

The tempting way to build that is inside the app itself — some flag that says "if these two people are linked, merge their data before showing it." I didn't build it that way, and it's the one decision in this whole project I'd defend hardest to anyone building something similar.

Push the decision into the database, not the app

Every table holding real financial data in MyFinCup — accounts, transactions, budgets — has an optional "couple" field. Empty means it's a personal record. Filled in means it belongs to a shared journey. There's no separate shared-mode version of anything. An account you created before you ever linked up with a partner doesn't get copied or migrated when you do link — it just keeps existing exactly as it was, and a layer of permission gets placed on top.

That's what makes "no mode switcher" actually true: linking two people together doesn't touch a single row of existing data. It only changes who's allowed to read it.

The mechanism is a built-in feature of the database itself (I use Supabase, which is Postgres under the hood) called Row-Level Security — rules that live inside the database and decide, on every single request, whether a given row is even visible to the person asking. When my wife links her account to mine, she doesn't get a copy of my accounts, and no code anywhere runs a "merge" step. The database itself just starts allowing her to see rows it wouldn't have shown her a moment earlier.

Here's the part I'd defend hardest: put that permission check in the database, not in your app's code. If "can my partner see this" lives as a rule scattered across your backend functions, every single feature that touches that data has to remember to apply it — forever, including something you build eight months from now, late at night, half paying attention. If the check lives in the database itself, it applies automatically to every request, including ones you haven't written yet. I didn't fully believe this until I shipped the bug that convinced me.

The bug that taught me the hard way

Early on, whenever any screen in the app needed to answer "who is my partner," it grabbed the answer from the partner's profile info — basically, "if I've already loaded my partner's profile, use their ID from that; if not, act like there's no partner."

That sounds harmless. In practice, loading someone's profile and confirming "these two people are linked" are two separate steps that happen a fraction of a second apart. Almost every time, both finish fast enough that nobody notices the gap. But every so often, a screen would render in that in-between moment — the link was real, but the partner's profile hadn't quite finished loading yet — and the app would quietly decide "no partner" and move on.

Here's what made it genuinely nasty: it didn't fail the same way for both of us. My wife could open the dashboard and see everything — my accounts, my transactions, our shared balance, all there. I'd open the exact same dashboard at the exact same time and see only my own stuff. No error. No spinner stuck forever. Just a normal-looking dashboard quietly showing me half our actual financial life.

I only found out because she said, "I don't see your loans." My first instinct was to go debug the loans screen — when the real problem had nothing to do with loans at all. The app simply didn't have a reliable way of knowing who my partner was, and every feature built on top of that shaky foundation inherited the same flaw.

The fix was to stop asking "has the partner's profile finished loading?" and instead always check the one place that answers "are these two people linked?" directly — the record created the moment two people connect, which doesn't wait on some other piece of information loading first. Once every screen pulled the partner's identity from that one dependable place, this stopped being possible — there was no more half-loaded state left to get caught in.

The balance had the same disease, for a different reason

Somewhere else in the app, the "All accounts" total didn't actually match what you'd get by adding up each individual account yourself — like your banking app showing "Total: $500" up top, while the two accounts underneath read $210 and $210, which add up to $420, not $500.

The cause: I was calculating that total two different ways in two different places and just trusting they'd agree. One method took last month's total and adjusted it by this month's income and spending. The other recalculated everything fresh from the full history. Nine times out of ten, both landed on the same number. For a handful of older transactions missing a small piece of information, they quietly disagreed — and nothing in the app had ever decided which one was "correct," so it just showed whichever finished loading first.

The fix, in plain terms: stop having two formulas for the same number and hoping they stay in sync. There's now exactly one way the app calculates a balance, used everywhere, for a single account and for the "All" total alike. One method can't disagree with itself.

What "shared" actually means, in practice

Both bugs trace back to the same root cause: somewhere, the app had two different ways of answering one question, and trusted them to agree. They didn't. The fix, both times, was the same idea — pick one dependable source for each fact, and route everything else through it instead of letting each screen figure it out on its own.

Put together, here's what "shared" means once two people link up in MyFinCup: each account still technically belongs to one person, but a linked partner is allowed to look at it — nothing copied, nothing merged, just visible to one more person now. Every transaction still remembers who logged it, but whether your partner can see it is decided in exactly one place, not re-decided by every screen that happens to show transactions. And if two people ever unlink, nothing gets deleted or duplicated — the person who started the journey keeps everything, the person who joined keeps what they personally added, and the underlying data never changes through any of it. Only who's allowed to look at it does.

The takeaway

This was never really "syncing two people's data together." It's one shared source of truth, built from day one to safely answer "who's allowed to see this" in a single, dependable place — so the app itself never has to make that judgment call fresh on every screen, and can't quietly get it wrong by forgetting to check.

My wife still doesn't know most of this happens under the hood. To her, it's just a dashboard that's always right, on both our phones, at the same time. That's the whole point.

In the next post, I'm pulling the curtain back on something less about the database and more about process: how I keep 40+ sessions of AI-generated screens looking like they were designed by one person, instead of drifting into a Frankenstein app one prompt at a time.