Back to Journal
Bridging

Designing a Cycle-Tracking App That Respects Both Partners

2026-04-0110 min read

There's a difference between being informed and being invited into understanding.

Most cycle-tracking apps treat the partner as an afterthought. A notification feed. A passive observer of data they didn't ask for and don't know how to interpret. Slap on a "share with partner" toggle, send a push notification that says "Day 14," and call it a feature.

But if you've ever been that partner — staring at a number on a screen, unsure what it means or what to do with it — you know that information without context isn't intimacy. It's surveillance with a friendly UI.

After the Dobbs decision overturned Roe v. Wade, period tracking became a privacy minefield. Users deleted apps. The FTC investigated data practices. Suddenly, the question wasn't just "how do we share cycle data?" It was "should we share it at all?"

The answer isn't to stop building these apps. It's to build them differently.

Ocean Drop is my attempt at that — a cycle-awareness app where both partners have a role, but neither has access they didn't consent to. Designing for two people who need fundamentally different things from the same data turned out to be one of the hardest UX and architecture challenges I've encountered. Here's how I approached it.

Why Do Most Period Trackers Fail the Partner?

The landscape is crowded. Clue, Flo, Cycles, DuoSync — dozens of apps tracking the same biological data. Most of them were designed for a single user, with partner features bolted on after the fact. The architecture tells you everything: a user table, a sharing toggle, and a read-only view.

The fundamental mistake is treating the partner as a viewer instead of a participant.

What both partners actually need looks radically different:

She needs control. Privacy. Autonomy over her own biological data. The ability to share selectively — not everything, not always, but when she chooses.

He needs context. Understanding. Actionable awareness that helps him show up better — without feeling like he's monitoring her.

This asymmetry isn't a design flaw. It is the design challenge. And most apps pretend it doesn't exist because symmetric experiences are easier to engineer.

The deeper issue is trust. FTC research shows users consider government and law enforcement access to period data "unacceptable." Period tracking apps are classified as "lifestyle" — not "health" — which means HIPAA doesn't apply. Your cycle data has fewer legal protections than a dental X-ray.

Building a partner feature on that foundation requires architecture that doesn't just say "your data is private." It has to enforce it at the database level, where no frontend bug or API misconfiguration can leak what shouldn't be shared.

How Do You Share Health Data Without Exposing It?

GDPR Article 9 classifies health data as a "special category" requiring explicit consent — not a pre-checked checkbox buried in a terms of service. Separate, informed, affirmative consent.

Ocean Drop takes this seriously. Three consent timestamps, each tracked independently:

ALTER TABLE profiles
  ADD COLUMN consented_to_health_data_at TIMESTAMPTZ;
ALTER TABLE profiles
  ADD COLUMN age_confirmed_at TIMESTAMPTZ;
ALTER TABLE profiles
  ADD COLUMN terms_accepted_at TIMESTAMPTZ;

Not a single accepted_terms: boolean. Three separate timestamps that create an audit trail. When did this user consent to health data sharing? When did they confirm they're 18+? When did they accept the terms? Each answer is independently verifiable.

The partner connection flow enforces this with a multi-step consent screen. When you enter a partner's invite code, you don't get connected immediately. First, you see exactly what will be shared:

  • Your current cycle phase (Deep, Swell, Crest, Ebb)
  • Your energy level from daily insights
  • Mood entries you mark as shared (private entries stay private)
  • Calendar events you mark as shared

Only after explicitly accepting does the connection finalize. No dark patterns. No "by continuing, you agree." A deliberate choice with full visibility.

Under the hood, the privacy layer runs on Supabase Row-Level Security. The key pattern is a SECURITY DEFINER function that resolves the partner relationship:

CREATE OR REPLACE FUNCTION get_my_partner_id()
RETURNS UUID AS $$
  SELECT partner_id FROM public.profiles
  WHERE id = auth.uid();
$$ LANGUAGE sql STABLE SECURITY DEFINER
   SET search_path = public;

This function runs with elevated privileges to look up the partner ID once, preventing the infinite recursion that happens when RLS policies on the profiles table need to query the same table to resolve relationships. It's a single point of truth for "who is my partner?" — and every other policy references it.

The RLS policies themselves are granular:

-- Period logs: partner gets read-only access
CREATE POLICY "partner_period_logs_read"
  ON period_logs FOR SELECT
  USING (user_id = get_my_partner_id());

