Moorwell
Pages
Look
Open Moorwell

Privacy — Moorwell

Last updated: 2026-09-08 · Applies to version 0.12.0

This is written to be true, not to be reassuring. Every claim below corresponds to something checkable in the source, and the file references are there so you can check it.


The short version


What Moorwell stores, and where

Everything is stored on your device first. Sync is an addition, not the mechanism.

WhatOn your deviceSynced (only if signed in and sync is on)
Mood check-ins — a 1–5 score, a category, a timeyesyes
What you wrote in a check-inyes — stored on this device so you can read it backnever
Your goal and why it mattersyesonly as ciphertext we cannot read
Sleep diary — how rested you felt and optional timesyesyes
The note you add to a nightyesonly as ciphertext we cannot read
Groundwork steps you recordedyesyes
The note you add to a stepyesnever
If-then plans you wroteyesonly as ciphertext we cannot read
WHO-5 answersyesyes
"Did that help?" answers, and the situation tags at the timeyesyes
Your safety planyes, encryptedonly as ciphertext we cannot read
Which recognition notes you've seenyesno — device-local
The discreet-mode settingyesno — device-local
The name you asked to be called, if you set oneyesno — device-local
A gender, if you state one — including "rather not say"yesno — device-local

Nothing in the right-hand column happens in this version, because syncing needs an account and no account is created — see Accounts, below. It says what would sync if one existed, and it is written out rather than left blank so that there is something to hold us to on the day it starts.

The name is a name, and nothing else reads it

If you tell the app what to call you, that name is used in one place — the greeting at the top of the home screen — and stored in one place, on this device. It is never synced, and "Erase everything on this device" clears it.

It is deliberately kept out of four things, because a name in any of them would be doing work a name should not do: the risk detector (which reads the raw words you write, and a name is a word that could collide with one it watches for), search and matching, the context passed to any AI model, and usage data. Setting a name changes what the app calls you and nothing about what it does.

The gender field is stored and read by nothing

Settings has an optional gender question. It has five answers — Man, Non-binary, Woman, Something else (with a box for your own words if you want one), and Rather not say — and skipping it entirely is the default.

It shares a settings row with the name — You — and that is the only thing the two share. They are two separate stored values: you can give one without the other, answering one never fills in or changes the other, clearing one leaves the other alone, and either can be left unanswered for good. They are asked as two questions on one screen, not as one question with two parts.

Nothing in the app reads it. Not the greeting, not what you are offered, not how anything is worded, not what the app says back to you. It is kept out of the same four things the name is, and for the same reasons: the risk detector, search and matching, the context passed to any AI model, and usage data. Two people writing the same sentence get the same answer from this app, and stating a gender does not change that. If we ever want it to do something, that will be a decision made in the open, and this paragraph will have to change first.

It is not a database column. It lives in this device's own settings store, alongside your theme and text size, so there is nothing about it in the tables that sync — not in the app's schema, not in the server's, not in the sync rules. It cannot leave this device even if you turn sync on, and "Erase everything on this device" clears it.

Rather not say is stored as an answer, not as an empty value. Declining is a reply, and the app records it so it stops treating the question as open. Clearing the field is a separate action, and puts it back to never having been asked.

The check-in note is the important row

What you type into a check-in is stored on this device, in the app's own database, so that you can read your own words back later — the check-in history shows them. It is not synced and not logged, and no other app can read it. It leaves this device only if you turn on cloud help, which is off unless you turn it on — see below.

The learning memory is a separate thing and it does not record the words — only situation tags like lowEnergy, night, quick (lib/ai/memory.dart; a test asserts that only tokens from a closed vocabulary can enter that record).

The note is also the one thing you write that could ever leave this device in a form somebody could read, and only if you turn on cloud help — see below, where the state of that in 1.11.0 is set out. Other things you write can reach a server, but only sealed: the row above is the readable case. Turning cloud help on does not change what is stored here.

What is encrypted before it syncs

