Project Record · For Brian's review

Salesforce Write Access

From read-only API today to a guarded, tested write path. The full record of what we decided, why, and the two questions that need your call.

Prepared for Brian Di · Authored by Anthony + Claude · 2026-05-30 · Pairs with Jason's two playbooks

The one-minute version

  1. Goal: let a small set of operators write to Salesforce (Purchase Orders and B2C Products) safely, with everything logged and a Claude job auditing the logs so no human has to self-monitor.
  2. V1 is a deliberate test on the existing shared apireadonly user, not per-person licenses. Prove the write path and guardrails first, decide the identity model after.
  3. The one accepted deviation from your framework: a shared user can't attribute writes natively, so the Google Sheet audit log becomes the system of record for "who," with the real human's email on every write (your Option B).
  4. We need two things from you (Q1, Q2): MCP vs. direct-API scripts, and your sign-off on the Option B deviation for a bounded V1.

01Objective

What Anthony actually wants out of this.

02Where Salesforce is today

Four pieces exist and are verified. Going to write adds exactly one more (a permission set), not a new license.

PieceWhat it is
Integration userapireadonly@emazinglights.com. The API identity. On a Salesforce Integration license (API-only, no human seat, no extra cost). Read-only today.
Read-only profileMinimum Access - API Read Only. Edit / Create / Delete unchecked. This is the current lock.
Read permission setread-only field access. Field-level Read on Purchase Order (header + lines) and Product objects.
External Client AppSFOAuthEG. Issues the OAuth credentials the scripts use. Revoking it kills every token at once.

It is a direct API integration, not an MCP. The path to write (per Jason's status doc): add one Edit-enabled permission set, assign it to the integration user, test, promote. Same user, same app, same credentials. The read-only side is untouched.

03The V1 decision: ship on the shared user as a test

Anthony's explicit call. Prove the write path before committing to an identity model.

Your framework says writes should be per-actor. The clean way to do that here is one named Integration user per person (we have 4 free, see Section 09). Anthony chose not to commit to that yet. V1 ships on the existing shared apireadonly user with a small named operator set, to prove the write path and the guardrails work on real data. If it runs well, we revisit per-person from there.

The V1 operator set (4-5 people sharing one API)

PersonRole
Maryanne DaclanAssistant Merchandise Manager
Arvhiane ValerioAssistant Buyer
WillDirector of Finance
JasonSalesforce admin (owns the integration)

The workflow V1 automates: today, when a cost or unit changes on a PO, the lines are edited by hand one at a time (a 5-line PO is 5 manual edits). V1 lets Claude make those edits through the guarded write path. Anthony also confirmed the operators need to write B2C Products, so that object is in V1 scope too (see Section 04).

04The write scope

Exactly what becomes writable, and the landmines we mapped.

ObjectAPI nameWrite
Purchase Orderserp__Purchase_Order__cEdit + Create
PO Lineserp__Purchase_Order_Line__cEdit + Create
B2C ProductECS__Product__cEdit + Create

Delete stays off on every object. "Removal" is a status / flag update, never a delete() call. Note the product object is ECS__Product__c (B2C Product), not the standard Product2.

What was verified in Setup (2026-05-29)

The single biggest risk to flagBecause B2C Product is in scope, the UpdateMagentoInventory trigger means a Product write pushes downstream to live storefront inventory. A bad write propagates to the store in seconds. This is why Product is riskier than POs and why it needs the extra controls in Section 06.

05Identity and the compensating control

The one place V1 deviates from your framework, and how we keep it honest. This is Q2 for you.

On the shared apireadonly user, Salesforce's native history stamps every change as "apireadonly," not the actual human. That does not meet the per-actor bar on its own. We accept it for V1 only with a named compensating control (your framework's Section 03, applied here):

Honest framingThis is a conscious, signed-off deviation (Option B), the same logic you use for a shared service account where native audit is weak. Acceptable for a bounded V1 on a named operator set. The path back to per-actor identities stays open. We want your explicit yes / no on this.

