Mobile App · iOS
P31
Status
Live on App Store
Stack
[ 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
SECTION 01 — DESIGN 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.
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.
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.
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.
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 02 — DISTRIBUTED 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 03 — PAIRING PROTOCOL
Six lines of code and a 6-character key lock two devices to one couple
- 01
Partner A signs up
Supabase Auth (email/password or magic link). A profile row is created in public.users with couple_id = NULL.
- 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.
- 03
Partner A shares the code
In person, iMessage, WhatsApp — code only. No link. No server-initiated invite.
- 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.
- 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.
- 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 04 — ENGINEERING 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 05 — CODE 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.