store-listing

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

DataWhere it comes fromWhere it goesOptional?
Email address, passwordSign-up / sign-in (SignInScreen, SignUpScreen)Supabase AuthRequired
Display name, profile photoEditProfileScreen (photo via expo-image-picker)Supabase (DB + storage)Optional
Journal entries — title, body, mood, tags, notebookEntry editorSupabase Postgres (RLS)Core function
Notebooks, goals, goal check-insNotebooks / Goals screensSupabase Postgres (RLS)Core function
Sleep hours per nightSleepEntryScreenSupabase Postgres (RLS)Optional
Dreams and dream stepsDreamPro screensSupabase Postgres (RLS)Optional
Entry text sent for AI reflection; JQ chat messagesJournal Analysis, JQ chatHiveJournal backend → OpenAIOnly on explicit user action
Entry title + body sent for automatic mood detectionSaving an entry without a mood only if the user enabled auto-mood (off by default)HiveJournal backend → OpenAIOpt-in — off by default; see the note below
Workout Window check-in photoWorkoutWindowCheckInScreen (camera/library)Supabase storage bucket workout-checkinsOptional feature
Workout Window check-in coarse coordinatesexpo-location, only if permission grantedHiveJournal backend, stored on that check-inOptional; denying it still submits the check-in
Session tokenSupabase AuthStored on device in AsyncStorage; sent as a bearer tokenRequired
Turnstile challenge token + IPTurnstileWidget WebView at sign-inCloudflareOnly when a site key is configured
Server-side request logs (IP, timestamps, errors)AutomaticSupabase / Railway infrastructure logsAutomatic

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

ProcessorRoleLocation
Supabase, Inc.Auth, Postgres, file storageUnited States
RailwayBackend API hostingUnited States
Vercel, Inc.Web app + share linksUnited States
OpenAIAI reflections and JQ chat responsesUnited States
CloudflareTurnstile bot protection at sign-inGlobal

⚠️ 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 /privacy before 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 typeCategoryPurposesNotes for the form
Email AddressContact InfoApp FunctionalityAccount identity
NameContact InfoApp FunctionalityOptional display name
PhotosUser ContentApp FunctionalityProfile picture; Workout Window check-in photo
Other User ContentUser ContentApp Functionality, Product PersonalizationJournal 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 SupportUser ContentCustomer SupportOnly if a support inbox is wired up — omit if not
User IDIdentifiersApp FunctionalitySupabase auth.uid()
Precise LocationLocationApp FunctionalityOptional, Workout Window check-in only. Declare Precise, not Coarse: Location.getCurrentPositionAsync({}) returns full GPS precision
Crash Data / Performance DataDiagnosticsDo 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:

QuestionAnswer
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 typeCollectedSharedRequired/OptionalPurposes
Personal info → Email addressYesNoRequiredAccount management
Personal info → NameYesNoOptionalAccount management, App functionality
Personal info → User IDsYesNoRequiredAccount management, App functionality
Photos and videos → PhotosYesNoOptionalApp functionality
Files and docsNo
App activity → Other user-generated contentYesNoRequiredApp functionality, Personalization
Location → Approximate / Precise locationYesNoOptionalApp functionality (Workout Window check-in only; declare Precise)
Health and fitnessNoSleep hours are journal metadata; no Health Connect integration
MessagesYes → declare as Other in-app messagesNoOptionalApp functionality (JQ Bridge messages, encouragement Drops)
App info and performance → Crash logs / DiagnosticsNoNo crash reporter in the build
Device or other IDsNoNo advertising ID, no device fingerprinting
Financial infoNoNo 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:

PermissionSourceShipped?
ACCESS_COARSE_LOCATIONexpo-location plugin
ACCESS_FINE_LOCATIONexpo-location plugin✅ — matches the Precise declaration on the form, because getCurrentPositionAsync({}) requests full precision
CAMERAapp.json
INTERNETExpo core
MODIFY_AUDIO_SETTINGSexpo-av✅ — playback routing
SYSTEM_ALERT_WINDOW, VIBRATEReact Native core
RECORD_AUDIOwas injected by expo-image-picker❌ blocked (tools:node="remove") — the app has no recording feature
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGEanother 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:

  1. Row-level security is enforced by the database, not the client. Every journal_entries row is scoped to auth.uid() by a Postgres policy, so a compromised or malicious client still cannot read another user's entries.
  2. 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:

  • /privacy lists 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.

Privacy And Data Safety — Docs | HiveJournal