06Security analysis

Anthony asked directly: what are the security concerns? Three, plus one caveat. None are blockers; each has a named control baked into the build.

1. Credential custody is the whole boundary (highest)

With one shared user, the OAuth credentials are the security perimeter. Two consequences:

2. Magento blast radius on Product writes (second)

A ECS__Product__c write fires UpdateMagentoInventory → live storefront. The dry-run preview shows the Salesforce before/after, not necessarily the Magento consequence.

3. The audit Sheet must be append-only (third)

That Sheet is now the system of record for "who." If operators can edit it, they can edit the record of their own actions and the control is theater.

Keep this honestThe auto-auditor is detective, not preventive. It catches a bad write on its next run (every 2 hours), after Magento already synced. The preventive controls below carry the real weight. The auditor is the backstop, not the seatbelt.

The full guardrail set (added after Anthony's review)

Grouped by job. Preventive controls stop a bad write at the door; detective controls catch and reverse what slips through.

Stop a bad write before it happens:

Catch and reverse what slips through:

07The five guardrails, as wired

Same five as your framework. Here is the V1 wiring.

  1. Dry-run by default. Every write previews the exact records and fields (count + before/after) and changes nothing until a second explicit confirm.
  2. Threshold starts at 0%. Because POs are financial, every write requires a second explicit override to start. Relax toward your 20% only if 0% proves too noisy.
  3. Audit Sheet, one row per write: timestamp (PT), real human's email, object, record ID, action, before, after, API response, notes.
  4. Slack ping per write to a dedicated channel where the operator and a reviewer both see before/after. Best-effort; a failed ping never rolls back a real change.
  5. Identity = shared apireadonly + the compensating control from Section 05.
Belt and suspendersIf the audit row fails to write, do NOT fake a rollback. Surface the failure so a human logs it. A visible gap beats an invented recovery.

08The auto-auditor

A log nobody reads is not a control. A scheduled Claude job reads it for us. This is Anthony's signature requirement.

A scheduled job (launchd cron running claude -p, matching existing infra) runs every 2 hours over the new rows since its last run, pulling from the write log, Setup Audit Trail, and Field History. It flags: a write with no matching audit row (bypass), any threshold override and whether it looked justified, any delete (should never happen), a write outside the approved scope, a write by someone not on the approved-access list, off-hours or volume spikes, and audit-append failures. It posts a green "clean" check to Slack, or a flagged list with record links. On a serious flag it does not just alert, it auto-pulls the write permission set to freeze all writes until a human looks.

Load-bearingThe auditor is only as good as the logs feeding it. That is why Field History on the written fields (including PO Line Cost) and Setup Audit Trail retention are not optional. Not built yet — described here as the target; building the scheduled job is fast-follow work.

09Licenses (so the identity options are grounded)

Verified from Setup screenshots, 2026-05-29.

License typeReality
SF Integration (API-only)5 total, 1 used by apireadonly, so 4 free, then $10/user/month. This is what write integration users would use.
API Integration PSL5 available, 0 used. Expands an Integration user's object/API access (how we grant write scope).
SF full (UI login)4 total, 1 free, effectively maxed. The costly per-seat licenses, not the API path.
API call volume3.45M/month allowance, ~1% used. Not a constraint.

Why this matters: per-person identity (the framework-ideal) is free for up to 4 operators. Anthony still chose shared-for-V1 to keep the test small and prove the path first, not because per-person costs money. The door to per-person stays open at no license cost.

10Testing

Two options, both on the table. Jason leans controlled-live; Anthony's earlier instinct was sandbox-first.

Option A — Controlled live test (Jason's lean): read + dry-run scope checks on production are 100% safe (no writes). Then real writes only on a single disposable test PO we create and own, all guardrails on. This is your Meta approach (real account, control record): faster, real data, no stale-sandbox friction.

Option B — Salesforce sandbox: stand up the write permission set in emazing--uat and run the full seven-test cycle there first. Fully isolated, zero risk to live records. Trade-off: sandbox data is stale and test-record setup is UI-heavy.

The interaction that mattersControlled-live is fine for a throwaway PO (contained). It is NOT safe for Product, because there is no throwaway Magento sync. So: use the sandbox once to watch the ManagePurchaseOrder / ManagePOLines / Magento triggers, then controlled-live for POs. Do not skip the threshold-breach test (test 6). Run the audit-integrity check before declaring go-live.

11Build sequence and revocation

Build sequence (admin view)

  1. Create a permission set PO Write (Integration): check Edit + Create on PO + PO Line (and the selected Product fields). Leave Delete unchecked.
  2. Enable Field History on the PO Line Cost field (and any written field not yet tracked).
  3. Stand up the audit Google Sheet (append-only) + the Slack channel for write pings.
  4. Wire the guarded write path (dry-run / confirm, 0% threshold, audit-Sheet write with the human email, Slack ping). REST sObject calls.
  5. Run the seven smoke tests per Section 10.
  6. Assign the permission set to apireadonly, add a rollout decision-log row (date, operators, objects/fields in scope, reason), go live for the V1 operator set.

Revocation (the kill switch)

  1. Stop writes now: remove the PO Write (Integration) assignment. All write capability dies instantly; read access stays.
  2. Kill all API access: revoke / disable SFOAuthEG. Every OAuth token dies at once.
  3. Remove one operator (once per-person): freeze that integration user; others unaffected.
  4. Confirm: any credentials on a laptop are now inert. The lock lived in Salesforce, not the token.

12Jason's answers (the SF facts, verified)

All ten submitted through the playbook page and the Feedback sheet. Condensed.

Q1 — Licenses

4 free Integration licenses (5 total, 1 used), $10/user/mo beyond. API Integration PSL: 5 free. Per-person (Option A) free for up to 4 operators.

Q2 — Write scope

PO + PO Line, Edit + Create, Delete off. Product read-only at first, later expanded to B2C Product writes (ECS__Product__c, not Product2) per Anthony's call.

Q3 — Sandbox

Yes: emazing--uat (https://emazing--uat.sandbox.my.salesforce.com). Creds out-of-band.

Q4 — History / retention

Field History ON for PO, PO Line, B2C Product. Gap: PO Line Cost not tracked (enable before writing it). Setup Audit Trail ~180 days.

Q5 — Landmines

Zero validation rules. Triggers DO fire: ManagePurchaseOrder, ManagePOLines, and 5 Product triggers incl. UpdateMagentoInventory (Magento sync). REST sObject. Required fields / flows / approvals confirmed in sandbox.

Q6 — Threshold

Start 0% (every PO write needs the override), relax to 20% later if too noisy.

Q7 — Permission precedence

An additive Edit permission set grants write over the read-only profile (most-permissive wins). Proven in the org, re-checked in sandbox.

Q8 / Q9 — for you (Jason's lean)

Scripts first, move to MCP after the seven tests pass; preview_then_execute() ports either way. No-DELETE is a hard rule for a system of record. Option B acceptable only with a hardened app-layer audit.

13What we need from you

Two questions. Answer them with your Claude and send back; the high-level HTML has inline forms, or just reply.

Q1 Brian Integration shape

Build a Salesforce MCP with the dry-run tools, or keep the direct-API scripts and bolt the guardrails on? Does your preview_then_execute() helper port cleanly? Our lean: scripts a human runs to start, MCP after the seven tests pass.

Q2 Brian Sign off Option B

Is the shared apireadonly + hardened app-layer audit (Option B) acceptable for a bounded V1, or a hard no? And is there anything in the five-guardrail model you would change for a system-of-record database (Salesforce) versus an ad platform (Meta), especially around the shared-user audit deviation?

14Current state