---
id: "9.5"
title: "Retention and deletion"
dept: security-privacy-and-legal
status: draft
owner: Param
branded: false
last_verified: "2026-10-26"
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"
---

# Retention and deletion

## 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 | [src: brew/auth.js:245] |
| Sign in handshake `fs_g` | The person's browser | 10 minutes | [src: brew/auth.js:225] |
| Session rows: email, browser, times | `sessions` table | Deleted 7 days after they expire | [src: brew/sessions.js:34] |
| 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] |
| Invites | `private/members.json` | Count for 14 days; up to 50 kept | [src: 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 | [src: handbook content/engineering/data-model.md] |
| Stories | Research pool | 24 hours on the ring; 30 days and the newest 80 in the archive | [src: memory brew-stories-and-cards] |
| Build progress lines | `build_events` | 3 days | [src: brew/server.js:724] |
| Connected account keys | `private/vault/<id>.json`, encrypted | Until disconnect, which blanks the key | [src: 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 | [src: brew/shopify-app.js:31, 204-216] |
| Per customer Shopify records | None kept | Nothing to keep | [src: 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 | [src: brew/db/migrations/010_data_deletions.sql:1-18] [src: brew/docs/production.md:95-97] |
| 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] |
| Nightly export | `gs://brew-prod-backups/nightly/brew.sql.gz` | Overwritten each night | [src: brew/docs/production.md:40] |
| Files | `gs://brew-prod` | Old versions kept; 7 days soft delete | [src: brew/docs/production.md:12, 41] |
| Staging bucket old versions | `gs://brew-prototype` | Deleted after 30 days | [src: memory brew-storage] |
| 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] |

### How deletion works

- Delete my data opens an email to hello@joinsquareup.com and promises deletion within 7 days. [src: brew/public/live.js:341-351]
- The privacy page names what goes, the read, Plays, jams and connected keys, with no time. [src: brew/legal-pages.js:34]
- `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]
- 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]
- File deletes remove every old version too, so object versioning keeps no copy. [src: 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. [src: brew/db/brand-wipe.js:236-238]
- 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]
- 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]
- `tools/reset-brand.sh` is bucket era and exits 3 on Postgres. [src: brew/tools/reset-brand.sh:11-19]
- `db/reset-jams.mjs` clears jams only, never connections. [src: 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. [src: memory brew-wipe-data] [src: 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. [src: memory brew-live-stores] [src: 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

1. A request arrives at hello@; follow the runbook in data requests.
2. Never delete a partner who did not ask. Never touch Arata's protected connections without Param.
3. After a deletion, send the person the grants the run printed, to revoke themselves, and what backups still hold for up to 14 days.
4. Check the access log size once a month until the 12 month purge exists:

```bash
gcloud run jobs execute brew-ops --region=asia-south1 --wait
```

`db/status.mjs` prints table sizes, `access_log` among them. [src: 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

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.
2. **How long do we keep the access log?** Proposal: 12 months.
3. **Do restored backups bring deleted brands back?** Proposal: keep a list of deleted slugs and delete them again after any restore.