Changed 2026-08-10, and it is a real change rather than a wording one. Until then the only thing encrypted anywhere was your safety plan, and eight further free-text columns reached the server readable — including why your goal matters. They are now sealed on your device with the same AES-256-GCM, so what arrives is ciphertext: your goal and its meaning, your supporting goal, previous goals you have set, the note on a night's sleep, and both halves of every if-then plan.

Two notes you write are handled the other way and simply never go: what you wrote in a check-in, and the note you add to a groundwork step. For the check-in note the promise is never, not encrypted — see above. For the groundwork note nothing in the app ever shows it back to you, so there is nothing for a backup to restore and no reason for it to leave.

The honest limit. The key is generated on this device and stays here, and there is not yet a way to carry it to a second one. So today this means the operator cannot read what you wrote; it does not yet mean you could restore it onto a new phone. If you lose the device you lose the key, and encrypted is encrypted. lib/data/db/upload_filter.dart states this column by column.

The safety plan

Encrypted with AES-256-GCM on your device before it is stored (lib/data/sync/encryption_service.dart). If you sync, what reaches the server is ciphertext. The key is derived from your password, or — for Google sign-in — is a random key backed up to your own Google Drive private app folder, which we cannot read.

If you lose the key and the recovery phrase, the safety plan is unrecoverable. That is what end-to-end encryption means, and we would rather say so than quietly keep a copy.


Permissions

Internet access (Android INTERNET). For optional sign-in, optional sync and optional cloud help — all three off unless you turn them on — for error reports, which are on by default (see Error reports), for the launch-time check described below, and in a future version for fetching a corrected model file. Anonymous usage counts are on by default and are not a use of this permission in this version: there is no uploader, so nothing is transmitted. The app opens, works and answers with the permission denied.

Two more, and both are reminders (Android SCHEDULE_EXACT_ALARM and RECEIVE_BOOT_COMPLETED). A reminder also needs POST_NOTIFICATIONS, which your phone asks you about the first time you turn a reminder on — that one Moorwell does not declare; it arrives from the notifications library's own manifest and merges in at build time, so it belongs to the six below rather than to the three Moorwell asks for. All three exist so that a reminder you set arrives at the time you chose, survives a restart, and is allowed to appear on your screen. None of them transmits anything: the schedule is on your phone and your phone rings it. Refuse them, or leave reminders off, and every other part of the app is unchanged.

Vibration is deliberately not one of them, and it is the one permission named on this page that Moorwell does not have. The taps you feel are the ones Android hands to any app for free. The notifications library declares VIBRATE in its own manifest; Moorwell's manifest subtracts it, so it never reaches the app you install, and that removal is asserted in test/haptics_test.dart. It is named here because a permission you can read about on this page and not find on your phone is worth explaining — it is not one of the nine.

Six more come from the libraries Moorwell is built on, and are listed here because they appear on your phone's permission screen whether or not we mention them. POST_NOTIFICATIONS, above, is the first of the six; the other five are below. Of the six, only POST_NOTIFICATIONS ever puts a question in front of you, and only because you turned a reminder on. Moorwell never asks you for the other five, and none of the six is a way to reach anything the rest of this page says we do not touch:

PermissionIt is there becauseWhat it does here
ACCESS_NETWORK_STATEthe connectivity libraryLets the app see whether there is a connection, so it can wait instead of failing. It does not say what network, or where.
WAKE_LOCKthe download libraryLets a download you started finish instead of stopping when the screen sleeps.
FOREGROUND_SERVICEthe same download libraryThe same job, when Android requires it to be visible rather than hidden.
USE_BIOMETRICGoogle sign-inOnly ever used by Google's own sign-in screen, and only if you choose to sign in. Moorwell never reads a fingerprint or a face, and never receives one.
USE_FINGERPRINTthe same, for older phonesThe same thing, under the older name.

And that is the whole list: nine — three Moorwell declares in its own manifest, and six the libraries merge in. test/privacy_permissions_test.dart reads the manifest Moorwell declares and fails if it ever grows a permission this page does not name.

Fetching the feeling reader, once, on a new device

This is a third thing you cannot opt out of, and it started on 2026-09-07. It is a plain file download and it is worth being precise about, because it is the largest single thing this app does over a network.

