Lovel Up
Private nightly-ritual PWA for one long-distance couple

Overview
A local-first, offline-capable PWA that gives one long-distance couple a shared nightly ritual: a deterministic "what should we do tonight" recommender, a browsable experience library, partner ideas, a shared memory timeline, and live + async cross-device co-presence with end-to-end-encrypted sync.
My role
Solo build. Designed, architected, and built end-to-end: product/UX, a "Celestial Glass" design system, the offline IndexedDB data layer, the E2EE append-log sync protocol, the deterministic recommendation engine, Email-OTP auth + Face ID/PIN app lock, and the PWA/notification stack.
The problem
A long-distance couple (Abuja ↔ Dublin) struggle to feel like they're sharing the same evening, night after night. The owner wanted something genuinely private, reliable offline, and emotionally intentional, not a cloud-hosted, account-based social app that treats a relationship as a feed.
The solution
IndexedDB is the system of record and a deterministic engine picks tonight's experience synchronously from on-device data, never blocked on network or AI. Cross-device connection is an append-log of ops that are AES-256-GCM-encrypted before being relayed through a "dumb" Supabase relay that only ever sees ciphertext plus routing/timing metadata, never names, photos, or answers. Live co-presence uses commit-reveal so a synchronised mood reveal is identical on both devices and neither partner can peek.
Key features
- Deterministic "tonight" recommendation engine (scoring → session-generator → templates) that runs synchronously from local IndexedDB, fully isolated from network/sync/AI so a sync or AI failure can never change or delay the pick.
- End-to-end-encrypted, offline-first cross-device sync: each mutation appends a Lamport-ordered op to a local append-log; ops are AES-256-GCM-encrypted only when relayed (couple code → PBKDF2 600k → HKDF keys), applied idempotently with per-op failure isolation and tombstone deletes.
- Live + async co-presence: synchronised mood reveal via commit-reveal (SHA-256 of answer‖nonce, so neither can peek), a live Play-Together co-play mode with snapshot-on-join, and "left this for you while you slept" async hand-off with delivery confidence.
- Content system: curated experience library + partner "ideas" inbox + recurring traditions engine + daily photo streak (encrypted photo bytes) + shared "Our Story" timeline with emoji reactions + insights dashboard.
- Relationship-context editor that feeds the deterministic engine (intensity ceiling caps bold picks; key dates drive anniversary/occasion boosts) and captures love languages, boundaries, and inside-jokes for copy and the optional AI layer.
- Private access + resilience: Supabase Email-OTP allowlisted to two emails + Face ID (WebAuthn PRF)/PIN lock that cryptographically wraps the couple code; content-free Web Push; encrypted local backup/restore as the only recovery path (the couple code is the sole key, no server reset).
Architecture & engineering
Three strict layers with hard seams: (1) local-first core: IndexedDB is the system of record and a synchronous deterministic engine is never awaited on I/O; (2) E2EE append-log sync: commit() writes the row and appends a plaintext op in one IndexedDB transaction, ordered by Lamport clock + deviceId, and the op payload is AES-256-GCM-encrypted only at relay time, applied idempotently; (3) real-time co-presence over an ephemeral Supabase channel whose live frames never enter the durable op log. The couple code is the sole E2EE root (PBKDF2 600k iters → HKDF into independently-salted roomId/dataKey/connSecret); the relay only ever sees ciphertext plus routing/ordering metadata and presence/timing. Next.js 16 App Router with a persistent provider tree (Auth → Lock → Sync → Live → Sky) that survives client navigation. Optional SDKs (Supabase, Anthropic) are dynamic-imported to keep the offline core bundle lean; the single AI seam (BYO key, off by default) always has a deterministic fallback.
The hardest problem
Two devices must produce a byte-identical shared reveal while a sync or AI bug can never break tonight's pick, solved by keeping the recommendation engine synchronous and fully isolated from network/sync/AI. Separately, the mood/energy answer space is tiny (7×5), so a naïve commit-reveal hash of the bare answer is brute-forceable, fixed by hashing answer‖nonce (SHA-256) and exchanging the nonce only at reveal. Sync was hardened over several audit passes: deterministic check-in ids to avoid a unique-index error that could wedge all sync for the room, a durable cursor that advances only on ordered backfill, and per-op failure isolation.
Results
Built solo over ~2 months through six phases, three feature iterations, and four documented audit passes; deployed on Vercel and in active daily use. A 9-file Vitest suite covers the engine, crypto key derivation, sync merge/apply, commit-reveal, catalogue integrity, app-lock key-wrapping, identity, and a full relay encode→decode round-trip.
Key learnings
Hard architectural seams paid off: keeping the deterministic core synchronous and isolated made strong correctness guarantees tractable and stopped every later feature from leaking into the core. E2EE without accounts is a genuine trade-off: the couple code is the only key, so recovery has to be an encrypted export / re-pair, and a tiny answer space forced a nonce to make commit-reveal actually peek-proof. And a durable decision log (not just code comments) kept a long, multi-phase solo build coherent across sessions.
Curious how this works?
This one's a private build; request a walkthrough and I'll show you around.