Security and data
Casefork sends a narrow slice of a Case out of Salesforce, keeps the usage ledger for one year, and does not keep case text in the gateway database.
What leaves Salesforce
Section titled “What leaves Salesforce”CaseforkClient is the only HTTP speaker. It calls the packaged Named Credential Casefork_Gateway.
POST /v1/decide sends:
subject, unmaskeddescription, with email addresses and phone numbers masked whenRedaction_On__cis true (the packaged default). A prior-examples block may be appended inside that description when exemplars are onqueues, each asapi_nameanddescriptionother_label, the fixed sentenceNone of the named queues.- header
X-Casefork-Org-Id, the 18-character org id - header
Authorization, the org’s Casefork token from External CredentialCasefork_Gateway
It does not send the Case Number, the Account, the OpenRouter key, or a separate exemplars array.
POST /v1/feedback runs only when Feedback_On__c is true. The body is request_id and label (the final queue API name). It does not send subject or description.
Masking
Section titled “Masking”Email addresses and phone numbers in the description are masked when Redaction_On__c is true. That is the packaged default. Subject is not masked. The gateway applies that same mask again to the description, and to every exemplar description, before the upstream call below. The usage ledger does not store Subject or Description.
Where that data goes
Section titled “Where that data goes”- The Cloudflare Worker
casefork-gatewayreceives the call. - D1 database
casefork-ledgerstores the ledger rows below. It does not store subject, description, exemplar text, or queue descriptions. - When tier 1 is enabled, the subject plus the masked description is embedded. The embedding model name is not returned to Salesforce.
- Vectorize index
casefork-vectorsstores that embedding under the request id. The namespace is the org id. Metadata on the vector isorg_idandlabelonly. - Tier 2, and the cold-start call when that flag is on and the org has fewer than the label minimum, call OpenRouter
POST https://openrouter.ai/api/v1/chat/completions. The body setsprovider.data_collectiontodeny. OpenRouter may forward the masked case text and the queue names to its upstream model provider. If OpenRouter has no provider that accepts that request, the Worker fails closed and Salesforce holds the Case for review. The call is not retried withoutdeny.
The OpenRouter key stays a Worker secret. It is not a subscriber secret and it is not in the package.
Zero-retention LLM routing
Section titled “Zero-retention LLM routing”The upstream request sets provider.data_collection to deny. That is the zero-retention instruction on the call. If no provider accepts the request with that instruction, the Worker fails closed. The call is not retried without deny. Case text is not kept for training by this package.
Per-org isolation
Section titled “Per-org isolation”Each org is its own vector namespace: the namespace is the org id. A query also filters on org_id. A match whose metadata org does not match the caller is dropped. Ledger rows, labels, feedback, calibration, and the token hash are keyed by that same org id. A purge of one org does not touch another org.
What is stored
Section titled “What is stored”In Salesforce: the Case, the Casefork fields on Case, and one Casefork_Decision__c row per Case (choice, confidence, queues, outcome, request id). Casefork_Raw_Json__c can hold the gateway JSON for that Case. Subject and description remain on the Case.
In D1:
| Table | Kept | Not kept |
|---|---|---|
decisions |
org id, request id, outcome, token counts, cost, latency, tier, choice, raw confidence, created_at |
subject, description, exemplars, queue text |
labels |
vector id, org id, queue label, created_at |
case text |
feedback |
org id, request id, final queue label, created_at |
case text |
drift_bump |
org id, bump, updated_at |
case text |
calibration |
org id, tier, isotonic knots, created_at |
case text |
org_tokens |
SHA-256 of the Casefork token, status, timestamps | the token itself |
Vectorize stores the numeric embedding plus org_id and label in the org’s namespace.
The decide response Salesforce can see is choice, confidence, probabilities, request_id, and other_label. Tier, model, vendor, and cost are not in that response.
What is never stored
Section titled “What is never stored”- The OpenRouter key, in Salesforce or in D1
- The Casefork token, except as a hash in
org_tokens - Subject, description, or exemplar text in D1
- A model name, vendor name, or the cold-start marker on the decide response or on ledger columns the package reads
One-year retention
Section titled “One-year retention”RETENTION_DAYS defaults to 365. The Worker cron 0 8 * * * computes a cutoff from that many days and, for each org id it finds, deletes only that org’s rows older than the cutoff:
decisionsbycreated_atfeedbackbycreated_atlabelsbycreated_at, and the Vectorize vectors for those label ids in that org’s namespacedrift_bumpbyupdated_at
calibration is not deleted. org_tokens are not deleted. After the delete, the same cron refits calibration from the feedback and decisions that remain.
node scripts/purge-org.mjs <org id> is not the retention job. Purge deletes that one org’s labels, Vectorize vectors, calibration, feedback, and drift_bump. It does not delete decisions. --offboard also deletes that org’s org_tokens. Purge does not touch another org. Decisions older than a year are removed only by the daily retention cron.