Until 2026-09-07 the emotion classifier — the model that reads a check-in note for what it is mostly about — was inside the app you installed, all 125,397,543 bytes of it. It is not any more: the download shrank from 267.5 MB to 178.0 MB, and the model is fetched instead, once, and then stays on the device.

What that request is. One GET to huggingface.co for one published file. It carries no account, no device identifier, no note, no mood, nothing you have written and nothing derived from it — it is the same request anybody downloading that file makes, and the file is the same for every person who installs Moorwell. Hugging Face can see that some device asked for a public model file, which is what a web server sees when anyone downloads anything.

When. After you are in the app, on Wi-Fi only, and only when the device has room to spare — the same storage margin the app applies before any model download. Never on cellular unless you ask for it on that model's own page. Not during startup, and it cannot delay or break the app opening.

If it never arrives, nothing is missing that you were promised. No network, a blocked host, a full disk, a checksum that does not match: all of them leave the app running exactly as it does today, reading your notes with the rules it ships with. The reading is a little less precise about which feeling and identical about everything else, including every crisis and safety surface, which never involve a model at all.

The bytes are checked before they are used. The file's SHA-256 is pinned in the app; a file that does not match is deleted rather than used.

Why it is not behind the consent switch, which is a decision rather than an oversight: the owner's ruling of 2026-08-22 is that this reading is part of the app rather than an addition to it, and a switch implies the app is lesser without something it is not lesser without. What is behind consent is anything that sends your words anywhere — see If you turn on cloud help — and this sends nothing.

Contacting our server on every launch

This is one of the two things here you cannot opt out of — the other is error reports, below — and it started on 2026-08-11. When Moorwell opens it asks our own Supabase database — no third party — up to three questions:

  1. which features are switched on,
  2. whether there is corrected wording for any screen,
  3. whether there are extra activities or reflections to add.

In practice that is one request, not three. The second and third travel on a channel that will not open without a pack-signing key compiled into the build, and no key ships in this checkout — the code returns before it opens a connection (publicKeyBase64.isEmpty in lib/data/remote/remote_pack_channel.dart). Whether some particular build was given one is a build-time value this page cannot prove, so three is written as the ceiling rather than as the number.

What it sends: nothing about you. No account, no identifier of any kind, no device name, and nothing you have written. They are read-only requests; the app asks and does not tell.

What comes back is checked, and the two channels are checked differently — worth separating, because this page used to describe them as one. The feature-flag answer is not signed: it is a list of names, and what protects you is the shape of it. The app discards any name it does not recognise, and that answer can only ever turn something off. The wording and activity packs are signed, and are verified on your device against a key compiled into the app before a single line of either is used. Neither channel can replace the crisis screens or the safety plan — those are fixed in the app and are not something a server may change.

What it unavoidably reveals, said plainly because a page like this is worth nothing if it only lists the comfortable facts: any request over the internet arrives somewhere with an IP address attached, and roughly when it arrived. So our server can tell that an Moorwell install opened, and from roughly where. It cannot tell which install, cannot tell it apart from any other, and has nothing to attach it to — but "nothing at all" would be the wrong word and we are not going to use it.

Why it is not a switch. The first of those three is how a feature gets turned off without waiting for an app-store update. If we find that something in Moorwell is making people's lives harder, that check is how it stops — and making it optional would mean the people we could not reach are exactly the ones still using the thing we wanted to withdraw. A safety switch that only protects the people who opted into being protected is not a safety switch.

What can actually be withdrawn, by name. Until 2026-08-14 this paragraph described a capability the app did not have — the check happened and switched nothing. It does now, and a list is checkable where "a feature" is not. Six things can be turned off from our side: the on-device model download, cloud assistance, the games, the sleep diary, the reading library, and anonymous usage counting (AppFeature in lib/data/remote/feature_flags.dart). Read-aloud was a seventh until 2026-08-15, when the feature was removed from the app rather than switched off. Two of the six — the on-device model download and cloud assistance — are already withheld from this build when it is compiled, and nothing our server says can switch either back on.

