Privacy nutrition labels + Data safety answers
Both store privacy forms, answered field by field, with the evidence each
answer rests on. Written against the HiveJournal flavor of apps/mobile
as of 2026-07-30.
Companion policy: hivejournal.com/privacy
(source: apps/frontend/src/app/privacy/page.tsx).
The model, in one paragraph
HiveJournal mobile is a thin client over a Postgres row-level-security
boundary. The app holds no server credentials: it authenticates against
Supabase Auth, keeps the resulting session token in AsyncStorage, and every
read or write goes to either Supabase (under RLS policies that scope every row
to auth.uid()) or the HiveJournal backend on Railway (which re-checks the
bearer token). There is no analytics SDK, no crash reporter, no ad network,
and no attribution/tracking library in the binary — verified by grepping
apps/mobile/src and package.json for Sentry, Firebase, Amplitude, Mixpanel,
Segment and Google Analytics: zero hits. Nothing is collected for advertising,
and nothing is sold.
What that means for both forms: everything we declare is collected (it leaves the device) and linked to the user's identity (it's stored under their account), but none of it is used for tracking in Apple's sense — we never share it with a data broker or join it to third-party data for ads.
Inventory — what actually leaves the device
| Data | Where it comes from | Where it goes | Optional? |
|---|---|---|---|
| Email address, password | Sign-up / sign-in (SignInScreen, SignUpScreen) | Supabase Auth | Required |
| Display name, profile photo | EditProfileScreen (photo via expo-image-picker) | Supabase (DB + storage) | Optional |
| Journal entries — title, body, mood, tags, notebook | Entry editor | Supabase Postgres (RLS) | Core function |
| Notebooks, goals, goal check-ins | Notebooks / Goals screens | Supabase Postgres (RLS) | Core function |
| Sleep hours per night | SleepEntryScreen | Supabase Postgres (RLS) | Optional |
| Dreams and dream steps | DreamPro screens | Supabase Postgres (RLS) | Optional |
| Entry text sent for AI reflection; JQ chat messages | Journal Analysis, JQ chat | HiveJournal backend → OpenAI | Only on explicit user action |
| Entry title + body sent for automatic mood detection | Saving an entry without a mood only if the user enabled auto-mood (off by default) | HiveJournal backend → OpenAI | Opt-in — off by default; see the note below |
| Workout Window check-in photo | WorkoutWindowCheckInScreen (camera/library) | Supabase storage bucket workout-checkins | Optional feature |
| Workout Window check-in coarse coordinates | expo-location, only if permission granted | HiveJournal backend, stored on that check-in | Optional; denying it still submits the check-in |
| Session token | Supabase Auth | Stored on device in AsyncStorage; sent as a bearer token | Required |
| Turnstile challenge token + IP | TurnstileWidget WebView at sign-in | Cloudflare | Only when a site key is configured |
| Server-side request logs (IP, timestamps, errors) | Automatic | Supabase / Railway infrastructure logs | Automatic |
Not collected: contacts, calendar, health/fitness records (sleep hours are journaling metadata — the app does not integrate HealthKit or Health Connect), browsing history, advertising identifiers, purchases (no IAP in the app), audio recordings (Graphene only plays audio; there is no recording feature on mobile).
Third-party processors to declare
| Processor | Role | Location |
|---|---|---|
| Supabase, Inc. | Auth, Postgres, file storage | United States |
| Railway | Backend API hosting | United States |
| Vercel, Inc. | Web app + share links | United States |
| OpenAI | AI reflections and JQ chat responses | United States |
| Cloudflare | Turnstile bot protection at sign-in | Global |
⚠️ Policy gap the owner must close before submitting.
/privacy§5 lists only Supabase and Vercel as processors, and discloses OpenAI only in the Chrome-extension section (§12.2). The mobile app sends journal content to OpenAI through the backend whenever a user asks for a reflection or messages JQ. Both store forms require this to be declared, and the linked privacy policy must agree with the form. Add OpenAI (and Railway, and Cloudflare) as platform-wide processors in/privacybefore submission — a nutrition label that contradicts the linked policy is a rejection, and for a journaling app it's also the wrong thing to do to users.
Apple — App Privacy (nutrition labels)
App Store Connect → App Privacy. Structure: for each data type, declare collected? → purpose → linked to identity? → used for tracking?
Tracking answer is No for every single type: the app performs no tracking as Apple defines it (no linking to third-party data for ads/measurement, no data broker sharing). Therefore do not enable "Data Used to Track You", and the app does not need App Tracking Transparency.
Data Linked to You
| Data type | Category | Purposes | Notes for the form |
|---|---|---|---|
| Email Address | Contact Info | App Functionality | Account identity |
| Name | Contact Info | App Functionality | Optional display name |
| Photos | User Content | App Functionality | Profile picture; Workout Window check-in photo |
| Other User Content | User Content | App Functionality, Product Personalization | Journal entries, notebooks, goals, sleep logs, dreams. Product Personalization covers the AI reflections, which are generated from the user's own entries at their request |
| Customer Support | User Content | Customer Support | Only if a support inbox is wired up — omit if not |
| User ID | Identifiers | App Functionality | Supabase auth.uid() |
| Precise Location | Location | App Functionality | Optional, Workout Window check-in only. Declare Precise, not Coarse: Location.getCurrentPositionAsync({}) returns full GPS precision |
| Crash Data / Performance Data | Diagnostics | — | Do not declare — there is no crash reporter or performance SDK in the app |
Data Not Collected
Contacts · Health & Fitness · Financial Info · Browsing History · Search History · Purchases · Sensitive Info · Advertising Data · Usage Data (no analytics SDK) · Audio Data.
"Usage Data — none" is defensible but check it at submission. It relies on the app carrying no analytics SDK, which is true today. If a product analytics library lands before 1.0 ships, this label must change.
Privacy Choices to declare
- Data deletion: ✅ available in-app — Settings → Delete Account, which
removes entries, notebooks, goals, files and the account itself
(
/privacy§6.1). Apple checks this for Guideline 5.1.1(v); make sure the in-app path works in the reviewed build. - Account creation required: yes.
Google Play — Data safety form
Play Console → App content → Data safety. For each type: collected? / shared? / processed ephemerally? / required or optional? / purposes.
Global answers:
| Question | Answer |
|---|---|
| Is all data encrypted in transit? | Yes — HTTPS/TLS to Supabase and the Railway backend |
| Do you provide a way for users to request data deletion? | Yes — in-app (Settings → Delete Account) and by emailing privacy@hivejournal.com |
| Has your app been independently validated against a global security standard? | No |
| Is data collected by a third-party SDK? | No — the SDKs in the build (Supabase, React Navigation, TanStack Query, Expo modules) transmit only to our own backends; none collects independently |
"Shared" definition note. Play counts transfer to a service provider as
processing, not sharing — Supabase, Railway and Vercel are processors under
contract, so they are not "shared". OpenAI is the judgement call: it
receives entry text to produce a reflection at the user's request. The
conservative and defensible answer is to declare "Shared: No" while
disclosing OpenAI as a processor in the privacy policy — Play's own guidance
treats processing on the developer's behalf as not-sharing. If the owner
prefers maximum caution, declare Personal info → "Other user-generated
content" as shared for App functionality. Pick one and make /privacy say the
same thing.
Data types to declare
| Play data type | Collected | Shared | Required/Optional | Purposes |
|---|---|---|---|---|
| Personal info → Email address | Yes | No | Required | Account management |
| Personal info → Name | Yes | No | Optional | Account management, App functionality |
| Personal info → User IDs | Yes | No | Required | Account management, App functionality |
| Photos and videos → Photos | Yes | No | Optional | App functionality |
| Files and docs | No | — | — | — |
| App activity → Other user-generated content | Yes | No | Required | App functionality, Personalization |
| Location → Approximate / Precise location | Yes | No | Optional | App functionality (Workout Window check-in only; declare Precise) |
| Health and fitness | No | — | — | Sleep hours are journal metadata; no Health Connect integration |
| Messages | Yes → declare as Other in-app messages | No | Optional | App functionality (JQ Bridge messages, encouragement Drops) |
| App info and performance → Crash logs / Diagnostics | No | — | — | No crash reporter in the build |
| Device or other IDs | No | — | — | No advertising ID, no device fingerprinting |
| Financial info | No | — | — | No in-app purchase |
Permissions in the generated AndroidManifest
Cleaned up in #1709. Read off a real npx expo prebuild --platform android
run, not off app.json — several permissions come from library plugins rather
than from our config:
| Permission | Source | Shipped? |
|---|---|---|
ACCESS_COARSE_LOCATION | expo-location plugin | ✅ |
ACCESS_FINE_LOCATION | expo-location plugin | ✅ — matches the Precise declaration on the form, because getCurrentPositionAsync({}) requests full precision |
CAMERA | app.json | ✅ |
INTERNET | Expo core | ✅ |
MODIFY_AUDIO_SETTINGS | expo-av | ✅ — playback routing |
SYSTEM_ALERT_WINDOW, VIBRATE | React Native core | ✅ |
RECORD_AUDIO | was injected by expo-image-picker | ❌ blocked (tools:node="remove") — the app has no recording feature |
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE | another Expo plugin adds them | ❌ blocked — no-ops on Android 13+ and Play flags broad storage permissions |
The three blocked entries appear in the pre-merge manifest with
tools:node="remove" and are stripped when Gradle merges manifests. Confirm
against the first real AAB (bundletool dump manifest) before submitting the
Data safety form — a permission that survives the merge has to be declared.
Sensitive-content note for both reviewers
A journal is among the most sensitive content categories a phone app can hold. Two things are worth stating plainly in review notes, because they're genuinely true here and they defuse the obvious reviewer question:
- Row-level security is enforced by the database, not the client. Every
journal_entriesrow is scoped toauth.uid()by a Postgres policy, so a compromised or malicious client still cannot read another user's entries. - All AI is now user-initiated. Reflections and JQ replies are generated only when the user taps to request them, and automatic mood detection is opt-in and off by default — see below.
✅ Automatic mood detection is opt-in (default off) — resolved (GH #1708)
Fixed 2026-08-02. POST /api/journal (and the entry-update path) now call
analyzeTone() — the OpenAI request — only when the user has explicitly
enabled auto-mood (preferences.ai_mood_detection, gated by aiMoodOptedIn()
in apps/backend/src/routes/journal.ts).
The default is off, so saving an entry without a mood does not send anything
to OpenAI; a blank mood simply stays blank. Users turn it on under
Settings → AI Features, and the flag is served/toggled via
GET|PUT /api/user/ai-mood-preferences.
Disclosure now agrees across surfaces:
/privacylists OpenAI, Railway, and Cloudflare as platform-wide processors, and an "Optional AI features" section explains auto-mood is opt-in.- In-app: the Settings → AI Features toggle states exactly what turns on.
- The store table above marks the row Opt-in — off by default.
Because the default is off and the opt-in is explicit, "AI only runs when you ask for it" is now a true statement and safe to use in review notes / copy. Remaining (nice-to-have, not a store blocker): mirror the Settings toggle into the mobile app so a mobile user can opt in without visiting the web app — tracked for the mobile contributor lane.