For Long-distance couples · A Cycle-Aware Companion for Two

Better-Half

Cycle-aware daily guidance for long-distance couples.

DecisionEnforce consent in the database, not the interface: every sharing choice maps to a row-level security policy.

Instead of filtering shared categories in the interface.Cost: column rules live in SECURITY DEFINER functions, which concentrate trust in a few places.

Status
Private
Ownership
Solo · product and engineering
Evidence
Tested: automated tests, no usage data
Checked
Last verified

A two-user PWA for a long-distance couple: one partner's phase, mood and stress become shared context for daily guidance, with privacy enforced by the database, not the UI. Deployed for two private users; not a public product.

Role
Solo Builder · Product & Engineering
Timeline
Aug 2026 – Sep 2026 (last commit 16 Sep 2026)
Team
Solo
My part
Everything: product, design, and engineering. Solo build.
Stack and tools6

React 19 · TypeScript · Tailwind v4 · Supabase · Dexie · PWA

Illustrative mock-up with invented demo data: Person A's view with a cycle estimate, check-in, sharing dials and a suggestion; Person B's view with his own state, guidance, only the categories Person A shared, and a weekly retro.

Illustrative mock-up with invented demo data, not the private deployment.

TL;DR

Most cycle apps treat one partner as the user and the other as an outsider, or worse, treat her as the subject. Better-Half is built for two first-class users in a long-distance relationship: her phase, mood and stress, shared per category on her terms, drive daily guidance for her, for him and for the couple. Privacy is enforced with row-level security, checked by a 575-check policy matrix run against the hosted database. It is deployed for two private users and is not a public product.

575RLS policy checks passing (13 Aug 2026)
1,940Test Assertions (self-recorded)
220Guidance Items at v0.1.0
$0Monthly hosting cost on free tiers (self-recorded)
Chapters

Problem

Long-distance couples run on asynchronous context: how she is actually doing, whether now is a good time to talk, what would genuinely help today. Cycle-tracking apps hold half of that context but are built for one user. The partner is either locked out or handed raw clinical data with no guidance, and most "share with your partner" features make her data the product and him the audience.

Users

Two people: one couple, in a long-distance relationship, with patchy connectivity (hostel wifi, metro commutes). That is the whole user base, by design.

The contract that shaped everything: she is a first-class user, not a subject. Her data is hers; sharing is per category, under her control, and revocable instantly. His experience is keyed on his own context (sleep, stress, energy), so the app tells him how to show up, not how to monitor.

Decision

Trade-offs

  • Transparent scoring, no magic ML. Guidance ranking is a deterministic formula (feedback-weighted, recency-decayed) the code can explain. Predictions widen their confidence window when cycles vary, say "learning" under two cycles of data, and never claim fertility-planning precision.
  • Offline is the normal case. Every write queues locally (Dexie/IndexedDB) with per-field merge, dead-lettered failures and idempotent replay, so "saved" means saved.
  • Notifications pull you in, never spam. Discreet lock-screen text by default (never cycle details), quiet hours, a daily cap and per-kind dedupe, all enforced server-side.
  • Column-level gating costs trust concentration. Row-level policies cannot gate columns, so column rules live in SECURITY DEFINER functions. Workable, but it concentrates trust in a handful of functions that deserve their own audit.
OptionStatusWhy
Row-level security policy per sharing choiceChosenA category she has not shared is invisible at the SQL layer, and the policy matrix makes that testable.
Filter shared categories in the interfaceRejectedA client-side filter pretending to be a boundary is not one.
Client-side encryption for private journal entriesDeferredNext step, so even the database operator cannot read entries she marked private.

What shipped

The mock-up at the top of this page uses invented demo data, not a screenshot of the private deployment: Person A controls what Person B can see, and unshared categories never reach his view.

Three views share one engine. Her side: a quick daily check-in, a journal with per-entry sharing, a cycle dashboard and phase-aware suggestions. His side: a shorter check-in and guidance keyed on his own state, plus whatever she has opened via sharing dials. Us: side-by-side insights, an expectations exchange ("I can give / I need"), a weekly retro with appreciations, and scheduled partner messages.

The guidance engine drew from a 220-item content library at v0.1.0 (her/him/us × phase × mood band × stress band), later grown to 284. It filters by today's context, ranks by one-tap feedback (helped/meh/worse), enforces diversity against yesterday, and degrades honestly when a context cell is sparse. The cycle engine is pure, timezone-safe functions with an explicitly labelled ovulation estimate.

Open Book mode (full mutual visibility) exists only as a couple-level setting both must enable and either can revoke instantly.

Milestones M0 to M8 (onboarding, check-ins, journal, cycle engine, guidance, dashboards, notifications, weekly plans, couple layer) landed between the first commit on 3 Aug 2026 and the v0.1.0 tag on 12 Aug 2026.

Under the hoodArchitecture and implementation

React 19 + TypeScript + Tailwind v4 PWA on Vercel's free tier; Supabase (Postgres, email-OTP auth, Edge Functions) on its free tier; a transactional email provider's free tier for OTP mail. A pg_cron-scheduled Edge Function dispatches six nudge types through Web Push. No third-party analytics, trackers, CDNs or fonts.

The RLS matrix runs in rolled-back transactions against the hosted database, checking owner, partner-shared, partner-private, other-couple and anonymous access for every table, two-sided Open Book consent with instant revocation, no cross-couple leakage, and no back doors through helper functions.

Validation

  • 575 RLS policy checks, 575 passing, recorded in the project roadmap on 13 Aug 2026; the matrix grew milestone by milestone from 95.
  • A real leak found before release. A pre-release privacy review found plans indirectly revealing categories that had not been shared; the policy matrix is what made that contract checkable.
  • 1,940 unit test assertions across 121 test files (self-recorded at the time of writing; the repository has since grown), plus Playwright visual regression at 320px and 412px in both themes.
  • 220 guidance items at v0.1.0, with tests asserting dietary, situational and medical-claim constraints hold across the library.
  • $0 a month hosting on free tiers (self-recorded).

Limits and next

  • Deployed for two private users; not a public product. There is no public link, and its data is nobody else's business. That also means there are no usage metrics.
  • Next: client-side encryption for private journal entries, so even the database operator cannot read what she marked private.
  • Audit the definer functions. The SECURITY DEFINER functions that carry column-level rules deserve their own review ceremony.

Credits

Solo build: product, design and engineering. The image on this page is an illustrative mock-up with invented data; the real deployment stays private. How the sharing rules became a test suite is written up in Turning a privacy contract into a regression suite.

  • Product

    DeskTasks

    Your task list, pinned behind every window on the desktop.

    Decision: Pin the widget behind every window, not on top of them, and refuse attention: accountless, local-first, no dock icon.

    Solo · product, design and engineering

    LiveHosted v1.3.1 line is frozen; v2 alpha runs locallyEvidence: Tested

    0Failures · release gate run, 15 Sep 2026

  • Product

    ExperimentHub

    Self-hosted A/B testing with deterministic variant assignment.

    Decision: Let the statistics engine say no: return a sequential verdict such as keep running, not a bare p-value.

    Solo · product and architecture

    Local build · not deployedEvidence: Built

    ContinueSequential verdict at p = 0.0337 (synthetic demo)