What can never be. The crisis screens, the safety plan, the helplines, the consent switches and "Save a copy of your data". Not as a promise — there is no name for them in what the app will accept, so there is nothing for our server to say. A test fails the build if a crisis surface ever starts reading one.

It only goes one way. Our server can switch a shipped feature off. It cannot switch anything on, and it cannot add anything: a name the app does not recognise is discarded before it is stored. The worst a compromised server could do is make Moorwell smaller — never different.

If the check fails you lose nothing. No network, a stale answer, an unreadable one, or the very first launch all leave every feature on. That is the difference between a safety switch and a licence check, and it is worth being explicit about beside a request you cannot opt out of.

Nothing disappears under your thumb, and nothing is deleted. A withdrawal takes effect at the next launch, not mid-use. A model already downloaded stays on your device; counts already collected stay until you erase them.

The request itself carries no identifier — not an account, not a device id. Only the app's public key, which is the same for everybody who installs it.

The other two only ever add or correct: wording and activities. If our server is unreachable, unsigned, or says something the app does not understand, Moorwell uses what shipped inside it and carries on. That is the ordinary case, not the failure case.

About that last one. On the installed app, the models Moorwell uses to read paraphrase and detect emotion are built in. They are not downloaded and no request is made for them. The machinery for fetching a corrected one exists in the code and is switched off (kModelProvisioningEnabled in lib/ai/models/model_provisioning.dart), because it introduces a network request where there was none and that is not a change to make quietly.

On the website it is different, and this page used to say otherwise. A browser cannot open a 23 MB file that is bundled into an Android package, so the web build fetches the paraphrase model from the same site you are already on (lib/ai/models/model_manager_web.dart) — the same shape of request as loading a font or an image, to the same origin, to nobody else. Its bytes are checked against a fingerprint before use and a file that does not match is deleted rather than used. No third party sees anything, and nothing you wrote is part of the request. Corrected 2026-08-08; the sentence above previously said "this version makes no request for them at all", which was true of the app and not of the site.

If it is ever switched on, this is exactly what it would be and would not be. A model fetch is a request for a file — the same shape as your browser loading an image. It has no request body. It carries nothing you wrote, no mood score, no safety plan, no identifier, and no account. What it does reveal is what any file download reveals: an IP address, a time, and that the requester is running this app. It is not the cloud-help switch, which sends what you typed to another company; the two are separate mechanisms with separate consequences, and this page will not blur them.

Why it would exist at all: a model shipped inside the app can only be corrected by a full app-store release, and in August 2026 we found one whose licence was not what we had recorded. Being able to replace a model without making every user wait for a store review is a safety property, not a feature. Settings always states which of the two situations you are actually in, computed from this device rather than from what is true of most.

Deliberately absent, and each absence is a design decision, not an oversight:

Not requestedWhy
LocationYour country comes from the device locale. Precise location is sensitive and unnecessary — place-based suggestions are written so you supply the place.
MicrophoneNothing in the app listens. There is no voice input, no recording, and no dictation.
CameraNothing in the app takes a picture.
ContactsSocial suggestions never name anyone. You know who you mean.
CalendarExam dates are two dates you type in. Nothing is read from your calendar.

Notifications were listed in that table as "not used in this version", and that stopped being true when reminders landed. A reminder is a notification: POST_NOTIFICATIONS is one of the nine above, and your phone asks you for it the first time you turn a reminder on. What is still true is the part that mattered — nothing is pushed to you from anywhere. There is no push service, no server and no account behind a reminder; your phone holds the schedule and your phone rings it, and with reminders off, which is the default, no notification is ever raised.


What we never do


If you turn on cloud help

Off by default. Two questions, asked separately, and when the screen exists they are in Settings → Cloud help.

1. Where it may be processed.

SwitchWhat it meansDefault
Use an outside AI serviceWhat you typed is sent to another company, under their terms rather than ours.off

