---
id: "5.10"
title: "Connectors"
dept: engineering
status: draft
owner: Param
branded: false
last_verified: 2026-10-26
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"
---

# Connectors

## 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

| Connector | How it connects | Scopes or access | Read every | Source |
|---|---|---|---|---|
| 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] |
| 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] |
| 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] |
| 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] |
| Klaviyo | A pasted read-only private key | lists, sizes, the five flows | 12 h | [src: brew/connectors/klaviyo.js:1-3] |
| Judge.me | OAuth or the store's token | `read_reviews read_shops` | 6 h | [src: brew/connectors/reviews.js:1-14] |
| 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] |
| Marketplaces | Off; shown as "Coming soon" | | | [src: brew/connectors/index.js:32] [src: brew/DUMMY.md] |

Slack is never evidence: no cards, no ledger. [src: brew/connectors/index.js:5-7]

### Freshness and failure

- 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]
- A failed read waits for the platform's Retry-After, else doubles from 15 minutes. [src: brew/connectors/index.js:44-50]
- 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]
- A new connector needs entries in `EVERY_H`, `OPEN_H`, `NAMES` and `PRECEDENCE`. [src: memory brew-connected-facts]

### Who sees what

- Tokens are AES-256-GCM in `private/vault/`, never sent to the page, never logged. [src: brew/vault.js:1-3]
- 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]
- On the fact sheet, connected numbers beat the site read for members; demo and stale rows never count. [src: memory brew-connected-facts]
- Only admins (owner or staff) connect or disconnect; teammates can sync. [src: memory brew-real-connectors]
- 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]

### Protected connections

`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]

Rules around it:
- Never call `DELETE /me/permissions` on disconnect: one Meta grant may serve several brands. [src: memory brew-meta-connector-qa]
- Disconnect tests run on Cozy only. [src: memory never-disconnect-arata-meta]
- 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]

### What a connection changes

The 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]

### The demo

On 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]

### Approvals

| Platform | State | Source |
|---|---|---|
| Google | Verified 21 Oct for `adwords` and `analytics.readonly`; a new scope re-triggers review | [src: memory brew-google-oauth-scopes] |
| Shopify public app | Submitted 21 Oct; after approval deploy with `SHOPIFY_OAUTH=1` and the install URL | [src: memory brew-shopify-app-review] |
| Meta App Review | Draft filled 24 Oct; blocked on business verification and two screencasts | [src: memory brew-meta-app-review] |

Both 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 |
|---|---|---|
| Read only, every connector | v1 | Stays |
| One-click Shopify through the public app | v1 | Changes after Shopify approves |
| Meta for any brand (out of development tier) | v1 | Changes after App Review |
| Connections and secrets in their own table | v2 | New |
| Marketplace seller APIs | v3 | New |
| 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:

```bash
node tools/meta-audit.mjs <slug>      # needs FEED_VAULT_KEY and the Meta app secrets
node tools/shopify-audit.mjs <slug>   # needs FEED_VAULT_KEY
```

[src: memory brew-meta-connector-qa] [src: memory brew-shopify-connector-qa]

2. Check that connectors still read on both sites:

```bash
gcloud run jobs execute brew-prod-ping --region=asia-south1 --wait
gcloud run jobs execute brew-staging-ping --region=asia-south1 --wait
```

[src: memory brew-prod-backend]

3. Any script touching `private/vault/meta.json`, `connections.json` or disconnect must skip `arata-in`. [src: memory never-disconnect-arata-meta]
4. Do not hammer syncs; Meta throttles "too many calls to this ad-account". [src: memory brew-brand-book]
5. 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 |
|---|---|---|
| Live connections | Connections in state live, per brand | `connections.json`; `db/status.mjs` |
| Stale connections | Connections marked stale | same |
| 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.
2. **Who besides Arata goes on the protected list?** Proposal: every design partner once their first account connects.
