← Back

Mobile App · iOS

P31

Status

Live on App Store

Stack

React NativeExpo SDK 54SupabaseSQLite

[ THE THESIS ]

A private couples app should be architected like a trust boundary, not a chat room.

P31 turns prayer, scripture, journaling, and voice notes into a two-person system where intimacy is protected by database rules, offline scripture, and realtime sync. The point is not adding social features to faith; it is proving that a mobile app can feel personal while the architecture keeps every couple's data locked to exactly two people.

LIVE · App Store · iOS · iPadOS · macOS · visionOS

Technical proof
11.4%
iOS conversion
views → download · organic
5.0★
App Store rating
early reviewers
31,102
offline KJV verses
SQLite · 0 network
RLS
on every table
scoped by couple_id

SECTION 01DESIGN COMMITMENTS

Three commitments that shape every table and screen

P31 is not a social app. It is built for exactly two people, and the architecture enforces it.

01

One account. One partner.

Every couple-scoped table keys off a single couple_id. A user cannot belong to two couples. Unpairing is explicit, timestamped, and routed through the Danger Zone flow.

02

Privacy over server convenience

Voice notes relay through Supabase Storage — they are not archived. Intimate audio becomes eligible for deletion the moment the partner's device downloads it.

03

Offline-first where it matters

The full 66-book, 1,189-chapter, 31,102-verse KJV ships inside the app as SQLite. Opening P31 in airplane mode still reads scripture.

04

Real-time where it matters too

Highlights, journal edits, and walkies fan out via Supabase Realtime. A verse you highlight in Genesis shows up on your partner's phone in seconds.

SECTION 02DISTRIBUTED SYSTEM TOPOLOGY

How the four layers cooperate

01 · Mobile client

Expo app with offline scripture and realtime couple sync.

Us · Walkie

expo-av .m4a · hold-to-speak · busy-mode queue

Bible · KJV

expo-sqlite · 31,102 verses · dual-color highlights

Journal

TenTap rich text · folders · realtime sync

Timeline

Love Clock · milestones · push reminders

supabaseClient + offlineQueue · session in iOS Keychain via expo-secure-store · newArchEnabled: true

02 · Supabase

Realtime WSS · HTTPS/TLS

Couple-scoped tables

voicemessages · bible_highlights · journal_folders / journal_notes · timeline_events / milestones

Row-Level Security

couple_id IN (SELECT couple_id FROM users WHERE id = auth.uid())

Storage bucket · voicemessages/<couple_id>/<timestamp>.m4a · ephemeral relay · server copy eligible for deletion after delivery

03 · Notifications · APNs via Expo · Resend SMTP

Push: "Your partner sent you a walkie." · Email for sign-up + milestones.

Delivery, not storage

SECTION 03PAIRING PROTOCOL

Six lines of code and a 6-character key lock two devices to one couple

  1. 01

    Partner A signs up

    Supabase Auth (email/password or magic link). A profile row is created in public.users with couple_id = NULL.

  2. 02

    App generates a 6-character pairing code

    Random, uppercase, collision-checked server-side. Stored as a pending invite tied to Partner A's user_id.

  3. 03

    Partner A shares the code

    In person, iMessage, WhatsApp — code only. No link. No server-initiated invite.

  4. 04

    Partner B signs up and enters the code

    The redeem RPC runs inside a Postgres transaction: it verifies the invite, mints a new couple_id, and stamps it on both users. Either both rows update or neither does.

  5. 05

    Every table unlocks at once

    Because RLS is scoped on couple_id, the moment both profiles share the same UUID, all four feature tables light up — walkies, highlights, journal, timeline — without a single feature flag.

  6. 06

    Unpairing is deliberate

    Triggered only through the Danger Zone flow. Uses pg_notify so both phones receive the separation event in real time. No accidental separation.

SECTION 04ENGINEERING HIGHLIGHTS

Four decisions I can defend line by line

OFFLINE-FIRST

The whole Bible ships inside the app

66 books · 1,189 chapters · 31,102 verses in expo-sqlite. Opening P31 in airplane mode still reads scripture. Highlights and notes layer on top and sync when connectivity returns.

0 API calls for Bible reads

PRIVACY

Ephemeral walkie relay

Audio uploads to voicemessages/<couple_id>/<ts>.m4a, a row lands, Realtime notifies the partner, they download, and the server copy becomes eligible for deletion. Intimate audio does not live on my servers long-term.

commitment, not slogan

SECURITY

RLS policy template · one line protects every table

Every couple-scoped table applies the same policy: couple_id IN (SELECT couple_id FROM users WHERE id = auth.uid()). Enforcement lives in Postgres. A compromised client still cannot leak another couple's data.

enforced at the DB

SECTION 05CODE PROOF

The one RLS policy that protects every couple table

Verbatim from the supabase/migrations/ folder.

supabase/migrations/0007_couples_rls.sql

sql

-- This one policy is applied to every couple-scoped table:
--   voicemessages · bible_highlights · journal_folders ·
--   journal_notes · timeline_events · milestones

CREATE POLICY "Couples can manage their own data"
ON public.voicemessages FOR ALL
USING (
  couple_id IN (
    SELECT couple_id
      FROM public.users
     WHERE id = auth.uid()
  )
);

-- Same shape, different table names. No JOINs. No client checks.
-- A compromised client cannot leak another couple's data — the
-- database itself refuses the query.