Moorwell does not run an AI service of its own and does not intend to — the only two places anything you write could be worked on are an outside AI vendor, above, and a model running on your own device, which needs no switch and sends nothing over the network. (This project does operate a server, and this page does not pretend otherwise: it answers the launch-time check above and receives the error reports below. It has never been asked to read a note.) The larger of the on-device models is too big to ship inside the app and has to be downloaded, which is a file transfer in one direction with nothing you wrote in it — and that model, like cloud help, is withheld from this build when it is compiled, so 1.11.0 neither downloads it nor runs it. Sending a note to a vendor is not private: it goes under their terms, not ours, and the copy on the consent screen says so in the same breath.

2. What it may be used for — rewording a line Moorwell already chose, or naming the topic of a note Moorwell could not read. Also separate, also off. Nothing happens from these alone: with no recipient agreed to, there is nowhere for them to send.

What is sent, exactly: the text of that one check-in, and Moorwell's own wording and category lists. Nothing else — there is no field for a mood score, a date, an identifier, your goal, your diary or your safety plan (lib/ai/cloud/cloud_payload.dart, and the consent screen shows you the real payload rather than a description of it). If anything you wrote reads as being about your safety, nothing is sent and nothing is asked.

When the screen exists, one tap turns all of it off, and the app is unchanged: the offline path produces the whole response on its own, and cloud help can only ever alter how a response is worded. That is also why withholding it from this build costs you nothing.


Accounts

There is no account here today: no account is created in this version, at any setting and at any age. The account machinery is switched off at kAccountsEnabled in lib/startup.dart, and nothing is wired to ConsentAccount, so the call that would mint one does nothing. Everything below describes what happens when it is switched on.

When it is switched on, an anonymous account is created for you if you are at or above the age threshold for your country — 18 in India. It holds no email, no name and nothing you write. It is not something you turn on: it is created because you are an adult who was told, on the first screen, that it would be.

If you are below the threshold, or you have not answered the age question, none is created — ever. Not a limited one, not a local one. The app itself is completely unaffected: every check-in, the whole library, the safety plan and the crisis resources work identically at any age.

Clearing or lowering your age deletes it, on our server as well as here, along with everything attached to it. So does "Erase everything on this device". If we cannot reach the server to do that, the app tells you so rather than claiming it did (lib/data/auth/consent_account.dart, test/consent_account_test.dart).

What was refused, and why this is not it. A silent anonymous account — one created quietly the first time the app has a connection, to count things with — was proposed twice and refused twice. An anonymous id plus a list of when you opened the app is "someone used a mental-health app forty times this month, mostly after midnight", which is information about your health even with nothing you wrote attached to it. What made that refusable was that nobody was told. The account described above is told to you here and on the first screen of the app, before anything is created, and that is the whole difference — remove the telling and the refusal applies again.

What we still deliberately do not do: attach usage counting to that account. The counts stay what they have always been — whole numbers per screen, no identity, no clock, no ordering — and nothing on that path touches the account. An account you were told about is a different thing from a hidden history of when you opened the app, and the second one is still refused.


Anonymous usage counts

On when you install the app. Off in one tap, in Settings → "Share anonymous usage data", separately from sync.

What is counted. Integers, one per named surface and action — breathing.opened, guidedActivity.completed, activityReshuffle.tapped. The vocabulary is a fixed list in the app's source (lib/data/usage_events.dart); it is not free text, so nothing you type can end up in it even by accident.

There is a second, smaller set of counters — which kind of help Moorwell offered and what became of it — and it has its own section below, Which kind of help landed. Until 2026-08-10 this paragraph said the counters above were the whole record. They are not, and the correction is written out there.

Which kind of help landed, and how hard the day was

A second set of counters records which kind of help Moorwell offered and what became of it, so that help which never lands can be found and changed. They are integers with the same shape as the ones above — no clock, no ordering, no identity — and they live in lib/data/route_outcomes.dart.

Each counter has exactly three parts: the kind of help (breathing, grounding, reappraisal, and eight more), one fact about the moment it was offered, and what became of it. A key looks like reappraisal.band_high.served.

The middle part is the one that needs saying plainly, because this page used to deny it. It can be the distress band Moorwell read from your check-in, your mood score, whether it was late at night, or a grouped emotion family. One of those per counter and never two together: a counter crossing all of them would describe a person rather than the content. A cell holding fewer than twenty is withheld from any report, because a cell of one is a person.

