Skip to content

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.

CaseforkClient is the only HTTP speaker. It calls the packaged Named Credential Casefork_Gateway.

POST /v1/decide sends:

  • subject, unmasked
  • description, with email addresses and phone numbers masked when Redaction_On__c is true (the packaged default). A prior-examples block may be appended inside that description when exemplars are on
  • queues, each as api_name and description
  • other_label, the fixed sentence None of the named queues.
  • header X-Casefork-Org-Id, the 18-character org id
  • header Authorization, the org’s Casefork token from External Credential Casefork_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.

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.

  1. The Cloudflare Worker casefork-gateway receives the call.
  2. D1 database casefork-ledger stores the ledger rows below. It does not store subject, description, exemplar text, or queue descriptions.
  3. When tier 1 is enabled, the subject plus the masked description is embedded. The embedding model name is not returned to Salesforce.
  4. Vectorize index casefork-vectors stores that embedding under the request id. The namespace is the org id. Metadata on the vector is org_id and label only.
  5. 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 sets provider.data_collection to deny. 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 without deny.

The OpenRouter key stays a Worker secret. It is not a subscriber secret and it is not in the package.

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.

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.

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.

  • 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

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:

  • decisions by created_at
  • feedback by created_at
  • labels by created_at, and the Vectorize vectors for those label ids in that org’s namespace
  • drift_bump by updated_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.