Answer
What we keep, for how long and how it is deleted; today most brand data is kept with no end date until someone asks, and this document proposes the periods the privacy policy will state.
Today
What is kept, and for how long
| Data | Where | Kept | Source |
|---|---|---|---|
Session cookie fs_s |
The person's browser | 30 days | brew/auth.js:245 |
Sign in handshake fs_g |
The person's browser | 10 minutes | brew/auth.js:225 |
| Session rows: email, browser, times | sessions table |
Deleted 7 days after they expire | brew/sessions.js:34 |
| Who opened which brand | access_log table, and visitors.json on bucket sites |
No end date; logging began 23 Oct | brew/db/migrations/003_access.sql:19-30 memory brew-access-rules |
| Invites | private/members.json |
Count for 14 days; up to 50 kept | brew/auth.js:79, 102, 187 |
| The read, facts, Plays, cards, memory, jams, briefs | Brand rows in Cloud SQL, binaries in gs://brew-prod |
No end date | handbook content/engineering/data-model.md |
| Stories | Research pool | 24 hours on the ring; 30 days and the newest 80 in the archive | memory brew-stories-and-cards |
| Build progress lines | build_events |
3 days | brew/server.js:724 |
| Connected account keys | private/vault/<id>.json, encrypted |
Until disconnect, which blanks the key | brew/vault.js:20-30 |
| A Shopify store's key and numbers | Vault and live rows | Dropped on shop/redact, 48 hours after uninstall, unless installed again |
brew/shopify-app.js:31, 204-216 |
| Per customer Shopify records | None kept | Nothing to keep | brew/shopify-app.js:18-19, 218 |
| A record of each deletion | data_deletions table: slug, request, operator, times, counts by folder, grants, what backups still hold; never contents or member names |
No end date; on staging now, production with the next promote | brew/db/migrations/010_data_deletions.sql:1-18 brew/docs/production.md:95-97 |
| Production database backups | Cloud SQL | Point in time 7 days; daily backups, 14 kept; one before every promote | brew/docs/production.md:37-39 |
| Nightly export | gs://brew-prod-backups/nightly/brew.sql.gz |
Overwritten each night | brew/docs/production.md:40 |
| Files | gs://brew-prod |
Old versions kept; 7 days soft delete | brew/docs/production.md:12, 41 |
| Staging bucket old versions | gs://brew-prototype |
Deleted after 30 days | memory brew-storage |
| Demo visitor sandboxes | demo/<sid>/ |
Never cleaned, as of the 15 Oct audit | brew/docs/handover/engineering/audits/E-accounts-demo-security.md:29 |
How deletion works
- Delete my data opens an email to hello@joinsquareup.com and promises deletion within 7 days. brew/public/live.js:341-351
- The privacy page names what goes, the read, Plays, jams and connected keys, with no time. brew/legal-pages.js:34
db/delete-brand.mjsdeletes a brand for a data request on a Postgres site: every row under the brand with our stored tokens, workspaces,_deleted/copies, build progress, jobs, bus, counters, access log rows, every bucket version of its files, and media only it points at. Dry run until--yes. brew/db/delete-brand.mjs:1-8; brew/db/brand-wipe.js:5-6- It keeps no archive, only the
data_deletionsrow, and it writes that row before it starts, so an unfinished run shows. brew/db/brand-wipe.js:187-195, 222 - File deletes remove every old version too, so object versioning keeps no copy. brew/db/brand-wipe.js:206-209
- What still holds the brand for a while is printed for the reply: 7 days of point in time logs, 14 daily backups, the nightly export, and the bucket's 7 day soft delete. brew/db/brand-wipe.js:236-238
- It refuses the five partners unless
--partner-override --confirm-slug <slug>, and never deletes a brand inPROTECTED_CONNECTIONS. brew/db/brand-wipe.js:27-30 - Production has no
brew-prod-opsjob and nodata_deletionstable yet; both come with the next promote. Until then a brand on production is deleted by hand. brew/docs/production.md:95-106; deploy state 26 Oct tools/reset-brand.shis bucket era and exits 3 on Postgres. brew/tools/reset-brand.sh:11-19db/reset-jams.mjsclears jams only, never connections. brew/db/reset-jams.mjs:36, 53- Disconnecting inside the product, and the deletion tool, never revoke the grant on Shopify, Meta, Google, Slack or Klaviyo; the tool prints where the brand revokes each one. memory brew-wipe-data brew/db/brand-wipe.js:74-84, 240
- The five partners are never wiped unless they ask, and Arata's Meta and Shopify connections are protected. memory brew-live-stores memory never-disconnect-arata-meta
Tomorrow
Proposed periods
| Data | Proposed period | Version | Tag |
|---|---|---|---|
| Brand data | While the brand uses the service; deleted within 7 days of a request; a brand with no sign in for 12 months gets an email, then deletion 30 days later | v1 | New |
| Access log | 12 months, then deleted | v1 | New |
| Session rows | As today: 30 days plus 7 | v1 | Stays |
| Deletion audit row | Kept as proof of the deletion; no contents | v1 | Stays |
| Backups | As today: 14 days; a deleted brand is never restored from them | v1 | Stays |
| Demo sandboxes | 7 days after the last visit | v1 | New |
| Raw page fetches | 90 days | v1 | New, matches the data model proposal |
| Posts and schedules the brand approves | While the brand uses the service | v2 | New |
| Measured results | While the brand uses the service; they teach only that brand | v3 | New |
Deletion tools
| Item | Version | Tag |
|---|---|---|
db/delete-brand.mjs: a dry run, then removes one brand's rows, parts, files and keys, with an audit row and no archive. On origin/main; production after the next promote |
v1 | Stays |
| Delete one person: sign in, member rows, mail settings, jam authorship | v1 | New, proposed |
| Self serve deletion for admins | v2 | New |
How we do it
- A request arrives at hello@; follow the runbook in data requests.
- Never delete a partner who did not ask. Never touch Arata's protected connections without Param.
- After a deletion, send the person the grants the run printed, to revoke themselves, and what backups still hold for up to 14 days.
- Check the access log size once a month until the 12 month purge exists:
gcloud run jobs execute brew-ops --region=asia-south1 --wait
db/status.mjs prints table sizes, access_log among them. memory brew-prod-backend
Numbers
| Metric | Definition | Where measured | Status |
|---|---|---|---|
| Days to delete | Days from request to done reply | hello@ inbox | Proposed |
| Data with no end date | Rows in the first table with no period | This page | 6 on 26 Oct |
| Access log rows | Rows in access_log |
db/status.mjs |
Proposed |
Open
- Brand data with no end date: do we set one? Proposal: 12 months without any sign in, then an email to admins, then deletion 30 days later.
- How long do we keep the access log? Proposal: 12 months.
- Do restored backups bring deleted brands back? Proposal: keep a list of deleted slugs and delete them again after any restore.