What these still never touch. Nothing from a crisis turn — the crisis card and the safety-plan card are excluded in the code, so a crisis response increments none of them. Not the words you wrote. Not which helpline was shown. And nothing that says when.

Nothing is sent. There is no uploader for these either — the constant is routeUploadImplemented in lib/data/route_outcomes.dart, it is false, and if it is ever flipped this section owes you a sentence saying what leaves. Turning off Share anonymous usage data stops these at the same instant it stops the others: it is one switch, not two.

Three counts from the crisis screen, and why they exist

Added 2026-08-09. This section previously said nothing from a crisis screen was ever counted, and that has changed — so it is written out here rather than folded into the list above.

Three integers, and this is all of them:

CounterWhat increments it
crisisSupport.openedthe crisis resources screen was opened
crisisCall.tappeda phone or text contact was tapped
crisisWebsite.tappeda web resource was tapped

The reason is a safety reason, not a business one. If people reach that screen and never tap anything on it, the screen is failing at the only job it has, and until now there was no way for us to know. That is worth a number.

What these three do not contain, and each of these is a deliberate refusal:

They obey every other rule on this page. They stop the instant you turn the setting off, they are not sent anywhere in this version, and "Erase everything on this device" clears them with the rest.

And they can never stand in your way. The counter cannot fail in a way that affects the screen: if writing it fails for any reason the failure is discarded and the page carries on. A test opens the crisis screen with a recorder rigged to fail on every write, and checks the helplines still appear.

What is never counted, under any setting:

Nothing is sent. There is no uploader in this version — the counts are integers in the app's own settings store and they go no further. The constant that says so is uploadImplemented in lib/data/usage_events.dart, it is false, and the wording the app shows you is derived from it, so the app cannot claim to be sending something it cannot send. Anyone building the upload has to re-read this section and make it true again, and flipping that constant changes the sentence shown on the first screen and in Settings in the same move.

If it is ever built, it will be aggregate totals only, with no account and no persistent identity behind them, to a table the server has no policy allowing anything to read back (sql/schema.sql, analytics_events). That is not an implementation detail: counters with a persistent identity behind them would be a per-person history of when someone opened a mental-health app, which is the thing the Accounts section above says we refused.

Turning it off means nothing is written, not written-and-discarded. A test proves that against the stored values rather than against the switch (test/usage_events_test.dart), because a switch that flips a flag nothing reads is the easiest possible thing to ship by mistake.


Error reports

On when you install the app, and from 2026-08-11 there is no switch for it.

That is a change, and it is written here rather than left to be discovered. Until then Settings carried a "Send errors on their own" toggle. It was removed for the plainest reason there is: a crash reporter nobody opts into reports nothing, and the crashes that matter most happen on phones whose owner never opened Settings.

What makes that defensible is the payload, and nothing else. An error report is a failure type, the part of the app it happened in, a stack of frames, and four more fields written out in full further down. There is no message, no identifier of any kind, no account, and no clock finer than a date — the app never stores an error's message, which is what makes a stack safe to send at all. Nothing you have written can travel in one.

The line this does not cross. Every switch governing something you wrote leaving the device is still a switch, still separate, and still off until you turn it on. Cloud help is the one that matters, it is untouched, and in this version it is withheld from the build altogether — see above. If a future version wants to send anything with your words in it, that needs your consent and this paragraph stops being true — which is the point of writing it down.

Sending them at all is the older of the two changes: that started on 2026-08-10, the day before the switch went. It is the only thing on this page that has ever moved in this direction, so here is the reason in plain terms. Before, a crash was written to a log on your phone with a Copy button under it, and essentially nobody presses that button — which means the crashes that got fixed were the ones that happened to the people who write this app, and the ones that happen to you went nowhere. A wellbeing app that breaks silently on your phone and never tells anybody is not being careful with you; it is just being quiet about failing you.

When Moorwell breaks, it writes down what broke — in Settings → Errors, where you can read every word of it. That log stores an error's kind and where in the code it happened, and never the error's message — which is the rule that keeps your words out of it. No check-in, no note and no safety plan has been stored there.

