Publish a privacy policy and let people delete their account - #115
Merged
Merged
Conversation
The app had no /privacy route and no way to delete an account, though the Terms describe ending one. Both are launch blockers. /privacy is a public page laid out like the Terms. It names what the service keeps, what it never receives, what a team sees, the three processors (Google, Cloudflare, Twilio SendGrid), what the browser stores, how long each kind of record lasts, and what deletion removes. Sign-in, sign-up, the Terms and the Account page link to it. Delete account on the Account page asks for the account's email typed back and a fresh sign-in, then: - DELETE /api/account removes the person's tokens, sessions, sealed password copies, vault, comments, notifications and membership in one transaction. What they typed into colleagues' sessions stays in the team's trail with the email replaced. A team nobody else is in is dissolved; a team they own is handed to its longest-standing admin, or member if there is none. - The browser then clears its vault key, cached passwords and open tabs, and deletes the Firebase user. A deleted uid is remembered for two hours (migration 008), because an ID token issued before the deletion still verifies for up to an hour, and the first request from a tab left open would otherwise build the person a new team.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Launch blocker: the app had no privacy policy (
app.shell.online/privacyfell through to sign-in) and no way to delete an account.Privacy policy (
/privacy). A public page laid out and styled like the Terms. It covers what the service keeps, what it never receives, what a team sees, the processors (Google: Firebase Auth and Cloud SQL; Cloudflare; Twilio SendGrid), email, browser storage, analytics, retention, deletion, rights, security, transfers and children. Sign-in, sign-up (the acceptance checkbox), the Terms (lede, Termination, footer) and the Account page link to it. Each statement was checked against the code, for example: SendGrid tracking is disabled in the send body, the rate limiter keeps addresses in memory only, the landing analytics writes no IPs, and Cloud SQL keeps 7 backups plus 7 days of logs.Account deletion. A Delete account button on the Account page opens a panel. The panel lists exactly what will happen, including who becomes owner. The button stays disabled until the account's email is typed back and, for email accounts, a password is entered.
DELETE /api/accountchecks the typed email and a sign-in within 10 minutes (the vault-reset rule). It then runsStore.deleteAccountin one transaction:actor_emailset todeleted account.deleteUser.The service goes first on purpose. If
deleteUserfails, what's left is an empty Firebase account, and deleting again finishes the job (the route is idempotent). The reverse order would strand data under an account nobody can sign into.Tombstone (
008_deleted_accounts.sql). An ID token minted before the deletion still verifies for up to an hour. Without a guard, the first background request from another open tab would reachensureMembership, find no membership, and create a new team for the person.ensureMembershipnow returnsnullfor a uid deleted in the last two hours, and those routes answer 401.purgeExpireddrops the row after that, so only the uid is ever kept.Deploy note
Migration
008_deleted_accounts.sqlmust be applied (npm run db:migrate) before the Worker goes out. The new code queriesdeleted_accountsfromensureMembership, which is on the create-membership path.Verification
npm --prefix app test: 755 passed, 3 skipped (742 onmain). New tests:app/server/app.test.ts,DELETE /api/account:store-conformance.test.ts, "account deletion", run against both stores:successorForinorgs.test.tsandsrc/lib/account-deletion.test.tspglite-server. All 4 account-deletion conformance tests pass onPostgresStore. Across the whole suite on PGlite, 131/134 pass. The 3 failures are the existing concurrent "claiming an organization" race tests, which hit PGlite socket protocol errors ("unexpected commandComplete message from backend"). They are unrelated to this change and worth confirming on a real Postgres in CI.tsc -b --forceinapp/: clean.oxlint: no new warnings. TheAuthProviderfast-refresh warning is also present onmain./privacyrenders with 17 numbered sectionsFor the owner to decide or check
shell.online/privacy, on the landing and relay Worker, still returns the landing page. The policy lives atapp.shell.online/privacy. A redirect there needs a relay deploy, so it is not in this PR.Changelog
Two entries under Unreleased → Added.