以 React Native 打造 MoodMO 本地優先情緒日誌:繁瑣同步細節幾乎令開發功虧一簣
Built a local-first mood journal in React Native — and the boring sync parts nearly broke it
開發者以 React Native/Expo 為自己打造 MoodMO,一款本地優先的情緒日誌,目前只在 iOS App Store 上架。登入後可選用 Supabase 同步,但上傳與下載分開操作;下載會以雲端版本覆蓋相同 ID 的本機項目,未先上傳的離線修改可能被覆蓋。
After tense conversations, I used to open Notes, type half a paragraph, and delete it.
Not because the feeling wasn't real—because Notes felt like the wrong container. Too easy to sync into places I didn't want. Too unstructured to help me see patterns later. And every "mood tracker" I tried pulled me toward something else: emoji grids that felt like homework, couple apps that need two people online, or chatty AI coaches that wouldn't leave me alone.
What I actually needed was quieter:
A private place to write emotions that are usually about someone—without turning it into therapy homework or a social product.
So I built MoodMO ** for myself first, in React Native / Expo. It is currently published on the **iOS App Store —not Android. This post is about the problem, the lines I refused to cross, and a few sync/privacy decisions that burned real engineering time. If it sounds familiar, I'd love for you to try it on iOS and tell me what's still awkward.
Important: This is a personal journaling tool, not therapy, diagnosis, or crisis support. If you need immediate help, contact local emergency services or a trusted hotline in your area.
The problem I was solving
Most mood apps answer: "How do you feel on a scale?"
My nights looked more like: "I'm upset **with this person, about **this kind of trigger, and I don't know whether to talk today, this week, or just digest it alone."
That relationship context kept falling out of generic trackers. Couple apps solve shared journaling. AI coaches solve conversation. I wanted a single-player loop: record → later see whether things softened → optionally release words I no longer needed to keep staring at.
Privacy wasn't a marketing line. If diary body, tags, or voice notes ever felt like telemetry fodder or social content, I simply wouldn't use the app myself.
What I deliberately did not build
Saying no early saved me from shipping the wrong product:
- No medical framing — not a crisis tool, not CBT coursework.
- No conversational AI coach — no chat that follows you around (details in the next section).
- No couple co-writing / no in-app social feed — one person's relationship memory, not a timeline for others.
- Local-first by default — write without an account; cloud sync is opt-in later.
- Share cards that don't dump the diary by default — weather/garden chrome + a closing line; optional short user snippet only if they opt in—not entry bodies or people/trigger lists by default.
Those constraints shaped the UX metaphors more than any feature brainstorm did.
The AI I deliberately kept boring
Optional extras are batch-style only: mood forecast text, a short reflective "podcast-style" blurb, suggested next steps, and one review closing line. There is no in-app "paste your API key" screen.
When AI runs, it is gated by a Groq key present at build time. That key is an EXPO_PUBLIC_ env var, which means it ships inside the client bundle—anyone who unpacks the IPA can extract it. That is a deliberate trade-off for a small indie app with no backend proxy, not a claim that client-held provider keys are safe. If this grows, the right fix is a server-side proxy (and rotating whatever leaked). If the key is missing, those features fall back to on-device default copy.
When the key is present, calls send aggregates (counts, averages, trigger labels)—not full diary paragraphs—but that is still third-party processing. Same honesty class as the at-rest gap below.
How the product took shape
Weather for intensity. A garden for growth. People/trigger tags, a gentle "when might I talk about this?", a person timeline, "on this day," a ritual to release words, and a small home-screen widget (Soft Stack—garden/weather chrome only, no diary text).
I am not claiming the best architecture in the industry—only consistency with the problem I set out to solve.
How the boring parts actually worked
This is the section I almost skipped—and the one that made sync feel trustworthy enough to ask strangers to try the app.
Local-first storage (and an honest at-rest gap). Entries live on-device first as JSON in AsyncStorage (guest key vs per-user key). Guest mode never requires a network. Optional sync uses Supabase when you sign in. Auth session tokens use SecureStore; diary entries do not. Entries sit in the iOS app sandbox and rely on platform protections. I have not added application-layer encryption at rest. That is a known gap, not a claim.
Soft delete vs "gone forever." Soft delete stamps deletedAt (recycle bin can restore). Permanent delete inserts a tombstone id for signed-in users; pull/push filter those ids so another device cannot resurrect a purged entry. Tombstones live in a cloud table with no automatic expiry job today—the guarantee holds as long as those rows remain.
Upload and pull are separate actions—not one automatic pipeline. In Profile you choose upload (push) or pull (recover from cloud). Pull shows a confirm dialog that cloud wins on id collision.
Semantics:
- Pull → for each shared id, the server version replaces local; local-only ids stay.
-
Push → only rows whose revision field (
updatedAt—not a logical clock; a known simplification) advanced since the last successful sync. - Result → after you deliberately pull, devices look like the server for shared ids.
So yes: if you edit offline and pull without uploading first, cloud can overwrite you. That is a deliberate trade-off with a confirm prompt—not a silent magic sync. Clearer "you have unsynced local edits" UX before pull is still on my list.
Widget as hostile multi-process storage (iOS). Soft Stack never receives diary body, people names, or audio paths—only a whitelist snapshot (weather, growth stage, active-count hint, schema version). Publish runs when weather recalculates; clear runs on logout, serialized so a late publish cannot overwrite a clear. On iOS the snapshot is JSON in an App Group UserDefaults suite shared with the widget extension. (Android widget code exists in the repo; Android is not published.)
Telemetry we refuse. No Sentry. No sending diary body / tags / audio content to analytics. Cloud sync (when enabled) uses HTTPS and user-scoped access.
None of that is novel. It is the glue that made "private journaling" feel true enough that I kept using the app myself.
Where I still need your eyes
- Does the weather/garden language help—or get in the way after day three?
- Is recording fast enough for a bad evening?
- Do person tags + timelines help, or create pressure to "organize feelings"?
- Is the privacy story clear—especially once you know at-rest encryption and the client-held AI key are known gaps?
- What's missing in week two?
Critical notes are more useful than compliments.
Try it on iOS (and tell me what breaks)
iPhone only (App Store). Guest mode works offline; sync is optional after sign-in:
https://apps.apple.com/app/id6759703686
English and Chinese are in the app. Feedback in the comments—especially if you bounce—is exactly why I posted.
Thanks for reading a builder note instead of a launch blast.
來源:DEV Community · dev.to