{
 "id": "5.10",
 "title": "Connectors",
 "department": "Engineering",
 "department_slug": "engineering",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Each account, scopes, refresh, protected connections",
 "urls": {
  "html": "https://handbook.joinsquareup.com/engineering/connectors",
  "markdown": "https://handbook.joinsquareup.com/engineering/connectors.md",
  "json": "https://handbook.joinsquareup.com/engineering/connectors.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Read only connectors on a fixed cadence, with Arata's protected in code",
  "points": [
   {
    "icon": "link",
    "text": "Shopify, Meta, Google Ads, GA4, Klaviyo, Judge.me and Slack; Marketplaces stay off."
   },
   {
    "icon": "clock",
    "text": "Reads run every 3 to 12 hours, plus a re-read when a member opens the brand."
   },
   {
    "icon": "warn",
    "text": "After 3 failures or four missed cadences, numbers go stale and stop being evidence."
   },
   {
    "icon": "lock",
    "text": "Tokens are encrypted, and Arata's live connections are never disconnected."
   },
   {
    "icon": "flag",
    "text": "Connected numbers are the team's source of truth, for the team only."
   }
  ],
  "stat": {
   "value": "3",
   "label": "failed reads mark data stale"
  },
  "decide": "Who joins Arata on the protected list? Proposal: every partner once an account connects.",
  "mood": "solid"
 },
 "sources": [
  "brew origin/main: connectors/index.js:1-72, 113-330, 429; connectors/protected.js; connectors/shopify.js:8-20, 92; connectors/meta.js:1-12; connectors/google.js:7-29, 182-200; connectors/klaviyo.js:1-3; connectors/reviews.js:1-14; connectors/slack.js:11; vault.js:1-12; shopify-app.js:27",
  "brew origin/main: SCOPE.md (Connectors row); DUMMY.md rows 1, 2, 7; docs/shopify.app.toml; docs/slack-app-manifest.json; deploy.sh:39",
  "brew/.wt-prod/docs/production.md (sign-ins and outside settings)",
  "memory brew-real-connectors, brew-connected-facts, never-disconnect-arata-meta, brew-meta-connector-qa, brew-shopify-connector-qa, brew-google-oauth-scopes, brew-shopify-app-review, brew-meta-app-review, brew-connect-refresh, brew-prod-backend"
 ],
 "open_questions": [
  "**When should Marketplaces become real?** Proposal: after Meta App Review, starting with Amazon seller data for India.",
  "**Who besides Arata goes on the protected list?** Proposal: every design partner once their first account connects."
 ],
 "parts": {
  "Answer": "Connectors read a brand's own accounts, read only, encrypt every token, refresh on a fixed cadence per account, and make the team's numbers the source of truth for the team only; Arata's live connections are protected in code and are never disconnected.",
  "Today": "### The accounts\n\n| Connector | How it connects | Scopes or access | Read every | Source |\n|---|---|---|---|---|\n| Shopify | The brand's own app (client id and secret) through `own-oauth`; the public app \"Brew\" is under review | `read_products,read_inventory`; `read_orders,read_customers` with protected-data approval or the own app | 3 h | [src: brew/connectors/shopify.js:11-13, 92] [src: brew/connectors/index.js:37] [src: memory brew-shopify-connector-qa] |\n| Meta Ads | Facebook Login for Business on SquareUp's Meta app (config 2157080375167128), one or more ad accounts | `ads_read`, `business_management`; Graph v26.0 | 3 h | [src: brew/connectors/meta.js:1-12] [src: brew/deploy.sh:39] |\n| Google Ads | Google sign-in with the developer token (`GOOGLE_OAUTH=1`) | `adwords`, `analytics.readonly`, openid, email | 6 h | [src: brew/connectors/google.js:18] [src: memory brew-google-oauth-scopes] |\n| Google Analytics | Same sign-in, or a property ID with Brew's service account added as Viewer | `analytics.readonly` | 6 h | [src: brew/connectors/google.js:29, 182] |\n| Klaviyo | A pasted read-only private key | lists, sizes, the five flows | 12 h | [src: brew/connectors/klaviyo.js:1-3] |\n| Judge.me | OAuth or the store's token | `read_reviews read_shops` | 6 h | [src: brew/connectors/reviews.js:1-14] |\n| Slack | OAuth on the Brew Slack app | `chat:write`, `chat:write.public`, `incoming-webhook`, `channels:join`, `channels:read`, `groups:read`, `channels:history`, `groups:history`, `users:read`, `users:read.email`, `team:read` | event-driven | [src: brew/connectors/slack.js:11] |\n| Marketplaces | Off; shown as \"Coming soon\" | | | [src: brew/connectors/index.js:32] [src: brew/DUMMY.md] |\n\nSlack is never evidence: no cards, no ledger. [src: brew/connectors/index.js:5-7]\n\n### Freshness and failure\n\n- A member opening the brand re-reads an account due within `OPEN_H` (Shopify and Meta 1 h, Google and GA4 and Judge.me 2 h, Klaviyo 6 h). [src: brew/connectors/index.js:40, 315]\n- A failed read waits for the platform's Retry-After, else doubles from 15 minutes. [src: brew/connectors/index.js:44-50]\n- After 3 failures in a row, or once the last good read is older than four cadences (at least 24 hours), numbers are marked stale and stop being evidence. Owners and admins hear a week before a Meta sign-in ends. [src: brew/connectors/index.js:51-60]\n- A new connector needs entries in `EVERY_H`, `OPEN_H`, `NAMES` and `PRECEDENCE`. [src: memory brew-connected-facts]\n\n### Who sees what\n\n- Tokens are AES-256-GCM in `private/vault/`, never sent to the page, never logged. [src: brew/vault.js:1-3]\n- Account reads live in `private/live/<id>.json`; numbers reach only members. A visitor never gets one. [src: brew/connectors/index.js:1-4] [src: memory brew-connected-facts]\n- On the fact sheet, connected numbers beat the site read for members; demo and stale rows never count. [src: memory brew-connected-facts]\n- Only admins (owner or staff) connect or disconnect; teammates can sync. [src: memory brew-real-connectors]\n- A GA4 property already used by another brand, or whose web stream is not the brand's site, is refused unless staff override; 6 tries per person per 15 minutes. [src: brew/SCOPE.md]\n\n### Protected connections\n\n`PROTECTED_CONNECTIONS` (default `arata-in`, set on production) refuses disconnect, re-select, a new key or a reconnect unless a root admin forces it; the vault refuses to drop the token too. [src: brew/connectors/protected.js] [src: brew/connectors/index.js:148-150, 267-269, 429] [src: brew/tools/prod-env.sh:9]\n\nRules around it:\n- Never call `DELETE /me/permissions` on disconnect: one Meta grant may serve several brands. [src: memory brew-meta-connector-qa]\n- Disconnect tests run on Cozy only. [src: memory never-disconnect-arata-meta]\n- Arata's Shopify token (own app) never expires and Meta has no refresh, so the same vault serves both sites; connector reads run on both. [src: memory brew-prod-backend]\n\n### What a connection changes\n\nThe first connected account rewrites every Play privately (cap $40); later accounts rewrite only the Plays they touch. Best sellers become heroes in the team's view, and readings fill the health rings. [src: memory brew-connect-refresh] [src: brew/on-connect.js:1-3]\n\n### The demo\n\nOn the demo every connector is simulated from the brand's public read, tagged Dummy, and never evidence. [src: brew/DUMMY.md] [src: brew/connectors/index.js:67]\n\n### Approvals\n\n| Platform | State | Source |\n|---|---|---|\n| Google | Verified 21 Oct for `adwords` and `analytics.readonly`; a new scope re-triggers review | [src: memory brew-google-oauth-scopes] |\n| Shopify public app | Submitted 21 Oct; after approval deploy with `SHOPIFY_OAUTH=1` and the install URL | [src: memory brew-shopify-app-review] |\n| Meta App Review | Draft filled 24 Oct; blocked on business verification and two screencasts | [src: memory brew-meta-app-review] |\n\nBoth sites' callbacks (`/auth/google/callback`, `/connect/<id>/callback`) are registered with Google, Meta and Slack; Slack events go to production, which relays workspaces it does not serve to staging. [src: brew/.wt-prod/docs/production.md]",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Read only, every connector | v1 | Stays |\n| One-click Shopify through the public app | v1 | Changes after Shopify approves |\n| Meta for any brand (out of development tier) | v1 | Changes after App Review |\n| Connections and secrets in their own table | v2 | New |\n| Marketplace seller APIs | v3 | New |\n| Write scopes (post, pause, change budgets) | v3 | New, only behind a person's yes per action |",
  "How we do it": "1. Audit what Brew shows against the platform, read only:\n\n```bash\nnode tools/meta-audit.mjs <slug>      # needs FEED_VAULT_KEY and the Meta app secrets\nnode tools/shopify-audit.mjs <slug>   # needs FEED_VAULT_KEY\n```\n\n[src: memory brew-meta-connector-qa] [src: memory brew-shopify-connector-qa]\n\n2. Check that connectors still read on both sites:\n\n```bash\ngcloud run jobs execute brew-prod-ping --region=asia-south1 --wait\ngcloud run jobs execute brew-staging-ping --region=asia-south1 --wait\n```\n\n[src: memory brew-prod-backend]\n\n3. Any script touching `private/vault/meta.json`, `connections.json` or disconnect must skip `arata-in`. [src: memory never-disconnect-arata-meta]\n4. Do not hammer syncs; Meta throttles \"too many calls to this ad-account\". [src: memory brew-brand-book]\n5. Bump `META_GRAPH_VERSION` and `GOOGLE_ADS_API_VERSION` when the platforms announce a sunset. [src: memory brew-meta-connector-qa] [src: memory brew-real-connectors]",
  "Numbers": "| Metric | Definition | Where measured |\n|---|---|---|\n| Live connections | Connections in state live, per brand | `connections.json`; `db/status.mjs` |\n| Stale connections | Connections marked stale | same |\n| Read success rate | Good reads over all reads per connector | Proposed (from `connections.json` fails) |",
  "Open": "1. **When should Marketplaces become real?** Proposal: after Meta App Review, starting with Amazon seller data for India.\n2. **Who besides Arata goes on the protected list?** Proposal: every design partner once their first account connects."
 }
}