Files
runicgateway.com/PLAY_DATA_SAFETY.md
wtclaude c8a293b8f6 docs(admin): the engagement rules screen, and the privacy inventory an engagement mailer changes
Engagement Phase 12a. The site had pages for where a message goes (Notifications and
email) and what it says (Message templates), and nothing at all for what makes one get
sent -- the four Engagement screens the workstream built.

New page: Engagement rules. Rules, Audiences, the trigger catalog and the send log on
one page, sitting between the two it joins up. Templates already has its own page and
Suppressions is in Troubleshooting, so neither is repeated here.

Two things it exists to state plainly:

  * Every rule ships disabled, including the ones a module brings. "Installed" is not
    "on", and an upgrade whose Team mail went quiet is the same fact.
  * The ceiling is a TREE, not a ladder. The tempting reading -- a staff-only event
    could obviously also go to one person -- is wrong, and the example is the argument:
    "one person" for cheat detection is the player it was detected on.

Troubleshooting gains the symptom that page answers ("nothing is sent for one
particular event"): the rule is off, the rule is dormant, its own cooldown held it, or
the audience is empty.

Privacy: two rows the engagement work makes necessary, and one sentence it made false.

  * app-content claimed "Nothing is cached for offline use". Phase 8 shipped a DataStore
    snapshot of the inbox, so it was untrue -- and that row feeds the generated Play Data
    Safety answers, which is a store-review matter rather than a doc nit. The snapshot now
    has its own row and its own Play mapping (Messages / Other in-app messages; not
    collected by us, stored on the device), and app-content's claim is narrowed to
    everything else.
  * deploy-engagement, for the deployment scope: an address is now used for more than
    getting into an account, there is a delivery log holding a one-way hash of it, and
    there is a suppression list. Its retention line says what is true rather than what a
    reader assumes -- none of these tables has a retention sweep.

PLAY_DATA_SAFETY.md regenerated from the inventory; legal.lastUpdated moved with the page
it dates.

Verified: the whole `verify` chain green -- checkSidebar (plannedSidebar moved with the
live tree), checkFacts 19/19, checkQuickstart 59, checkReference 22, checkLinks 2605,
checkA11y, checkCsp, playDataSafety --check, 42 + 7 tests. Read in a browser as well, in
the served build.

AI-assisted: written with Claude Code.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 08:11:10 -05:00

132 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
GENERATED FILE — do not edit.
Source: src/data/collection.mjs (scope "app") + src/data/legal.mjs
Generator: scripts/playDataSafety.mjs
Edit the data file and run `npm run play:datasafety`. CI runs the same
generator with --check, so a hand edit here fails the build rather than
quietly disagreeing with the published privacy policy.
-->
# Google Play Data Safety — the answers, and what they are based on
The Play Console asks, for every category of data, whether the app **collects** it, whether it is **shared**, whether collection is **required or optional**, and *why*. This file holds the answers for the Runic Gateway Android app, generated from the same inventory the published privacy policy renders — see `/privacy`, section 2.
> **This is not a filled-in form.** Plays definitions change and no check here can read them. Every answer below is a fact about the code with the reasoning attached; read the consoles current wording against them when you fill the form. What this file exists to prevent is somebody answering from memory about what the app stores.
## The premise every answer rests on
We operate **no server the app talks to.** The app ships pointed at nothing: its first screen asks for the address of a Runic Gateway deployment and validates it before anything else in the app runs. That deployment belongs to whoever runs that community. Data therefore travels from the device to *their* server, and there is no endpoint of ours anywhere in the path — not for content, not for telemetry, and not for crash reports, of which there are none.
That is why nearly every answer below is "not collected", and it is also the answer most likely to be questioned in a review. The supporting facts are in the table: each row names the file it was read out of.
Where the console offers free text about security practices, two things are worth saying: credentials are held in Androids encrypted storage (AES-256-GCM via Jetpack Security), and push notifications carry **no content** — a relay receives a stream name and a reference, and the app fetches the actual message over its own authenticated connection.
## Data types
| Category | Data type | Collected by us | Shared by us | Answer |
|---|---|---|---|---|
| Personal info | User IDs | No | No | Not collected by us. |
| Personal info | User IDs | No | No | Not collected by us. |
| App info and performance | Other app data | No | No | Not collected by us. Stored on the device only. |
| Messages | Other in-app messages | No | No | Not collected by us. Declare the relay hop in the consoles free-text security section if it asks. |
| Messages | Other user-generated content | No | No | Not collected by us. |
| Messages | Other in-app messages | No | No | Not collected by us. Stored on the device only. |
| Device or other IDs | Device or other IDs | No | No | Not collected. |
## Each answer, and why it is the truthful one
### Your sign-in tokens
**Personal info → User IDs.** Not collected by us.
When you sign in to a deployment, the app keeps the access and refresh tokens it was issued, plus the username, role and account id they belong to. They are held in encrypted storage on the device (AES-256-GCM through Jetpack Security) and are sent to exactly one place: the deployment that issued them.
- **Why that answer:** The credentials are issued by, and returned to, a server the user nominated. Nothing reaches an endpoint under our control, because we run none.
- **Retention:** On the device until you sign out
- **In detail:** Signing out clears them; uninstalling the app removes them with it.
- **Read from:** `core/auth/EncryptedTokenStore.kt`
### The trusted-device token, if you asked for one
**Personal info → User IDs.** Not collected by us.
Ticking “trust this device” during two-factor sign-in stores an opaque token so the deployment can skip the second factor next time. It lives in its own encrypted store, deliberately separate from the session, because it has to outlive a sign-out to be worth anything — and the deployment holds only a hash of it, so the copy on your phone is the only usable one.
- **Why that answer:** Same as the session tokens: minted by the users deployment, stored on the device, presented back to that same deployment.
- **Retention:** On the device until it expires or you revoke it
- **In detail:** Thirty days, and revocable at any time from the deployments Trusted Devices screen, which is also where it can be revoked if the phone is lost.
- **Read from:** `core/auth/EncryptedTrustTokenStore.kt`
### The address of the deployment you chose
**App info and performance → Other app data.** Not collected by us. Stored on the device only.
The app ships pointed at nothing and asks for an address on first run. That address is stored in ordinary preferences rather than encrypted storage — it is not a secret, it is the equivalent of a bookmark — and it is what every other screen in the app talks to.
- **Why that answer:** It never leaves the phone. It is the destination of requests, not the contents of one.
- **Retention:** On the device until you change it or uninstall
- **Read from:** `core/prefs/ServerPreferences.kt`
### Push registration, if you turn notifications on
**Messages → Other in-app messages.** Not collected by us. Declare the relay hop in the consoles free-text security section if it asks.
Push is off until you enable it. When you do, the app mints a random, unguessable topic name on the notification relay the deployment nominates, and registers that topics URL with the deployment so it has somewhere to send a nudge. What actually travels through the relay is content-free — a stream name and a reference, never the message — and the app then fetches the real content over its authenticated connection to the deployment. A leaked topic name therefore reveals nothing, which is the reason the relay needs no account and holds nothing about you.
- **Why that answer:** The notification passes through a relay chosen by the deployment, and it carries no content — the app pulls the content itself, authenticated. Neither hop reaches a server we operate.
- **Retention:** Until you turn push off, sign out, or uninstall
- **In detail:** Signing out or disabling push unregisters the device with the deployment and discards the topic. The relay retains whatever its own operator configures it to; if the deployment points at a relay it does not run, that relay is a third party to both of us, and it still only ever sees a tickle.
- **Read from:** `core/push/NtfyTopic.kt, core/push/PushPreferences.kt`
### Everything you read and post in the app
**Messages → Other user-generated content.** Not collected by us.
Forum posts, Team activity, character and shard information, notification preferences: all of it is a live read or write against the deployment. Apart from the notification snapshot described in the next entry, nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with almost no content, which is a limitation and also an accurate description of where the data lives.
- **Why that answer:** Content is written to the communitys own installation. We have no copy, no access and no way to obtain one.
- **Retention:** Held by the deployment, under its operators policy
- **Read from:** `PLAN.md §9 section 2`
### A snapshot of your notifications, so the inbox opens without a signal
**Messages → Other in-app messages.** Not collected by us. Stored on the device only.
The app keeps the most recent notifications it has already fetched — at most thirty, and only the first page — on the device, so opening the inbox shows you what you had rather than a spinner. It is a copy of what the deployment already sent you and it is refreshed from there; nothing is written here that was not read from your own account. It is scoped to the account that fetched it, so a second person signing in on the same phone is never shown the first ones messages.
- **Why that answer:** The snapshot is written on the phone from data the deployment had already delivered. It is not uploaded anywhere, and no server we operate is on either end of it.
- **Retention:** Until you sign out, or the thirty are pushed out by newer ones
- **In detail:** Signing out deletes the snapshot outright. It lives in the apps ordinary preference store rather than the encrypted one — sign-in tokens are the thing that store is for — which is worth stating plainly: on a device where someone has root, these are readable, and they are notification bodies rather than credentials.
- **Read from:** `core/inbox/DataStoreInboxCache.kt, data/repository/AuthRepository.kt`
### No analytics, no crash reporting, no advertising
**Device or other IDs → Device or other IDs.** Not collected.
There is no third-party SDK in the app at all — no Firebase, no Crashlytics, no advertising identifier, no measurement library. That is checkable rather than claimed: it is what the dependency list and the manifest say, and a build that gained one would gain permissions with it.
- **Why that answer:** No advertising ID, no analytics identifier, and no library that would generate one is linked into the build.
- **Retention:** Nothing to retain
- **Read from:** `app/build.gradle.kts, app/src/main/AndroidManifest.xml`
## The rest of the listing
- **Privacy policy URL:** `/privacy` on this site. It is the URL Play is given, and section 2 of it is about the app specifically.
- **Target audience:** adults. The beta is stated as **18 or older** (D31); the app contains no content directed at children and no age verification.
- **Account deletion:** the app creates no account with us — an account belongs to the deployment the user chose, and is deleted there. The only list we hold is the beta signup, which is erased on request; `/privacy` section 4 says how to ask.
- **Data deletion request URL:** the contact address published on `/privacy`, which is read from the mounted `brand.json` rather than typed anywhere in the source (D13).
## What the website collects, for the same reviewer
Not part of the Data Safety form — that form is about the app — but a reviewer who follows the privacy policy URL lands on a page covering three things, so it is worth knowing which of them the site itself is responsible for:
- **Your email address** — Until the beta ends, or until you ask.
- **The wording you agreed to, and when** — For the life of the row.
- **A one-way hash of your IP address — never the address** — With the row; the rate-limit log is pruned after 48 hours.
- **Your browsers user-agent string, truncated** — With the row; blanked on removal.
- **The web servers access log** — Short-term operational retention, then rotated away.
Last generated from data dated 2026-09-01. Regenerate with `npm run play:datasafety` after any change to what the app stores.