diff --git a/PLAN.md b/PLAN.md
index 17800c5..5ee7e25 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -264,6 +264,7 @@ somewhere other than the thing it decided. The count of record is **eighty-four*
| D80–D81 | §16, "How phase 18 built it" | A name in a screenshot is a role; the Rust captures' players are mock data |
| D82–D83 | §15, "How the site reaches it" | `/beta` lists the demo's sign-ins as the demo publishes them; a demo deep link names its game and shows only for the games the mounted `demoGames` lists |
| D84 | §9, "How phase 6 built the legal pages" | The Data Safety answers say "collected, not shared" for everything the app sends, amending D33 |
+| D85–D86 | §9, "`/delete-account/`" | The deletion page is linked from the footer, `/privacy` and `/app/`; someone who cannot sign in is sent to their community's operator, never to us |
---
@@ -1086,6 +1087,34 @@ form's security questions. D33's test now asserts the part of the premise that i
**nothing is shared**. `/privacy` section 2 keeps saying none of it reaches us, and `lastUpdated`
moved with it. Report, block and hide (Android M8 step 3) adds its rows when it lands.
+### `/delete-account/` (2026-10-30)
+
+Play needs one account-deletion address from whoever publishes the app. The platform gained
+self-service deletion on the website and in the app (`docs/website/ACCOUNT_DELETION.md`, AD1), and
+this page is the address Play is given (AD2, §4.5 there). It cannot delete anything: we hold no
+account, so it explains where the button is in the app and on the website, what goes and what stays
+and for how long, and who to ask when you cannot sign in.
+
+**The facts are one row.** What a deletion takes is the `deploy-account-deletion` row in
+`src/data/collection.mjs`, whose `deletion` field holds the website's own words for what goes and
+what stays (`client/src/lib/accountDeletion.js`, which the app's strings repeat) plus how long each
+kept thing is kept, which Play asks and the screens do not say. `/privacy` section 3 renders the row
+as prose, the page renders its lists, and `PLAY_DATA_SAFETY.md` prints them under the console's
+deletion questions, so the three cannot disagree. `deletionRow()` throws when the row is missing,
+and `test/legal.test.mjs` fails a kept entry with no retention. The app rows that deletion touches
+(the session, the trust token, push and the inbox snapshot) say so, and `lastUpdated` moved.
+
+**D85 — The page is linked from the footer, `/privacy` and `/app/`** (org lead, 2026-10-30). The
+footer's legal bar reads Privacy · Terms · Delete account, because a player looking for it looks
+where the privacy link is; `/privacy` sections 2 and 3 and the bottom of `/app/`'s "Two ways to get
+it" link to it as well.
+
+**D86 — Someone who cannot sign in is sent to their community's operator, never to us** (org lead,
+2026-10-30). The page offers "Forgot your password?" first, which works when the account has an
+email address and the site can send mail, and then the operator, who can delete any account from the
+admin panel. It says plainly that we cannot do it: we hold no account and have no access to any
+community's site. Offering our contact address to help find an operator was considered and declined.
+
**What phase 6 also closed.** `/privacy/` and `/terms/` were the last two entries in
`checkLinks.mjs`'s `PLANNED_ROUTES`; building them emptied the list, and its reverse check is what
forced the deletion. The list itself stays, because §10's documentation routes land in phases 7 and 8
@@ -1111,6 +1140,7 @@ Organised by what a reader is trying to do. A reader should never need to know t
| `/beta/` | The closed-beta signup (§8). The one page that handles its own POST (D28) |
| `/community/` | Discord (`discord.gg/t2Jav8yT4g`) as the front door, the Gitea org for code and contributions, the `brand.json` contact address for vulnerabilities (D13) — the split in §14 N3 |
| `/privacy/`, `/terms/` | §9 |
+| `/delete-account/` | §9: how to delete an account you use in the app, and what it keeps. The address Play is given (D85, D86) |
**Feature grouping**, using project terminology:
diff --git a/PLAY_DATA_SAFETY.md b/PLAY_DATA_SAFETY.md
index dc8cd4a..6b0abfc 100644
--- a/PLAY_DATA_SAFETY.md
+++ b/PLAY_DATA_SAFETY.md
@@ -1,7 +1,7 @@
+
+ Done with it? How to delete the account you use in the
+ app, and what deleting it keeps.
+
@@ -335,6 +341,12 @@ const downloadBase = `${releasePage}/download/${release}`;
padding-top: 0.4rem;
}
+ .app-delete {
+ margin: 1.25rem 0 0;
+ color: var(--muted);
+ font-size: 0.95rem;
+ }
+
.app-get__foot {
margin: 1rem 0 0;
padding-top: 0.85rem;
diff --git a/src/pages/delete-account.astro b/src/pages/delete-account.astro
new file mode 100644
index 0000000..b48d6eb
--- /dev/null
+++ b/src/pages/delete-account.astro
@@ -0,0 +1,327 @@
+---
+import Base from '../layouts/Base.astro';
+import PageHeader from '../components/PageHeader.astro';
+
+import { deletionRow } from '../data/collection.mjs';
+import { legal } from '../data/legal.mjs';
+import { brand } from '../lib/brand.mjs';
+
+/**
+ * `/delete-account/` — the page Google Play is given for account deletion
+ * (docs/website/ACCOUNT_DELETION.md §4.5, AD2).
+ *
+ * ---------------------------------------------------------------------------------------
+ * IT EXPLAINS; IT CANNOT PERFORM
+ * ---------------------------------------------------------------------------------------
+ * Play needs one deletion address from whoever publishes the app, and that is us. But we
+ * hold no account: every community runs its own site, and the account lives there. So this
+ * page says where the button is on the website and in the app, what goes and what stays,
+ * and who to ask when you cannot sign in, which is that community's operator and never us
+ * (D86). We have no access to any community's site, so offering to help would promise
+ * something we cannot do.
+ *
+ * What goes and what stays is not written here. It is the `deletion` row in
+ * `src/data/collection.mjs`, the same row `/privacy` section 3 renders, so the two pages
+ * cannot disagree about what a deletion takes.
+ */
+const title = 'Delete your account';
+const description =
+ `How to delete an account you use in the ${brand.siteName} Android app or on a ` +
+ `${brand.siteName} community site, what is deleted, and what is kept.`;
+
+const row = deletionRow();
+const { goes, stays, staff } = row.deletion;
+
+const surfaces = [
+ {
+ id: 'on-the-website',
+ eyebrow: 'On the website',
+ heading: 'Your community’s site',
+ steps: [
+ 'Sign in to the site.',
+ 'Open “Account”. Staff open “Account” in the admin panel instead.',
+ 'Scroll to the bottom, to “Delete my account”, and press “Delete my account…”.',
+ 'Type your username, then your password, and a two-factor code if the account has two-factor sign-in on.',
+ 'Press “Delete my account”. You are signed out everywhere, and the site says the account has been deleted.',
+ ],
+ },
+ {
+ id: 'in-the-app',
+ eyebrow: 'In the app',
+ heading: `The ${brand.siteName} app`,
+ steps: [
+ 'Sign in, if you are not already.',
+ 'Open the menu and choose “My account”.',
+ 'Scroll to the bottom and tap “Delete my account…”.',
+ 'Type your username, then your password, and a two-factor code if the account has two-factor sign-in on.',
+ 'Tap “Delete my account”. The app forgets the account and returns to sign-in, saying it has been deleted.',
+ ],
+ },
+];
+---
+
+
+
+
+ For the {brand.siteName} Android app, and for the community sites it connects to. We
+ are {brand.siteName}, the project that makes both.
+
+
+ You can delete your account yourself, on the website or in the app. It is deleted at
+ once: there is no waiting period and nothing to cancel.
+
+
+
+
+
+
+
+
Your account is with your community, not with us
+
+ The app ships pointed at nothing. On first run it asks for the address of a
+ {' '}{brand.siteName} site, and that site is run by whoever runs that community. Your
+ account lives there, under their control. We hold no account for you and have no
+ copy of one, so the deletion happens on that site, and you are the one who does it.
+
+
+
+
+
+ How to delete it
+
+ The same steps on both, and either one deletes the account everywhere: deleting it in
+ the app also deletes it on the website, and the other way round.
+
+
+
+ {
+ surfaces.map((surface) => (
+
+
{surface.eyebrow}
+
{surface.heading}
+
+ {surface.steps.map((step) => (
+ - {step}
+ ))}
+
+
+ ))
+ }
+
+
+
+ -
+ A password is required. If you only ever signed in with Google,
+ Discord or Steam and never set a password, set one in the Password section of the
+ same page first.
+
+ -
+ The only admin cannot delete themselves. Make someone else an admin
+ first, so the site is never left with nobody to run it.
+
+
+
+
+
+
+
+
Deleted
+
What goes with the account
+
+ {goes.map((item) => - {item[0].toUpperCase() + item.slice(1)}.
)}
+
+
+
+
+
+
Kept
+
What stays, and for how long
+
+ {
+ stays.map((entry) => (
+ -
+ {entry.what}
+
+ How long
+ {entry.keep}
+
+
+ ))
+ }
+
+
+
+
+
+
+
+
+
If you cannot sign in
+
+ -
+ Use “Forgot your password?” on the sign-in page, on the website or in the app. It
+ works if your account has an email address and the site can send email.
+
+ -
+ Otherwise, ask the community that runs the site. Its operator can delete any
+ account from the admin panel, and the result is the same as deleting it yourself.
+
+
+
+ We cannot do it for you. We hold no account, and we have no access to any
+ community’s site.
+
+
+
+
+
+
diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro
index 17e9b35..2d0bc62 100644
--- a/src/pages/privacy.astro
+++ b/src/pages/privacy.astro
@@ -66,6 +66,8 @@ const sections = [
'the app happens between your phone and their server, and there is no account with ' +
'us, no service of ours in the middle, and no copy of anything on our side — because ' +
'we do not operate one.',
+ /* The deletion page Play is given (D85). The app is where most people will look. */
+ link: { href: '/delete-account/', text: 'How to delete the account you use in the app' },
rows: collectedIn('app'),
},
{
@@ -79,6 +81,7 @@ const sections = [
'and their policy — this page is not it. What follows is an inventory of what the ' +
'software collects, so that an operator can see plainly what they are taking on, ' +
'and a player can see what to ask their operator about.',
+ link: { href: '/delete-account/', text: 'What deleting an account takes, and what it keeps' },
rows: collectedIn('deployment'),
},
];
@@ -131,6 +134,11 @@ const sections = [
{section.heading}
{section.controller}
{section.lede}
+ {section.link && (
+
+ {section.link.text}
+
+ )}
@@ -304,6 +312,11 @@ const sections = [
color: var(--muted);
}
+ .legal-sec__link {
+ margin: 0.75rem 0 0;
+ font-size: 0.95rem;
+ }
+
.legal-rows {
display: grid;
gap: 1rem;
diff --git a/test/legal.test.mjs b/test/legal.test.mjs
index c2b42d1..3bc63b9 100644
--- a/test/legal.test.mjs
+++ b/test/legal.test.mjs
@@ -33,6 +33,7 @@ import {
assertScopeNonEmpty,
collected,
collectedIn,
+ deletionRow,
PLAY_DATA_TYPES,
PLAY_PURPOSES,
playRows,
@@ -139,6 +140,40 @@ describe('the Play Data Safety mapping', () => {
});
});
+/*
+ * `/delete-account/` is the address Google Play is given (ACCOUNT_DELETION.md §4.5), and
+ * Play asks two things of it that render fine when missing: what is deleted, and how long
+ * what stays is kept. A `stays` entry with no `keep` is a card with no answer to the
+ * second question.
+ */
+describe('the account-deletion row', () => {
+ it('exists exactly once, in the deployment scope', () => {
+ const row = deletionRow();
+ assert.equal(row.scope, 'deployment', 'the deletion row is about a deployment, not about us');
+ });
+
+ it('says what goes and what stays, with how long for each', () => {
+ const { goes, stays, staff } = deletionRow().deletion;
+ assert.ok(goes.length, 'the deletion row names nothing deleted');
+ assert.ok(stays.length, 'the deletion row names nothing kept');
+ for (const entry of stays) {
+ assert.ok(entry.what?.trim(), 'a kept entry says nothing about what is kept');
+ assert.ok(entry.keep?.trim(), `"${entry.what}" does not say how long it is kept`);
+ }
+ assert.ok(staff?.trim(), 'the staff note (AD9) is missing');
+ });
+
+ /*
+ * AD5 in test form: members see "[deleted player]", the wording the website and the app
+ * render. A page that named a different placeholder would describe a product that does
+ * not exist.
+ */
+ it('names the placeholder members see', () => {
+ const text = deletionRow().deletion.stays.map((entry) => entry.what).join(' ');
+ assert.match(text, /“\[deleted player\]”/);
+ });
+});
+
describe('the minimum age', () => {
it('is stated in the consent sentence the row records', () => {
assert.match(