{
 "id": "5.16",
 "title": "Tech debt",
 "department": "Engineering",
 "department_slug": "engineering",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "2026-10-26",
 "holds": "Known debt, its risk, the fix",
 "urls": {
  "html": "https://handbook.joinsquareup.com/engineering/tech-debt",
  "markdown": "https://handbook.joinsquareup.com/engineering/tech-debt.md",
  "json": "https://handbook.joinsquareup.com/engineering/tech-debt.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Known debt, ranked by risk, with the riskiest paid first",
  "points": [
   {
    "icon": "warn",
    "text": "D-1: the default compute service account holds project wide secret access."
   },
   {
    "icon": "lock",
    "text": "D-2 and D-3: shared links never expire, and no route table tests who may call what."
   },
   {
    "icon": "box",
    "text": "D-7 to D-10: one worker, no domain tables, and the demo on a second storage path."
   },
   {
    "icon": "chart",
    "text": "Debt today: 3 High, 16 Medium and 11 Low."
   },
   {
    "icon": "flag",
    "text": "Pay one item a week and strike it with a commit."
   }
  ],
  "stat": {
   "value": "3",
   "label": "high risk debts"
  },
  "decide": "Which High item first? Proposal: D-1 this week, then D-3.",
  "mood": "urgent"
 },
 "sources": [
  "brew origin/main: docs/handover/engineering/11-known-issues-and-debt.md, 08-testing-and-qa.md, 09-security-and-access.md, 10-dummy-to-real.md, audits/F-infra.md (15 Oct; each item rechecked against origin/main b91e364)",
  "brew origin/main: git ls-tree -l (file sizes); deploy.sh:8, 39, 51; moved.js:5-6; jam2/routes.js:1; remix.js:1-6; measure-*.js; tools/reset-brand.sh, tools/reset-jams.sh; handoff.js:18-20; db/migrations; Dockerfile; package.json",
  "brew origin/main: docs/audit-2026-10-24-design-partners.md",
  "brew origin/main d6154b3: .github/workflows/test.yml:1-37, db/reset-brand.mjs:1-13, tools/reset-brand.sh:11-19, tools/flush-live.sh:17-26, tools/reset-jams.sh:1-6; commits fccf3cb, e3e0fb4",
  "memory brew-prod-backend, brew-storage, brew-jam-reset, brew-one-stage, brew-qa-audit-2026-10-20, brew-qa-round2-2026-10-24, brew-jam-rca-2026-10-26, brew-meta-connector-qa, brew-arata-reload-2026-10-20, brew-access-rules"
 ],
 "open_questions": [
  "**Which High item first, now that CI runs?** Proposal: D-1 this week, then D-3.",
  "**Can D-1 be fixed without breaking the other services?** Proposal: list the 15 services and give each its own account first, then remove the default grants."
 ],
 "parts": {
  "Answer": "This is the known debt in Brew on 26 Oct 2026, each item with its source, its risk and the fix, ranked so the riskiest is paid first.",
  "Today": "Risk: **High** can hurt a partner or cost money now; **Medium** slows us or breaks under growth; **Low** is tidiness.\n\n### Security and access\n\n| # | Debt | Risk | Fix | Source |\n|---|---|---|---|---|\n| D-1 | The default compute service account holds project-wide secret access and `run.admin`, affecting about 15 other services | High | Remove those grants from the default account; every service its own account | [src: memory brew-prod-backend] |\n| D-2 | Shared jam and brief links never expire and cannot be revoked; the signature is 12 characters | Medium | Expiring, revocable links | [src: brew/handoff.js:18-20] [src: brew/docs/handover/engineering/09-security-and-access.md] |\n| D-3 | No route registry: about 150+ routes matched by regexes across modules, with no test of who may call each | High | One route table with an auth level per route and one test | [src: brew/docs/handover/engineering/04-api-routes.md] |\n| D-4 | Seats filter cards, not server data | Medium | Enforce views on every route | [src: brew/docs/handover/engineering/09-security-and-access.md] |\n| D-5 | The URL reader and photo fetch: private-range blocking on every redirect hop and Chromium `--no-sandbox` were flagged; photo fetch moved to safeFetch on 15 Oct, the rest is unverified | Medium | Verify, then block private ranges on every hop | [src: brew/docs/handover/engineering/09-security-and-access.md] [src: memory brew-audit-2026-10-15] |\n| D-6 | A personal Gmail is on the admin list on staging (kept by choice) | Low | Already decided; production has one admin | [src: memory brew-access-rules] [src: brew/tools/prod-env.sh:9] |\n\n### Architecture and storage\n\n| # | Debt | Risk | Fix | Source |\n|---|---|---|---|---|\n| D-7 | Only `objects` and `doc_parts` hold brand data; no domain tables for connections, jams, finals or spend | Medium | Real tables, connections first | [src: brew/db/migrations/] [src: memory brew-prod-backend] |\n| D-8 | One worker for every brand's builds | Medium | Per-brand build leases | [src: memory brew-prod-backend] |\n| D-9 | Staging still mirrors every row to the bucket and reads through it | Low | Turn off after a clean month | [src: brew/deploy.sh:39] |\n| D-10 | The demo runs on the bucket with `retire.js`, a second storage path to maintain | Medium | Demo on Postgres, then remove `retire.js` | [src: brew/tools/deploy-demo.sh] [src: brew/retire.js] |\n| D-11 | `tools/reset-jams.sh` is bucket era and still runs against the bucket on a Postgres site. Partly paid 26 Oct: `db/reset-brand.mjs` and `db/delete-brand.mjs` replace the brand tools, and `tools/reset-brand.sh` and `tools/flush-live.sh` exit 3 unless `STORAGE=gcs` | Low | Give `tools/reset-jams.sh` the same guard, then delete all three | [src: memory brew-jam-reset] [src: brew/tools/reset-jams.sh:1-6] [src: brew/tools/reset-brand.sh:11-19] [src: brew/tools/flush-live.sh:17-26] [src: brew commit e3e0fb4] |\n| D-12 | Workspace ids are short random hex and act as the bearer key | Medium | 128-bit ids tied to their owner | [src: brew/docs/handover/engineering/09-security-and-access.md] |\n\n### Code health\n\n| # | Debt | Risk | Fix | Source |\n|---|---|---|---|---|\n| D-13 | Giant files: `public/app.js` 877 KB and 5,997 lines, `feed-service.js` 340 KB, `remix.js` 305 KB, `build.js` 211 KB, `brew.css` 267 KB | Medium | Split by area behind existing tests | [src: git ls-tree -l origin/main] |\n| D-14 | No lint or type checks; the demo deploy runs no tests. Partly paid 26 Oct: GitHub Actions runs the suite on every push and pull request | Medium | Lint and `node --check` in the Action; tests in deploy-demo | [src: brew/.github/workflows/test.yml:1-37] [src: brew/docs/handover/engineering/08-testing-and-qa.md] [src: brew commit fccf3cb] |\n| D-15 | No client tests; no ranking fixtures | Medium | Headless smoke of feed and jam; ranking fixtures from five brands | [src: brew/test/] |\n| D-16 | Cut-feature code still in the tree: measurement modules, rollback and \"irreversible\" fields in the studio, the Pro cap lift | Low | Delete what impact and copy do not use | [src: brew/remix.js:1-6] [src: brew/measure-spec.js] [src: brew/docs/handover/engineering/10-dummy-to-real.md] |\n| D-17 | Jam v2-only routes still mounted after the screen retired; header comments still say \"?v=2 only\" | Low | Remove after checking shared links | [src: brew/jam2/routes.js:1] [src: memory brew-one-stage] |\n| D-18 | Comments that contradict settings: Arata's home in `deploy.sh` and `moved.js` | Low | Fix the comments | [src: brew/deploy.sh:51] [src: brew/moved.js:5-6] |\n| D-19 | `deploy.sh` runs `node --check` on everything but `jam2/` | Low | Add `jam2/*.js` | [src: brew/deploy.sh:8] |\n| D-20 | `brew-name.js` rewrites old names on every read instead of a one-time data migration | Low | Migrate saved data once, then remove | [src: brew/docs/handover/engineering/10-dummy-to-real.md] |\n| D-21 | Two Claude stacks and treg calls that bypass `treg.js` were flagged on 15 Oct | Medium | Recheck; route all through `ai.js` and `treg.js` | [src: brew/docs/handover/engineering/05-ai-and-external-data.md] |\n| D-22 | The external-data `audit.py` lived outside the repo and lagged stage versions | Low | Keep it in the repo and read versions from code | [src: brew/docs/audit-2026-10-24-design-partners.md] |\n\n### Product correctness\n\n| # | Debt | Risk | Fix | Source |\n|---|---|---|---|---|\n| D-23 | Most Plays never reach \"verified\"; flagged claims stay until rewritten | High | Converge the claims check and fix; show claims to review | [src: brew/moves-engine.js:620-626] [src: brew/docs/handover/engineering/11-known-issues-and-debt.md] |\n| D-24 | The same ask repeats across Plays (\"fix the pack prices\" on about six cards) and is deduped only at serve time | Medium | Dedupe at write time | [src: memory brew-arata-reload-2026-10-20] |\n| D-25 | The read is proven mostly on Shopify; custom, bot-protected and size-picker stores are partly untested | Medium | A 15 to 20 store batch read on mocks and cheap partial runs | [src: memory brew-checkout-walk] [src: brew/CLAUDE.md] |\n| D-26 | Cards in \"we\" voice; team named three ways; first look still in the panel; review-quote attribution | Low | Fix in the writers | [src: memory brew-qa-round2-2026-10-24] [src: memory brew-qa-audit-2026-10-20] [src: memory brew-jam-rca-2026-10-26] |\n| D-27 | A PUMA statics picture with drawn placeholder text; maker not traced | Low | Trace and add a look check | [src: brew/docs/audit-2026-10-24-design-partners.md] |\n| D-28 | The Meta budget card can judge campaigns still learning; old shared pages and plain words for team cards need a one-time cleanup | Medium | Learning-phase rule; cleanup with Param's OK | [src: memory brew-meta-connector-qa] |\n\n### Ops\n\n| # | Debt | Risk | Fix | Source |\n|---|---|---|---|---|\n| D-29 | No record of which sha production serves, except revision labels | Medium | Show the sha on `/api/config` and log each promote | [src: brew/tools/promote.sh:119] |\n| D-30 | The first load after a release is slow | Medium | Warm more before the probe passes | [src: memory brew-qa-round2-2026-10-24] |",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| D-1, D-3, D-23 paid before more partners onboard | v1 | New |\n| D-14 finished: lint and checks in CI | v1 | Changes |\n| D-7, D-8, D-10, D-12, D-13 | v2 | New |\n| D-11, D-16 to D-20 | v1 | Goes |",
  "How we do it": "1. Every new debt found goes here with a source, risk and fix the day it is found.\n2. Fixing a debt item means the shared cause, a test or audit, and healed data; then strike it here with the commit. [src: memory fix-as-product]\n3. Review this list at the start of each phase of work.",
  "Numbers": "| Metric | Definition | Where measured |\n|---|---|---|\n| Open debt by risk | Count of High, Medium, Low items | This page: 3 High, 16 Medium, 11 Low today |\n| Debt paid per week | Items struck with a commit | This page (Proposed) |",
  "Open": "1. **Which High item first, now that CI runs?** Proposal: D-1 this week, then D-3.\n2. **Can D-1 be fixed without breaking the other services?** Proposal: list the 15 services and give each its own account first, then remove the default grants."
 }
}