-- Calendar events: partner sees only shared events
CREATE POLICY "partner_calendar_read"
  ON calendar_events FOR SELECT
  USING (user_id = get_my_partner_id());

-- Mood entries: partner sees only non-private entries
CREATE POLICY "Partners see shared mood entries"
  ON mood_journal FOR SELECT
  USING (
    is_private = false
    AND EXISTS (
      SELECT 1 FROM profiles
      WHERE profiles.id = auth.uid()
      AND profiles.partner_id = mood_journal.user_id
    )
  );

Partners can SELECT. Never UPDATE. Never DELETE. The database enforces this regardless of what the frontend does. Even if the app has a bug — even if someone decompiles the client and makes raw API calls — the database says no.

This is the philosophical point that matters: privacy isn't about hiding. It's about choosing what to reveal. The architecture mirrors what healthy relationships actually look like — selective vulnerability, not forced transparency. The code encodes a relational ethic: you share what you choose to share, and the system respects that boundary at every layer.

How Do You Design One App for Two Fundamentally Different Users?

Ocean Drop uses a role system — partner_male, partner_female, self_female — that drives far more than authorization. It shapes the entire experience.

What you see changes. Males see their partner's cycle data rendered in the context of "how can I support her today." Females see their own cycle as a personal wellness dashboard.

What you input changes. Only females log periods. A male partner literally cannot create a period entry — the UI doesn't render the option, and the database rejects it via RLS. This prevents a category of problems that sounds absurd until you've built multi-user apps: accidental or malicious false data injection.

Your AI companion changes. The AI chat system I built with Supabase Edge Functions serves two distinct personalities — Drop for male partners (supportive, actionable, translates cycle awareness into concrete suggestions) and Marina for female users (warm, scientifically grounded, treats her data as her own). Same architecture, same streaming pipeline, different presence.

How you're notified changes. More on this below.

The data fetching pattern is where the principle of minimal exposure gets concrete. When a male partner's app loads, the CycleProvider fetches exactly three fields from his partner's profile:

const { data } = await supabase
  .from('profiles')
  .select('id, last_period_start, cycle_length')
  .eq('id', partnerId)
  .single();

Three fields. Not the full profile. Not her subscription status. Not her invite code. Not her mood history. Just enough to compute what phase she's in and render the cycle ring on his home screen.

This isn't laziness — it's discipline. Every additional field in that query is a privacy surface. The principle: fetch the minimum data required to serve the user's actual need. If the male partner's need is "understand where she is in her cycle so I can show up better," then last_period_start and cycle_length is all the database should hand over.

When a male user connects to a partner after today's daily plan was already generated, the system detects that the cached cycle data is stale and regenerates the plan with fresh partner context. The app adapts to relationship changes without requiring a manual refresh.

Asymmetry isn't inequality. It's respect for different needs wearing the same interface.

Should Privacy Be Global or Granular?

Most apps offer one master toggle for data sharing. All or nothing. Connected or disconnected.

Ocean Drop takes a different approach: every mood entry has its own privacy flag.

{isFemale && profile?.partner_id && (
  <Pressable onPress={() => setIsPrivate(!isPrivate)}>
    <Text>{isPrivate ? '🔒' : '👥'}</Text>
    <Text>
      {isPrivate
        ? 'Only you can see this entry'
        : 'Your partner will receive a gentle tip based on your mood'}
    </Text>
  </Pressable>
)}

Lock icon means private. People icon means shared. The copy tells you exactly what each choice means — no ambiguity.

Why per-entry instead of global? Because emotional sharing isn't binary. Some days you want support. Some days you need space. Some entries capture something vulnerable you're processing alone. Others are an invitation for your partner to understand what's happening.

A global toggle forces a permanent decision about something that's inherently fluid. The per-entry pattern lets the app mirror how emotional intimacy actually works — you choose what to share, moment by moment, based on how you feel right now.

The technical implementation is clean: the is_private boolean on each mood entry drives the RLS policy. The database won't return private entries to the partner. Period. No frontend logic can override it. The privacy decision travels all the way from the user's thumb to the database row, and the database enforces it unconditionally.

This pattern — choosing what to reveal, moment by moment — mirrors what emotional intimacy actually looks like. It's not "I share everything" or "I share nothing." It's "I trust you with this, today."

How Do You Notify a Partner Without It Feeling Like Surveillance?

