{
 "id": "9.5",
 "title": "Retention and deletion",
 "department": "Security, privacy and legal",
 "department_slug": "security-privacy-and-legal",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "2026-10-26",
 "holds": "What we keep, how long, how to delete",
 "urls": {
  "html": "https://handbook.joinsquareup.com/security-privacy-and-legal/retention-and-deletion",
  "markdown": "https://handbook.joinsquareup.com/security-privacy-and-legal/retention-and-deletion.md",
  "json": "https://handbook.joinsquareup.com/security-privacy-and-legal/retention-and-deletion.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Most brand data is kept until someone asks us to delete it",
  "points": [
   {
    "icon": "warn",
    "text": "Six kinds of data have no end date, brand rows and the access log among them."
   },
   {
    "icon": "clock",
    "text": "Delete my data promises 7 days; the privacy page gives no time."
   },
   {
    "icon": "lock",
    "text": "The delete tool keeps no archive, only a record that a deletion happened."
   },
   {
    "icon": "box",
    "text": "Backups hold a deleted brand for up to 14 days, and are never restored for it."
   },
   {
    "icon": "flag",
    "text": "Production has no deletion job yet; it arrives with the next promote."
   }
  ],
  "stat": {
   "value": "7",
   "label": "day deletion promise"
  },
  "decide": "Set brand data to end after 12 months without a sign in, with an email first?",
  "mood": "waiting"
 },
 "sources": [
  "brew/sessions.js:19-34",
  "brew/auth.js:79, 102, 225, 245",
  "brew/db/migrations/003_access.sql:2-30",
  "brew/vault.js:20-30",
  "brew/shopify-app.js:18-19, 31, 204-218",
  "brew/docs/production.md:12, 35-41",
  "brew/public/live.js:341-351",
  "brew/legal-pages.js:34",
  "brew/tools/reset-brand.sh:11-19",
  "brew/db/delete-brand.mjs:1-10",
  "brew/db/brand-wipe.js:5-9, 27-30, 74-84, 187-195, 206-209, 236-240",
  "brew/db/migrations/010_data_deletions.sql:1-18",
  "brew/docs/production.md:95-110",
  "brew commit e3e0fb4; deploy state 26 Oct",
  "brew/db/reset-jams.mjs:36, 53",
  "brew/server.js:724",
  "brew/docs/handover/engineering/audits/E-accounts-demo-security.md:29",
  "memory brew-storage, brew-wipe-data, brew-stories-and-cards, brew-access-rules, brew-live-stores, never-disconnect-arata-meta, prod-clean-only",
  "handbook content/customer-success-and-support/data-requests.md, content/engineering/data-model.md"
 ],
 "open_questions": [
  "**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."
 ],
 "parts": {
  "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\n\n| Data | Where | Kept | Source |\n|---|---|---|---|\n| Session cookie `fs_s` | The person's browser | 30 days | [src: brew/auth.js:245] |\n| Sign in handshake `fs_g` | The person's browser | 10 minutes | [src: brew/auth.js:225] |\n| Session rows: email, browser, times | `sessions` table | Deleted 7 days after they expire | [src: brew/sessions.js:34] |\n| Who opened which brand | `access_log` table, and `visitors.json` on bucket sites | No end date; logging began 23 Oct | [src: brew/db/migrations/003_access.sql:19-30] [src: memory brew-access-rules] |\n| Invites | `private/members.json` | Count for 14 days; up to 50 kept | [src: brew/auth.js:79, 102, 187] |\n| The read, facts, Plays, cards, memory, jams, briefs | Brand rows in Cloud SQL, binaries in `gs://brew-prod` | No end date | [src: handbook content/engineering/data-model.md] |\n| Stories | Research pool | 24 hours on the ring; 30 days and the newest 80 in the archive | [src: memory brew-stories-and-cards] |\n| Build progress lines | `build_events` | 3 days | [src: brew/server.js:724] |\n| Connected account keys | `private/vault/<id>.json`, encrypted | Until disconnect, which blanks the key | [src: brew/vault.js:20-30] |\n| A Shopify store's key and numbers | Vault and live rows | Dropped on `shop/redact`, 48 hours after uninstall, unless installed again | [src: brew/shopify-app.js:31, 204-216] |\n| Per customer Shopify records | None kept | Nothing to keep | [src: brew/shopify-app.js:18-19, 218] |\n| 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 | [src: brew/db/migrations/010_data_deletions.sql:1-18] [src: brew/docs/production.md:95-97] |\n| Production database backups | Cloud SQL | Point in time 7 days; daily backups, 14 kept; one before every promote | [src: brew/docs/production.md:37-39] |\n| Nightly export | `gs://brew-prod-backups/nightly/brew.sql.gz` | Overwritten each night | [src: brew/docs/production.md:40] |\n| Files | `gs://brew-prod` | Old versions kept; 7 days soft delete | [src: brew/docs/production.md:12, 41] |\n| Staging bucket old versions | `gs://brew-prototype` | Deleted after 30 days | [src: memory brew-storage] |\n| Demo visitor sandboxes | `demo/<sid>/` | Never cleaned, as of the 15 Oct audit | [src: brew/docs/handover/engineering/audits/E-accounts-demo-security.md:29] |\n\n### How deletion works\n\n- Delete my data opens an email to hello@joinsquareup.com and promises deletion within 7 days. [src: brew/public/live.js:341-351]\n- The privacy page names what goes, the read, Plays, jams and connected keys, with no time. [src: brew/legal-pages.js:34]\n- `db/delete-brand.mjs` deletes 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`. [src: brew/db/delete-brand.mjs:1-8; brew/db/brand-wipe.js:5-6]\n- It keeps no archive, only the `data_deletions` row, and it writes that row before it starts, so an unfinished run shows. [src: brew/db/brand-wipe.js:187-195, 222]\n- File deletes remove every old version too, so object versioning keeps no copy. [src: brew/db/brand-wipe.js:206-209]\n- 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. [src: brew/db/brand-wipe.js:236-238]\n- It refuses the five partners unless `--partner-override --confirm-slug <slug>`, and never deletes a brand in `PROTECTED_CONNECTIONS`. [src: brew/db/brand-wipe.js:27-30]\n- Production has no `brew-prod-ops` job and no `data_deletions` table yet; both come with the next promote. Until then a brand on production is deleted by hand. [src: brew/docs/production.md:95-106; deploy state 26 Oct]\n- `tools/reset-brand.sh` is bucket era and exits 3 on Postgres. [src: brew/tools/reset-brand.sh:11-19]\n- `db/reset-jams.mjs` clears jams only, never connections. [src: brew/db/reset-jams.mjs:36, 53]\n- 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. [src: memory brew-wipe-data] [src: brew/db/brand-wipe.js:74-84, 240]\n- The five partners are never wiped unless they ask, and Arata's Meta and Shopify connections are protected. [src: memory brew-live-stores] [src: memory never-disconnect-arata-meta]",
  "Tomorrow": "### Proposed periods\n\n| Data | Proposed period | Version | Tag |\n|---|---|---|---|\n| 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 |\n| Access log | 12 months, then deleted | v1 | New |\n| Session rows | As today: 30 days plus 7 | v1 | Stays |\n| Deletion audit row | Kept as proof of the deletion; no contents | v1 | Stays |\n| Backups | As today: 14 days; a deleted brand is never restored from them | v1 | Stays |\n| Demo sandboxes | 7 days after the last visit | v1 | New |\n| Raw page fetches | 90 days | v1 | New, matches the data model proposal |\n| Posts and schedules the brand approves | While the brand uses the service | v2 | New |\n| Measured results | While the brand uses the service; they teach only that brand | v3 | New |\n\n### Deletion tools\n\n| Item | Version | Tag |\n|---|---|---|\n| `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 |\n| Delete one person: sign in, member rows, mail settings, jam authorship | v1 | New, proposed |\n| Self serve deletion for admins | v2 | New |",
  "How we do it": "1. A request arrives at hello@; follow the runbook in data requests.\n2. Never delete a partner who did not ask. Never touch Arata's protected connections without Param.\n3. After a deletion, send the person the grants the run printed, to revoke themselves, and what backups still hold for up to 14 days.\n4. Check the access log size once a month until the 12 month purge exists:\n\n```bash\ngcloud run jobs execute brew-ops --region=asia-south1 --wait\n```\n\n`db/status.mjs` prints table sizes, `access_log` among them. [src: memory brew-prod-backend]",
  "Numbers": "| Metric | Definition | Where measured | Status |\n|---|---|---|---|\n| Days to delete | Days from request to done reply | hello@ inbox | Proposed |\n| Data with no end date | Rows in the first table with no period | This page | 6 on 26 Oct |\n| Access log rows | Rows in `access_log` | `db/status.mjs` | Proposed |",
  "Open": "1. **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.\n2. **How long do we keep the access log?** Proposal: 12 months.\n3. **Do restored backups bring deleted brands back?** Proposal: keep a list of deleted slugs and delete them again after any restore."
 }
}