Sub-processor and external-call register
Every external network destination the platform talks to, and whether it is in use today — approval and use are different facts, and a reader could not previously tell them apart. A PR that adds a destination not listed here fails review (AGENTS.md rule 6). Residency column states where data is processed. Exceptions are destinations outside Canada; each must name what leaves and why voter data does not.
| Destination | Purpose | Data sent | Residency | Status | In use (15 September 2026) |
|---|---|---|---|---|---|
| Google Cloud (Cloud Run, Cloud SQL, Storage, KMS, Logging, Error Reporting, Cloud Scheduler) | Platform | All platform data. Error Reporting reads the API's own log lines — stack frames with the error's message withheld, and request identifiers, never a person's details or a query's bound values (decision 1135) — and adds no destination. Cloud Scheduler (0.7.3, 12 September 2026) runs in Montréal because Google does not offer it in Toronto; a timer holds a cron expression and a job name, no data, and the job it starts runs in Toronto | Toronto / Montréal | Approved | Yes — the dev environment is applied and serving |
| Google Cloud Armor and the global external HTTPS load balancer — the firewall and per-path throttles in front of staging and production (story 0.1.6) | Refusing hostile or excessive requests before they reach the platform | Request metadata only — the client's address, the path and the headers of each request — inspected as it arrives. Never a request body, and nothing is stored at the edge: the load balancer's logs are written to the project's own log buckets — _Default, which Google stores globally, outside Canada, until canadian_log_bucket is turned on before production carries a campaign (B-101; infra/README.md, "Log residency") — and a request whose address carries a search term, a cursor or a file's name is dropped before it is stored, by an exclusion that is written and not yet applied (B-193). The platform no longer puts a name in an address at all (story 0.14.4; security review B-138 WEB-2, decisions 1155 to 1159). No campaign record is read or sent | Exception — outside Canada. The firewall runs on Google's worldwide front end, so each request is inspected at whichever Google edge location receives it before it reaches Toronto. The alternative, a regional load balancer, has no such firewall | Approved (Product Owner, 8 October 2026, ask B-70; decision 867). Recorded under his standing instruction to be told whenever anything non-local is used. To be disclosed in the privacy notice; counsel is told in the letter's question 6 | Yes — on staging, since its first apply on 16 September 2026 (the policy has been in infra/modules/edge since the repository's baseline); the per-path throttles story 0.1.6 added on 19 September are among the owed applies (B-87). Production when it is applied (L-21). Dev has no load balancer. This row was written on 8 October 2026, three weeks after the firewall went into use — the same reversal of rule 6's order the register has recorded about itself before (ask B-70 found it on 19 September) |
| Google Cloud Identity Platform (called by the console and, since story 0.9.2, by the phone app) | Authentication | E-mail addresses, phone numbers, password hashes and MFA secrets — of campaign staff and volunteers only. No voter, supporter or contact record is in it, and it holds nothing about a member of the public | Exception — outside Canada. Identity Platform is a global Google service with no Canadian-resident deployment; the platform cannot choose its region | Approved (D1, story 0.3.8). Rationale: it is the directory of who may sign in, the same category as a payroll system, and the alternative is running an authentication service ourselves — which would mean holding password hashes and second factors on a platform whose threat model is a voters’ list. The residency exception buys a smaller attack surface for the data that actually matters. To be disclosed in the privacy notice, which does not yet exist — see the note below the table; the tenant boundary this service enforces is K01 | Yes — the project and two fixed tenants exist, plus one per campaign since 0.3.7, and the Product Owner has signed in on dev with a real credential (14 September). Since story 0.12.1 the definition holds a third fixed tenant, sign-up, in which a stranger proves their address before an organization exists (decision 69) — the same destination and the same category of data, an e-mail address, so a sentence here and no new row; it is applied under L-20 |
Google's own OAuth endpoints (accounts.google.com, oauth2.googleapis.com) — reached by the browser or the phone's system browser during "Continue with Google" (story 0.3.15, 17 September 2026) | Proving who somebody is, so that Identity Platform can accept them without a password | The person's own Google sign-in, conducted between them and Google. Field OP asks for basic scopes only — name, e-mail address and profile picture, with no addScope call — and reads none of the name or the picture out of the token: the invitation's address stays the identity. Campaign staff and volunteers only, the same people Identity Platform already holds. No voter, supporter or contact record is sent, ever | Exception — outside Canada. Google's global sign-in service; there is no Canadian-resident deployment, and it is the same category and the same reasoning as the Identity Platform row above | Approved (Product Owner, decision 110, 16 September 2026; B-48 answered "yes please set these up for me" on 17 September), and the registration is his own — an OAuth client made in Gestalt's name with Google (decision 114). To be disclosed in the privacy notice beside Identity Platform | Yes — live on both dev tenants since midday 17 September (google.com enabled: true). This row was written on 17 September, after the call had already been made — the console's half (#386, #390) added no row, so the order rule 6 exists to enforce was reversed here exactly as it was for the Workspace relay. See the note below the table |
Microsoft Entra ID (login.microsoftonline.com) — reached by the browser or the phone's system browser during "Continue with Microsoft" (story 0.3.15, 17 September 2026) | Proving who somebody is, so that Identity Platform can accept them without a password | The person's own Microsoft sign-in, conducted between them and Microsoft. Basic OpenID Connect scopes only; Field OP reads no name and no picture from the token. Campaign staff and volunteers only. No voter, supporter or contact record is sent, ever. A Microsoft address arrives unverified — Microsoft does not vouch for it — so the platform proves it once by mail, as it does for a password account. (Since 10 October 2026, migration 0123 and decision 1150: a campaign's tenant creates no account from a sign-in, so Microsoft can no longer be somebody's first sign-in — Identity Platform will not join an unvouched address to the account an invitation made. Nothing new is sent to Microsoft.) | Exception — outside Canada, and the first destination on this register that is not Google. Microsoft processes globally; there is no Canadian-resident deployment available to us | Approved (Product Owner, decision 110, 16 September 2026; B-49 answered "yes please set these up for me" on 17 September), and the app registration is his own, made in Gestalt's name with Microsoft (decision 114). To be disclosed in the privacy notice — and see the count below, because this is the fifth residency exception where the draft names four | Yes — live on both dev tenants since midday 17 September (microsoft.com enabled: true). This row was written on 17 September, after the call had already been made, the same reversal as the row above |
| Sign in with Apple (Apple's ID service) — two callers: the iPhone app (story 1.21.1, ADR-040 decision 4), where iOS itself reaches Apple from the system sheet; and the console in a browser (story 1.24.8, decision 725), where the browser goes to Apple's own sign-in page with the campaign's Services ID and Apple sends it back to the console's auth handler — Google's, or the console's own address proxied to it (decision 314) — and Identity Platform exchanges Apple's answer for its own token. Either way Identity Platform then checks Apple's token against Apple's published keys | Proving who somebody is, on an iPhone or in the console, so that Identity Platform can accept them without a password. App Store Review Guideline 4.8 requires it on the iPhone beside Google and Microsoft; the console offers it so that somebody who signs in with Apple on a phone can sign in to the console the same way | The person's own Apple sign-in, conducted between them and Apple — by the operating system on an iPhone, by their own browser on Apple's own page in the console. Field OP asks for the e-mail address only — not the name — and a person may give Apple's relay address instead, which both refuse in words because it can never match an invitation; the console then deletes the Identity Platform account that sign-in had just made, or signs out of one that was there before, as the phone does (decision 402). (Since 10 October 2026, decision 1150, a campaign's tenant creates no account from a sign-in, so a relay address is refused by Identity Platform before any account exists.) What Apple returns goes to Identity Platform, never to Field OP's API. Campaign staff and volunteers only. No voter, supporter or contact record is sent, ever | Exception — outside Canada. Apple processes globally; there is no Canadian-resident deployment. The same category and reasoning as the Google and Microsoft rows above | Approved (Product Owner, 8 October 2026, for the iPhone app and the console; ask B-80 item 5, decision 867). Until then this cell read "Pending — the Product Owner's (ask B-80)". Built and off on both: the iPhone's button appears only on a build told EXPO_PUBLIC_APPLE_SIGN_IN=on, the console's only on one built with NEXT_PUBLIC_APPLE_SIGN_IN=on (an apps/web/Dockerfile argument no deploy workflow passes yet), and either only after configure-sign-in-providers has put the apple.com provider on the tenants — for the console, with a Services ID whose return address is the console's auth handler. Approved, it is one more residency exception to be disclosed in the privacy notice | No — nothing calls it; no build of either has it switched on. It is switched on by ask B-80's steps (the apple-sign-in secret after the owed apply, the configure-sign-in-providers job, and the build setting) |
| Google default mailer (Identity Platform sign-in links) | Sending the single-use sign-in link | The recipient's e-mail address and the link — for campaign staff and volunteers only, the same people Identity Platform already holds. Message content is the platform's own wording; nothing about a voter or a contact | Exception — outside Canada. Google's global mail service; the message leaves from a Google address on a Google domain until a sending domain is verified | Approved (Product Owner, 13 September 2026, decision 53) over decision 38's Canadian-first recommendation; the research stands in docs/decisions/2026-09-12-sign-in-mail.md. To be disclosed in the privacy notice beside Identity Platform | No — approved; the client half of 0.3.7 wires it next |
Google Workspace SMTP relay (smtp-relay.gmail.com, port 587, STARTTLS) | The platform's own mail — everything Field OP composes itself: the staff and administrator invitation and the self-serve welcome (0.3.10, 0.3.12, 0.12.5), the password-reset link an administrator sends (0.3.14), the request to approve a grant (0.14.6), the billing notices to an organization's administrators (0.14.1), shift confirmations and reminders (1.14.2e), and the alert to the people running a canvass when a volunteer on it asks for help (1.15.7) | The recipient's e-mail address and the whole message body. For invitations, welcomes, resets, grant approvals, billing notices and call-for-help alerts the recipient is campaign staff; a call-for-help alert carries the volunteer's name as the campaign knows them, the time, and the campaign, canvass and turf names — never what the volunteer wrote and never a location (2026-10-08, decision 955; no new destination, the same relay). For shift mail the recipient is a volunteer, and that includes a member of the public who signed up at a stall with their e-mail address — so the relay carries that person's address, the canvass name, the shift's time, the meeting point and up to 500 characters of the campaign's note. (Until 30 September 2026 this cell said no member of the public's details were ever carried; that stopped being true with 1.14.2e and was corrected under review M62.) An address from an imported list is not sent shift mail unless its person signed up at a stall for that campaign themselves or holds a granted consent (0.14.10, decision 446). Every message carries the sender identification set out in docs/legal/Email_And_CASL.md | Exception — outside Canada. Google's global relay; Workspace has no Canadian data region at any edition. Residency for campaign data is set aside through launch (decision 61), and the intended destination when it is not set aside is a Canadian provider — apps/api/src/mail/transport.ts is a generic SMTP client that names no vendor, so that is a redeploy rather than a rewrite. Whether a stall sign-up's details may go through it is put to the Product Owner (review M62); the collection notice says they do since fieldop-platform-2026-09-draft-3 | Approved (Product Owner, 15 September 2026, decision 58), docs/decisions/2026-09-15-the-platform-sends-its-own-mail.md. Authenticated by IP address, not by a password — Google's own recommendation for applications, and the reason the worker leaves through a fixed egress address (infra/modules/network). No credential exists anywhere. To be disclosed in the privacy notice beside Identity Platform and its default mailer. Delivery feedback is SMTP-time only: the relay reports no later bounce and noreply@fieldop.ca receives nothing, so a refusal at RCPT TO is recorded and marks the address bounced (0.14.10), and anything after the relay has accepted a message is invisible until a monitored reply mailbox or a provider with bounce webhooks exists | Yes. mail_smtp_host is set in infra/envs/dev/main.tf and in staging and prod. The Workspace setting naming the worker's egress address is recorded done on 17 September 2026 (L-3 on the open-asks page), so the relay accepts the platform's mail. (This cell said until 30 September that the relay refused everything, which had been untrue since 17 September — reviews M13 and M66.) No relayed message's headers have yet been recorded; that is the delivery check in docs/ops/The_Sending_Domain.md. This row was written after the first call rather than before it, the reversal rule 6 exists to prevent |
Vertex AI, northamerica-northeast1 | Drafting a canvass's script, variants and translations at authoring time, for a second person to approve (ADR-041, Proposed) | Campaign brief only, no person data. The brief has no field for any resident, voter, supporter, volunteer or staff member, the candidate's name is a placeholder the model never sees, and a brief carrying an e-mail address, phone number, postal code or street address is refused. No restricted notes, scores, traits or protected characteristics, ever. (Until 1 October 2026 this cell read "Allowlisted context only; no restricted notes, traits or protected characteristics", which left room for §7.12's Person Context; ADR-041 closes it for drafting.) | Montréal, no fallback to any other region or provider (ADR-024). Whether it should be Toronto instead is open: ADR-002's amendment of 11 September 2026 says a hosted model goes in Toronto, and ADR-041 puts the choice to the Product Owner | Approved (ADR-024); the drafting use is Proposed (ADR-041), and on 8 October 2026 the Product Owner declined to accept it until it has been discussed and several models compared (ask B-134, decision 869) | No — nothing calls it. The labelling model is in-house (see the note below the table). Since story 1.25.2 (1 October 2026) the one caller exists and is off: the gateway in apps/api/src/ai builds only the Montréal regional endpoint (northamerica-northeast1) and asserts it on every request, but every environment answers with its deterministic fake (AI_PROVIDER defaults to fake) and the Terraform that would enable the API and grant the role is off (vertex_enabled = false). The gateway refuses any resource of a restricted or inferred-trait class, and a source gate keeps it from importing the People, Interactions, Inference, Universes and Imports modules — the code side of this row's "no person data". This cell flips to "Yes" before the first real call, by the Product Owner's decision, not by a session, and the Product Owner is told when it does (rule 6) |
| Langfuse (self-hosted) | AI traces and evaluation | Redacted prompts and outputs | Toronto | Approved | No — approved, not deployed |
| PostHog (self-hosted) | Product analytics | Event names, tenant/effort IDs, role, surface — never names, addresses, contact points, content, scores | Toronto | Approved | No — approved, not deployed; docs/ops/Waiting_On_Bakir.md item 14 asks whether to drop it |
| Sentry | Error tracking | Stack traces and metadata after PII scrubbing | Exception — outside Canada | Deferred (Product Owner, September 9, 2026): error tracking stays in Google Cloud for now. Not wired; revisit when the operation needs alerting the Error Reporting console does not give. Scrubbing rules would be mandatory | No — deferred |
| Expo EAS | Mobile builds and OTA updates | Build artifacts and JS bundles — no tenant data, and since .easignore no backend source either. For iOS, also the signing credentials EAS generates and keeps — the distribution certificate and provisioning profiles — which Apple can revoke and reissue at any time (decision 408) | Exception — outside Canada | Approved | No, since 11 September 2026 — Android builds are local (docs/ops/The_Phone_On_A_Device.md); iOS has never been built. It will be in use from the Product Owner's first eas build --platform ios (ADR-040; docs/ops/The_iPhone_On_A_Device.md), because an iPhone app can only be compiled on macOS |
| Apple App Store / Google Play | Distribution | App binaries. Apple, since ADR-040, also: TestFlight distribution of the same binary, and Apple's CDN fetching the public /.well-known/apple-app-site-association file from the console (it names the app and one path, nothing about anybody). Crash reports and TestFlight feedback reach Apple only if a tester turns them on; the app sends none. Push (APNs) is not used | Global | Approved | No — nothing published; the first TestFlight build is the first use (ask B-80) |
Self-hosted PMTiles basemap (tiles bucket, served by GET /v1/map/basemap) | Basemap tiles | Nothing leaves: a browser's tile requests reach our API and our bucket and no third party. That is ADR-022's whole reason — a tile request says which neighbourhood a campaign is working | Toronto origin, CDN edge | Approved (ADR-022) | Yes |
build.protomaps.com (Protomaps daily planet build) | Build time only: cutting the extract above, by pnpm --filter @fieldop/api basemap:extract | Nothing — a bounding box is chosen locally and the tiles covering it are read with HTTP range requests. No request is made while anybody is using the product, and no browser ever talks to it | Protomaps' CDN | Approved (ADR-022, story 1.12.1). Tiles are OpenStreetMap data under ODbL; the credit is served with the basemap and shown on every map | Build time only — touched when an extract is cut, never while anybody uses the product |
| Address Point Registry (self-hosted) | Geocoding | Addresses | Toronto | Approved (ADR-023) | Yes |
| OpenCage Geocoding API (fallback geocoder, E1.8) | Registry misses only | Address line and country — never a name, unit or any person data | Exception — EU/US processing; results may be stored permanently under OpenCage's terms | Adapter built (apps/api/src/geo/geocode.ts); inactive until OPENCAGE_API_KEY is set. Selection of OpenCage vs HERE vs Mapbox Permanent is a Product Owner decision before first use | No — the adapter is inactive, and turning it on is a residency decision, not a vendor choice: every registry miss would send a street address to servers in the EU or US. Decision 41 (12 September 2026): it stays off unless the Product Owner chooses otherwise having read this cell |
Town of Milton ArcGIS REST service (api.milton.ca) | Municipal address points into the registry | Nothing — read-only download of open data | Canada | Approved (open data, no personal data) | Load time only — read when the registry is loaded |
Amazon SES ca-central-1 (P1-B) | Recipient address, message content | Montréal | Pending (P1-B) | No — pending. For the sign-in link the Product Owner chose Google's default mailer on 13 September (decision 53); SES remains the baseline for P1-B bulk e-mail | |
| Stripe (Checkout, Billing, the customer portal, and the webhook back to us) | Taking the card at sign-up: the one payment for an election cycle and its invoice (Local and Campaign, story 0.12.18); the subscription of a year-round plan, its invoices and dunning | The organization's name, the administrator's e-mail address, the billing address and the card — all of it typed on Stripe's own hosted page, never on a Field OP origin, which is what keeps card details off this platform entirely and the PCI burden in its lightest class. For an election cycle, also the election day the campaign gave — a public date — and the cycle's last day, sent by Field OP in the payment's metadata and printed on the invoice (since 9 October 2026, story 0.12.18; the Product Owner is told in its pull request, rule 6). Field OP stores Stripe's identifiers and a card fingerprint and never a card number. No voter, supporter or contact record is sent, ever | Exception — United States. Stripe processes in the US; there is no Canadian-resident deployment. What crosses is a paying customer's own commercial details, not campaign data, which is the distinction decision 61 and this register both turn on | Approved (Product Owner, 15 September 2026, decision 63), docs/decisions/2026-09-15-launch.md. A card at sign-up was the decision; Checkout was chosen over an embedded form precisely so no card detail touches the console's origin and its content-security policy is unchanged. To be disclosed in the privacy notice as the fourth residency exception | The code that calls it exists (story 0.12.4, 15 September 2026): Checkout Sessions are created and the webhook is verified on the raw body, both tested against an in-memory fake. In use on dev, in Stripe's test mode (the sandbox), since 16 September 2026: the Product Owner's restricted key and the dev webhook's signing secret are in dev's Secret Manager, stripe_configured is on, and the first test-card payment went through at 20:47 that day (docs/ops/Billing.md). Finishing the sandbox is ask B-88. No live key exists in any environment — live mode is L-5 — so no real payment has been taken. (Until 8 October 2026 this cell said no key was configured anywhere and nothing had been sent to Stripe, which stopped being true on 16 September.) Also (16 September, decision 100): Stripe's MCP server (mcp.stripe.com) is declared in .mcp.json for the engineering sessions, authenticated by the Product Owner's own OAuth grant; what it receives is what a session asks it — API calls on his account and the business description he gave — never a row from this platform. Stripe's agent skills are documentation, installed under .agents/skills/, and make no call |
| Telephony carrier (Twilio or Telnyx — P2) | Voice and SMS | Phone numbers, call media | Provider-dependent | Pending (P2) | No |
| GitHub | Source control and CI | Code; no data | US | Approved | Yes |
Build-time package registries and GitHub-hosted build machines — the npm registry (registry.npmjs.org) for every build; for the Android app (android.yml), Gradle's distribution and the Maven repositories it reads (Maven Central and Google's); for the iPhone app (ios.yml), the CocoaPods repositories expo prebuild installs from; for the web vulnerability scan of staging (zap.yml, B-138, decision 1073), GitHub's container registry (ghcr.io, served from pkg-containers.githubusercontent.com), from which OWASP ZAP's image is pulled by digest — ZAP itself runs with -silent, so it calls nothing of its own (no update check or usage report to zaproxy.org), and it scans only our own staging; and the Ubuntu and macOS machines GitHub runs those workflows on | Build time only: downloading the open-source tools and libraries an app is compiled from, and compiling it | Nothing about anybody. A request names a public package and a version. The build machines hold the repository's source for the length of a run, which the GitHub row above already covers; no campaign, voter, supporter or staff record is on them, because none is in the repository (AGENTS.md rule 5) | Exception — outside Canada. GitHub's hosted machines run in the United States, and the registries are served from worldwide networks. No Canadian mirror is used | Accepted as it is (Product Owner, 8 October 2026; decision 867). He asked "Any way to avoid servers outside of Canada for this?", and on the follow-up chose to accept it as it is with this row added. A Canadian mirror of the registries remains possible later; it would not change what the row says is sent, which is nothing personal | Build time only — every Android and iPhone build since android.yml and ios.yml were added (stories 1.21.1, #506 and #507), and every CI run; never while anybody uses the product |
There are drafts for counsel now, and still no published notice. _Rewritten 15 September 2026; until then this paragraph read "There is no privacy notice, in draft or otherwise, anywhere in this repository."_ Story 0.3.8 asks for the Identity Platform exception to be documented "in the sub-processor register and privacy notice draft"; the register half is the row above and its acceptance criterion, and the other half now has something to be written into — docs/legal/privacy/1.0-draft.md, landed under story 0.12.2, which discloses this exception by name in its §8. It is engineering's draft and nothing more — version 1.0-draft since 17 September 2026 and 0-draft before it, served at /privacy under a banner saying exactly that, not reviewed by a lawyer, and not usable to record anybody's acceptance until counsel has cleared it and the -draft comes off the version id. Closing the gap properly is still the Product Owner's and counsel's rather than an engineering task. What this register holds is the raw material: every destination, what leaves, and why.
This file is the page. Since story 0.12.2 the console serves /subprocessors from this file itself, compiled into packages/schema/src/legal/generated.ts by scripts/generate-legal.mjs, because a container carries dist/ and not this repository. There is one register and not two: a destination added here appears on the page, and pnpm check:legal fails the build if the page and this file ever say different things. Unlike the four legal documents the same generator compiles, the register is deliberately not frozen at a version — those are words somebody agreed to, this is a statement about what is true now, and a register that could not be corrected would be a worse register.
This register was behind two destinations the Product Owner had already approved, and is not any more. _Corrected 15 September 2026, under story 0.12.2, the same day docs/legal/Findings.md F-03 recorded it._ Stripe (decision 63) and the Google Workspace SMTP relay (decision 58) now have rows above. The relay is the one worth reading twice: it was not merely approved but configured in infra/envs/dev/main.tf, in an environment that has been applied, so the platform was calling a destination this file did not list — which is precisely the order AGENTS.md rule 6 exists to prevent. Nothing was delivered, because the Workspace setting that would accept our mail is still unset (B-13), but "the call was refused at the far end" is not the control rule 6 asks for. The privacy notice draft names four residency exceptions and the table above now names four as well, and says in terms that it must not be published while that is so. docs/legal/Findings.md, finding F-03, carries the evidence. Writing the two rows is engineering's, and it is not done here — this pull request lands documents and changes no destination.
And it happened again, the same way, with the two sign-in providers. _Corrected 17 September 2026._ Story 0.3.15 put "Continue with Google" and "Continue with Microsoft" on the console (#386, #390) and then on the phone, and neither pull request added a row here. The providers went live on both dev tenants at midday on 17 September, so between then and this correction the platform was sending people to two destinations this register did not list — one of them, Microsoft, a vendor that appears nowhere else in it. The same order that rule 6 exists to prevent, and the second time this register has recorded it about itself. The rows are above now. It is worth naming why the shape recurs: the decision was taken properly and in the open (decision 110 chose both providers; the Product Owner answered B-48 and B-49 himself), and the register was simply not part of the act. A decision that names a new destination should fail a check until the row exists — which is the same argument decision 116 made about ask ids and story 0.2.22 made about restrictive policies, and it is not built.
The count has moved, and the documents have caught up. _Corrected 17 September 2026, later the same day._ This paragraph read "the count has moved and the privacy notice has not": the draft at docs/legal/privacy/0-draft.md §8 named four residency exceptions where this table, with Google's OAuth endpoints and Microsoft Entra ID, names six. Microsoft is still the first destination on this register that is neither Google, nor Stripe, nor a vendor the Product Owner had already weighed.
Version 1.0-draft of the legal set says six. The two rows and the date are in the terms of service, the privacy notice and the data-processing terms — all three carry the table — and the privacy notice also says in terms that choosing one of those sign-in options sends the person to Google or to Microsoft and sends them nothing about anybody else. The set went to counsel the same day, so B-41 is no longer "send the drafts"; what it asks now is on docs/ops/Waiting_On_Bakir.md. This register is deliberately not part of that set: the copy that travelled with the uploaded 1.0 documents was out of date, this file is the one /subprocessors is served from, and producing a second competing copy of a register is the defect F-03 exists to record.
What is still not automated, and it is the real finding here. pnpm check:legal compares the page with this file and does not count exceptions, so nothing would have caught the documents saying four while this table said six. Both times the arithmetic was caught by a person reading.
8 October 2026: three rows, all on the Product Owner's answers of that day (decision 867). The Cloud Armor row records a processing point in use on staging since 16 September and never written down until now (ask B-70); Sign in with Apple moved from Pending to Approved, still unused; and the build-time registries and build machines row records where the phone builds fetch their tools, with nothing personal sent. So version 1.0-draft's "six" is now behind by two for campaign staff's and volunteers' data — Cloud Armor for every visitor's request metadata, and Apple once it is switched on — and counsel's 1.0 has to say so; the letter of 7 October (question 6, revised in PR #513) tells them. The terms' promise of thirty days' notice before a new sub-processor handles client data is not owed for any of the three today: there is no customer yet, and the registries handle no client data. The promise has a mechanism since the same day — the announced changes below, mailed to every administrator by the operations job (story 0.12.17's second half, decisions 990 to 999).
Forbidden: Google Maps Geocoding API for stored coordinates (30-day cache limit in terms); any AI endpoint outside Canadian regions; any third-party name-classification or demographic-inference API (§7.13.4).
Announced changes
A destination that will begin processing Client Data is announced here, with the day it may begin, before it begins: the terms of service (§11) and the data processing terms (§6) promise at least thirty days' notice. Each row is also e-mailed to every administrator of every organization. Gestalt runs the notice on the operations job (docs/ops/Announcing_A_Sub_Processor.md), which refuses a first send less than thirty-six days ahead — thirty for the promise, five because an e-mailed notice takes effect on the business day after it is sent (terms of service §16.2), and one for the mail queue's retries — and the worker reads each administrator's sign-in address from Identity Platform at the moment it sends, and stores none of them (the Product Owner's answer to B-63, 8 October 2026). Every organization's audit trail records that its administrators were told, by identifier.
A row is the words of a notice, so it is never edited and never removed once it is on main; pnpm check:legal refuses both, and a correction is a new row with a new name. The cells are mailed as written: plain sentences, no formatting. A name is lower case, digits and hyphens, and the day is written YYYY-MM-DD.
| Notice | Destination | What changes | Client Data it will handle | Takes effect |
|---|
None is announced today. No change since this mechanism was built (8 October 2026) brings a new destination to Client Data — see the paragraph of that date above.
The last line, and the destination story 1.19.2 did not add
Nothing was added to this register by story 1.19.2, and the classifier it built makes no external call. This note is here because the story nearly did add one, and the next person to read the brief will reach for the same thing.
The brief for 1.19.2 asked for the name classifier to run on Vertex AI Gemini in northamerica-northeast1, citing ADR-024's residency rule. Vertex is already in the table above, in that region, approved. The backend was written — and then removed, for two reasons that the residency line does not cover:
- The Vertex row's own scope excludes it. Its "Data sent" column read "Allowlisted context only; no restricted notes, traits or protected characteristics." (and since 1 October 2026 reads, more narrowly still, "Campaign brief only, no person data", ADR-041). A family name sent to a model to have an ethnicity or a faith inferred from it is the inference of a protected characteristic, and the input is People-domain data.
- The Forbidden line above says it in terms. "Any third-party name-classification or demographic-inference API (§7.13.4)." Vertex is Google's. §7.13.4 is not ambiguous either: "No third-party name-classification API by default, for residency and contract reasons", and "an in-house probabilistic model outputting distributions rather than labels".
Residency answers "where". These two lines answer "whether", and they are the ones that bind here. The classifier that shipped is in-house (packages/core/src/inference, run by the API and the worker; the second copy that sat unused in services/intel was deleted in story 0.14.19), and it calls nothing.
If a third-party model is ever wanted for this, it is a Product Owner decision with a row in this table behind it — the shape the OpenCage geocoder already takes above: adapter built, inactive, and the choice named as a decision before first use. It should also be part of what the legal opinion of the September 11 decision is asked about, because the question "may we send these names to Google to be classified" is a different question from "may we infer this at all", and the decision record answers only the second.
Where processing happens, and why it is narrower than the law requires
_Added September 11, 2026, after legal advice and a Product Owner decision (decision 11)._
Counsel's advice was that the labelling model could lawfully run in the United States. The Product Owner's decision is that it will not, and that no campaign personal data will be processed outside Canada, as a matter of preference and caution rather than obligation. Treat that as a standing constraint on every design decision, not a preference to be re-weighed each time:
- No campaign personal data leaves Canada, for any feature, including anything a model reads. Campaign personal data is the records a campaign holds about people — voters, supporters, contacts, and anything derived from them. That is what this decision is about and what the whole platform is for.
- No third-party name-classification or demographic-inference API, anywhere — the Forbidden line above, which is about whether rather than where and is not satisfied by a Canadian region.
- Any proposal to use a hosted model is a decision for the Product Owner with a row in this table, never a default. The OpenCage geocoder above is the shape: adapter built, inactive, chosen deliberately before first use.
Three standing exceptions, and why this decision did not reopen them. An earlier draft of this section said "no personal data leaves Canada, for any feature", which contradicted three approved rows in the table above it. The rule is about campaign data; these three are about operating the platform, and each was decided separately and earlier:
| What | What leaves | Why it is not the same question |
|---|---|---|
| Identity Platform | The e-mail addresses, phone numbers, password hashes and MFA secrets of people who work on a campaign — a manager, a volunteer, an administrator | Nobody in it is a voter. It holds the staff directory of who may sign in, which is the same category as a payroll system, and no record about a member of the public is in it. Approved as D1 |
| Expo EAS | The phone app's source and the packages it imports — apps/mission and packages/schema, and nothing else | The app, not anybody's data. No tenant data reaches it. What this sentence describes is enforced by .easignore, not by intention: EAS builds in the cloud and uploads from the pnpm workspace root, so without that file it would have taken apps/api — every migration, policy and privileged function — along with infra/ and docs/. Not personal data, but the design of the platform's defences, and it does not need to leave the country to build an Android app |
| GitHub | Source code | AGENTS.md rule 5: no production or voter data in this repository, in tests, in fixtures, or in prompts |
The September 11 decision was about the labelling model and the voters' list. It did not ask for the sign-in provider to be moved, and moving it is a much larger question — Identity Platform is where every account lives and there is no Canadian-resident equivalent inside Google Cloud. If the Product Owner wants that reopened it is its own decision, with its own note here; until then D1 stands and this section does not override it.
The question that outranked all of this, and its answer. Until 12 September the open question was whether a voters' list may be sent to any cloud provider at all, including one inside Ontario. On 12 September 2026 the Product Owner reported counsel's answer: yes, within Canada, and the Product Owner's own preference is Toronto (northamerica-northeast2), where everything already runs. The written opinion is to follow and will be filed in docs/legal/. Until that document is in the repository:
- only synthetic data is used, and
- every label the platform writes still carries the marker
not_for_a_real_campaign_before_a_legal_opinionin itsusage_restrictions. Removing it is a terms re-agreement and a migration (W4.2 of the 12 September plan), sequenced after the document arrives — a verbal report is enough to plan on and not enough to remove a safeguard on.