Notifications are the sharpest edge in a partner-aware health app. Get them wrong and the app becomes a monitoring tool. Get them right and it becomes a bridge.

Ocean Drop sends four notification types to partners: period_start, period_end, mood_shared, and phase_transition. The copy is gender-aware:

case "period_start":
  return isSenderFemale
    ? {
        title: `${senderName}'s period started`,
        body: "She may need extra comfort and support this week.",
      }
    : {
        title: "Period started",
        body: `${senderName} logged a period start.`,
      };

Notice the difference. When the sender is female, the male partner receives context: "She may need extra comfort and support this week." Not raw data. Not "Day 1 of menstruation." An empathetic frame that translates biology into relationship language.

The design principle: a notification should help the partner be a better partner, not a better monitor.

Notifications are fire-and-forget — they never block the primary user's action. If she logs a period start and the notification fails to deliver, her log still saves. Her experience is never degraded by the partner feature.

Rate limiting adds a safety layer: 10 notification requests per minute (covering normal usage without enabling notification spam) and 5 connection attempts per 5 minutes (brute-force protection on invite codes). The Expo push notification system handles delivery, and notification preferences let users toggle partner_activity on or off entirely, with quiet hours for overnight peace.

Realtime Sync as a Privacy Tool

There's an underappreciated property of Supabase Realtime: the subscriptions respect RLS. When a partner updates their display name, the change syncs across both profiles via Postgres change events — but only because the RLS policy permits that specific read. A private mood entry updated in realtime? The partner's subscription never receives it.

This means realtime sync is privacy-aware by default. No polling. No unnecessary data fetching. No wake-ups to check for changes the user isn't authorized to see. The database decides what crosses the wire, not the client.

The PartnerStatsCard — a side-by-side view of streaks and action counts — lives in this space. Shared progress without shared private data. You can celebrate together without exposing anything either person chose to keep private.

What I'd Build Differently

No shipped system is the final version. Here's what I'd change:

End-to-end encryption for mood content. The mood entries are currently plaintext in the database. The is_private flag controls access, but the data itself isn't encrypted at rest. For health data this sensitive, client-side encryption with key management per user would be the gold standard.

Local-first architecture with selective sync. Keep data on the device by default. Sync to the cloud only what's explicitly chosen for sharing. This inverts the current model — right now, data lives in the cloud and RLS controls access. A local-first approach would mean the data never leaves the device unless the user pushes it.

More granular notification preferences. Per-type toggles instead of a single partner_activity switch. Maybe she wants period phase notifications but not mood notifications. The current design is too coarse.

One-tap data export and deletion. GDPR's right to erasure should be frictionless. Currently it's possible but not one-tap. It should be.

The Architecture Is the Ethic

Building an app where two people share sensitive health data forced me to think about something I'd usually skip past: what values does this architecture encode?

Every technical decision in a two-sided system carries a relational philosophy. Forced transparency breeds resentment. Unlimited access breeds distrust. The architecture I landed on — granular consent, per-entry privacy, role-shaped experiences, minimal data exposure — encodes a specific belief: that intimacy requires chosen vulnerability.

There's something alchemical about this kind of design work. You're not just writing CRUD operations. You're transmuting a relationship philosophy into database policies and UI patterns. The code carries values whether you intend it to or not — the question is whether you're conscious of which values you're encoding.

This isn't just about cycle tracking. The same patterns apply to any app where two people share sensitive data — co-parenting apps, health companions, caregiver tools, collaborative wellness platforms. The fundamental challenge is the same: how do you build shared experiences on top of private data?

Start with consent. Design for asymmetry. Enforce privacy at the database layer, not the UI layer. And remember that the people using your app are navigating something more complex than your data model — they're navigating trust.


Building a multi-user app that handles sensitive data? I've shipped these patterns in production. Let's talk about your architecture.

I write about building apps with soul — architecture, privacy, and the patterns between code and consciousness. The full stack behind Ocean Drop is at The Stack I Use to Build Apps Solo.


Want the Full Workflow?

This is one piece of the system I used to ship Ocean Drop in 7 days.

The full playbook has the exact prompts, the pre-build checklist, the security audit workflow — all personalized for your stack via a single Day 0 prompt.

Get the Playbook — $17 →

Transmissions from the workshop

Code, consciousness, and the craft of building with soul. No spam, no filler — just signal.