That rule holds because it is enforced, not because it is obvious, and the honest version of this is worth more than the reassuring one. On 2026-08-09, the day the log was built, three code paths were found composing people's words into error objects handed to the very reporter this log listens on: a spoken line interpolated into an error, a sync row logged whole by its toString, and database failures that can carry the statement your note was bound to. All three were fixed that day, before the log shipped. Storing a type and a stack rather than a message is what makes a report safe to send at all (lib/data/error_log.dart).

Why this is not the same thing as the counts above. The counters stay on this device and are yours to check. An error report is the one thing Moorwell sends on its own — the launch check above also goes without asking you, but it asks a question and reports nothing — so a report is held to a different standard, and that standard is the payload rather than a choice: the list below is fixed in the app's source, carries nothing you wrote, and carries nothing that identifies you or your device.

What a report would carry, and this is the whole list: a signature of the failure, the error type, up to twelve stack frames, a coarse name for where in the app it happened, the app version, the platform, and the date — no time of day. Seven fields, fixed in the app's source (lib/data/crash_upload.dart), and a test fails the build if an eighth appears.

There is no identifier of any kind, and that is the whole design. Both industry crash tools were rejected for this: Crashlytics joins a stable installation UUID to every report on Google's own documentation, and Sentry writes a UUID to the app's cache directory on first launch with no way to switch it off. Moorwell instead hashes the failure itself — the error type and the top frames — so reports of one bug group together by the bug rather than by the phone. What that costs is the ability to say how many different people hit something. What it buys is that two reports from your phone cannot be told apart from two reports from two strangers'.

The time of day is deliberately not in it. The error log on your device keeps it, because you are the one reading it and the order matters. It is cut back to a date the moment a report is built, because a time precise to the second beside an uncommon event identifies a person even with no name on it — the same reason there are no timestamps on the counters above.

What leaves, and where it goes. The seven fields above are sent to this project's own Supabase database — no third party, no analytics vendor, no crash SDK. It is sent unauthenticated and with no account, so there is no session, no user id, and nothing for a row to be joined to even in principle. The constant is uploadImplemented in lib/data/error_log.dart, it is now true, and the sentence you see on the error screen is derived from it rather than written down here.

The server keeps a count, not a list — this is the promise this page made before the uploader existed, and it was kept. Arrival does not store one row per report. It increments a counter keyed by the failure, the day, the app version and the platform, so eleven crashes of one bug on one day are a single row reading count = 11, with one example stack kept and the other ten discarded. That matters for a reason worth stating: a row per request is the thing an access log's IP addresses can be lined up against, and a table with no per-request rows has nothing to line up. The table itself has row-level security on and no policies at all, so the key shipped in the app can neither read it nor write it — the only way in is a function that increments the counter (sql/schema.sql).

What is still true about IP addresses. Any HTTPS request reaches a server with an IP on it, and that is a property of the internet rather than a choice made here. What this design controls is that there is no row for one to be stored against, and no identifier in the payload to carry it forward.

The local log is yours regardless. Sending is not what makes the error log useful: it records on your device, you can read every word of it, copy it and send it yourself, and "Erase everything on this device" deletes it with the rest.


Age, and what changes at 18

Moorwell's features all run on your device, and none of them is held back by your age. Every check-in, every activity, the whole library, the safety plan, and the crisis resources — none of it asks how old you are.

Creating an account needs you to be 18 or over. India's Digital Personal Data Protection Act 2023 treats anyone under 18 as a child, and processing a child's personal data requires verifiable consent from a parent. A synced account holds what you write, so the honest answer is not to offer one rather than to ask you for a parent's details.

You are asked on the first screen, and you pick an age group. Six of them, starting at Under 18. Not a date of birth, not a month and year, and not "are you 18 or over" — that last one is a shape the US Federal Trade Commission calls a non-neutral age screen, because it tells you which answer unlocks the product before you answer. Neither is a list that starts at the minimum, which is why Under 18 is the first option and not a missing one.

Every group is one tap, and none of them asks you anything extra. That is deliberate for the same reason: a follow-up question that only one group has to answer signals which answer the app is hoping for just as clearly as a Yes/No does.

