Skip to main content

Syncing (Duo)

In cloud mode the plan belongs to a household, and users are linked to it. A second person joins with an invite code.

Sharing modelโ€‹

  • One household = one shared state (jsonb).
  • Members are linked through a membership table.
  • Access is enforced by Row Level Security โ€” only a member can read and change their household's state.
  • The state is end-to-end encrypted; the household's encryption key is shared with the invited partner via key exchange, so neither we nor third parties can read it.

Adding a partner (one tap)โ€‹

While the plan has only one adult, the dashboard shows an "Invite partner" card. One tap opens a sheet with the invite code (and an optional email invite). The second person creates an account and enters the code โ€” from then on you both see the same plan. The card disappears once the partner joins.

Granting access to the encrypted data happens automatically: when the inviter opens or focuses the app, their device seals the household key for the partner (an idempotent step, safe to repeat). No manual settings or admin actions are needed.

If the plan was created as "Just me" and the partner joined later with a code, the plan's profile doesn't have a second person yet. The dashboard then shows a "A second person joined the plan" card on both devices with a name field. Once the name is saved, the plan has two people and the monthly entry has separate columns โ€” otherwise you'd both enter contributions in one column and one person's save would replace the other's.

Until that happens, the partner sees a calm "Connecting you" screen โ€” the app checks the connection every few seconds and completes it on its own. If it takes a while, just ask the other person to open GrapeVest on their device.

Save with mergeโ€‹

The state is written as a whole, with optimistic concurrency control:

  1. The app remembers the updated_at marker of the state it last fetched.
  2. On save, it sends that marker. If someone wrote in the meantime, the markers diverge โ†’ a conflict is detected.
  3. The app then fetches the fresh state and merges: month-keyed maps (entries, snapshots) are combined, and the remaining fields are taken from the newer side by updated_at. This way "two people filling in different months" doesn't lose data.

Fetching and refreshingโ€‹

The state is fetched when you enter the household, live whenever your partner makes a change (a Realtime subscription on the household row), and when you return to the tab/window as a fallback. When a partner's change arrives, a short "Your partner updated the plan" message is shown.

If you currently have local, not-yet-synced edits, the remote state is merged with them (we don't overwrite them silently) โ€” using the same rule as a write conflict (month-keyed maps merged, the rest by the newer updated_at).

Changes are sent after a short delay (a burst of changes is one write). If the tab or app is closed before that, the change isn't lost: the device remembers it has unsent changes and, on the next start, merges them with the server state and sends them. Switching to another app (tab in the background) and signing out send changes immediately.

Leaving and deletingโ€‹

  • Leave the household โ€” revokes this account's access; when the last member leaves, the household (and its state) is removed so no orphaned data is left.
  • Delete account โ€” removes any household where you're the only member (GDPR). An account without a plan (e.g. removed by the partner) is deleted from the "Your plan" screen, with the same password confirmation.
  • Removed by the partner โ€” on its next start, the removed person's device clears its copy of the shared plan (empty plan). Nothing of the other person's data stays on it, and it can't seed a new household with it.