Files
runicgateway.com/PLAY_DATA_SAFETY.md
wtclaude a2faf07104
All checks were successful
PR checks / checks (pull_request) Successful in 55s
feat(legal): phase 6 — the privacy policy and the terms
PLAN.md §9. Builds /privacy and /terms, links them from the footer on every page,
and generates the Play Data Safety notes from the same inventory the policy renders.

Four decisions taken by the org lead before either page was written, recorded in
§9 under "How phase 6 built the legal pages":

  D30  DNS-only records, so the reverse proxy on the host keeps the only access
       log. Described qualitatively — the retention belongs to the proxy, and a
       policy that quotes a number the deployment does not enforce is worse than
       one that does not.
  D31  Eighteen or older. Above the children's-consent threshold everywhere in the
       EEA, so consent works with no parental-consent machinery this form could not
       honestly operate. Four surfaces render it from src/data/legal.mjs, and every
       one says plainly that nothing verifies it.
  D32  No governing-law clause. Nothing of value is contracted for here.
  D33  PLAY_DATA_SAFETY.md is generated from src/data/collection.mjs and checked in
       CI, so the published policy and the answers given to Google cannot drift.

/privacy is three separately-scoped sections because "we" means three different
parties: this site (one form, no cookies, no third-party requests), the Android app
(we operate no server it talks to — the rows are what the DEVICE holds), and a
self-hosted deployment (the operator is the controller, not us). Every row names the
file it was read out of, because a policy is the document most likely to be written
from a template and least likely to be re-read against the software.

/terms governs only what we run: this site, the beta list, and the APK we publish.
The software is governed by its licence, and a community's deployment by that
community — a terms page claiming authority over every install of a GPL program is
the thing a generated template gets wrong.

Also here:
  - the age clause changed CONSENT_TEXT, so CONSENT_VERSION gained a suffix; rows
    written from now on carry the new sentence and older rows keep theirs
  - PLANNED_ROUTES is now empty — these were its last two entries, and its reverse
    check is what forced the deletion; the list stays for phases 7 and 8
  - test/legal.test.mjs asserts the structural promises no build check can see,
    including that every mapped Play row still answers "not collected, not shared"
  - --check normalises line endings: the repo has no .gitattributes and Windows
    checkouts are CRLF, so a byte comparison would fail for every Windows developer
    while passing in CI

Verified: npm run verify green end to end (tokens, brand, data safety, astro check,
36 tests, build, 214 links, 19 facts), both pages walked in a browser, and neither
overflows at 390px. One defect the checks could not see and a look could: the
retention line was being pushed to the foot of the tallest card in its row, opening
a void in the middle of the short ones.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 04:21:47 -05:00

9.3 KiB
Raw Blame History

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.
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. Nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with 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

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-08-24. Regenerate with npm run play:datasafety after any change to what the app stores.