Until 2026-09-07 this was a box you typed an age into, and the number that was typed on installs made before then is still on those devices and still what the app reads. Choosing a group replaces it.

Your answer is stored on your device, and you can change it any time in Settings → You. Nothing about it is sent anywhere. It cannot be cleared back to unanswered — that is a deliberate limit, not an oversight — but changing it to a lower group deletes the account, as Accounts above describes, and in this version there is no account for it to delete.

Every answer you give is kept, on your device, with the date you gave it — so if you pick Under 18, change it to 18–24 and change it back, all three are in the record and in your export. That exists to make the history checkable rather than to watch you: it never leaves your device, and "Erase everything on this device" removes it along with everything else.

Anonymous usage counts are not tied to this, because they are not about you. They are whole numbers per screen — how many times Breathing was opened, on this device — with no identity, no clock and no ordering attached, so there is nothing in them that could point at a person. See Anonymous usage counts above, and turn them off in Settings if you would rather they did not exist.


Your control

You want toHow
Use it with no accountDo nothing. That is the default, and everything works.
Stop anything you wrote reaching a serverNothing you wrote reaches one in this version, and there is no Cloud help row in Settings to turn off, because cloud assistance is withheld from this build. When that row exists it is one tap: Settings → Cloud help → "Turn all of this off". There is no account for it to delete either — see Accounts — and when there is, this deletes that too.
Stop the launch check and the error reportsYou cannot in this version, and it is the one row in this table with no answer. Neither carries anything you wrote; both are described above, under Contacting our server on every launch and Error reports.
Stop syncingSettings → turn off "Back up to my account". There is nothing to sync in this version, because no account is created.
Stop anonymous usage counts (they start on)Settings → turn off "Share anonymous usage data". Nothing is written from that moment.
Change your ageSettings → You. Pick a different age group. It cannot be removed back to unanswered; moving to a lower group removes the account too — and in this version there is no account to remove.
Delete everything, here and on our serverSettings → "Erase everything on this device". Where a server-side account exists it deletes that too, and says so if it could not reach it. In this version there is none to delete.
Delete everything on the device onlyUninstall the app.

What Moorwell is not

Moorwell is a companion, not a substitute for professional care. It does not diagnose, it does not treat, and it is not an emergency service. If you are in danger, contact the emergency number for your country or one of the helplines the app lists.

The crisis helpline numbers are curated and bundled with the app. They are shown alongside your national emergency number and an international directory, so a stale entry cannot leave you with nothing.


Where this document is published

https://moorwell.app/privacy.html — live, verified 200 on 2026-10-11, with a Let's Encrypt certificate for that name. The app itself is at https://moorwell.app/.

This closes the Google Play requirement for a publicly hosted privacy policy.

The address above replaced moorwell.vercel.app on 2026-10-11. That section said the move would be a coordinated edit in three places rather than a redirect left to expire — here, the Play listing, and anywhere the app links out. Two of the three are done in the same commit as this sentence: this file, and AppStrings.privacyPolicyUrl, which is what the app opens. The Play listing is the third and is the owner's, and it is the one with a takedown risk attached, because a listing pointing at a policy URL that stops resolving is grounds for removal. The old address does redirect permanently, so a link saved before the move still lands — but a store listing should name the real URL, not rely on that.

The deployment is Vercel, configured by vercel.json and vercel_build.sh. Note that MOORWELL_BGE_MODEL_URL and MOORWELL_BGE_VOCAB_URL are Vercel project environment variables, not repository values — nothing in this checkout can prove what they are set to, so a claim that they are configured is owner-verified, never repo-verified.

Contact

There is no contact address on this page yet, and that is the true position rather than an omission. An address a privacy policy names has to be one somebody reads and answers, and this project does not have one it can promise that of. Putting a placeholder here would be a worse answer than this paragraph, because you would have written to it. The About page carries whatever is currently true about reaching this project, and this line changes the moment there is an address to give.

Do not wait on any of that if you need help now. Nothing on this page is a way to reach a person quickly. If you are in danger, use the emergency number for your country, or one of the helplines at the foot of this page.