Engineering

Why partner data sharing has to be enforced on the server

By Co-Signal Team

Account overlap tools sit on a specific kind of sensitive data: one company's customer list, compared against another company's customer list. Get the sharing model wrong and the product does not fail loudly, it fails quietly, by showing a partner more than they were supposed to see. This post is the engineering argument for why that failure mode has to be closed at the server, not somewhere further out, and what Co-Signal actually does about it.

A client-side filter is not a privacy control

Picture the tempting shortcut: fetch the full comparison, every account field, both sides, down to the browser, and then decide in the UI layer what to display based on the current sharing setting. It is fast to build and it demos fine, because in a demo nobody opens the network tab. In production it is a privacy defect waiting for someone to open that tab, or call the underlying API directly, or read the response body in a browser extension. Anything a client can filter, a client can also unfilter. The data already left the server; the boundary the product claims to enforce never actually existed.

The only place a sharing decision can be enforced honestly is the place that decides what to send in the first place. If a partner's visibility level is counts only, the server must never construct, let alone transmit, the full account list for that comparison. There is no version of client-side redaction that closes this gap, because the information the redaction is supposed to hide has, by construction, already been sent.

One authorization layer, every surface

Co-Signal's answer is a single, shared authorization module that sits between every data source and every consumer of overlap data. Given a partnership and a population, it resolves the effective sharing rule, the explicit setting if one has been made, or a conservative default if one has not, and returns exactly the level and the field allowlist that governs what may be shown. Every surface that renders comparison data, the web application, Slack notifications, the AI agent, the MCP server, and CRM write-back, calls through this same module. None of them contains its own copy of "what is this partner allowed to see." A second, independent implementation of that question is exactly the kind of drift that eventually produces two surfaces disagreeing with each other, and one of them being wrong.

Downstream of the sharing decision sits a second function, a pure redaction step, that takes a full account record and the resolved decision and returns only the fields that decision permits. It is deliberately a pure function with no side effects and no external dependencies, so it can be tested directly, reused everywhere a record needs to cross a sharing boundary, and, notably, imported into a client-side demonstration on our own security page without weakening the guarantee: the demonstration runs the real function, it just runs it against fictional data instead of a live comparison.

What k-anonymity suppression actually protects against

Counts-only sharing sounds safe: no names, just a number. But a number can still leak identity. If a partner already suspects which specific account might be in the overlap, and the count comes back as one, that number just confirmed a fact about a company the partner has no explicit permission to see anything about. A small enough count is functionally as identifying as a name.

Co-Signal closes this with a k-anonymity-style suppression rule: any counts-only cell whose true value falls below a fixed threshold is never returned as an exact digit. The API returns a suppressed marker instead, and every surface renders that marker the same way, a deliberately vague "fewer than the threshold" treatment rather than a number a partner could reverse-engineer by watching it change over time. The threshold is a single named constant, not a per-caller judgment call, so every surface that renders a counts-only cell inherits the same protection automatically rather than having to remember to apply it.

Encrypted tokens, conservative defaults

The sharing layer governs what leaves the server, but the server also has to hold the CRM credentials that let it fetch data in the first place. Every OAuth token and refresh token for a connected Salesforce or HubSpot account is encrypted at rest, never stored or logged in plain text, and decrypted only for the duration of a sync.

The default matters as much as the mechanism. A brand-new partnership, before anyone has touched a sharing setting, starts at the most conservative level available: counts only, no additional fields. Nothing is shared by default just because a partnership was created. A company has to make an affirmative choice to loosen sharing for a specific population and a specific partner before anything beyond a count becomes visible.

Every sharing change is written down

The last piece is accountability for the decisions themselves. Every time a company changes a sharing level or opts a field group in or out, that change is written to an audit log: what changed, from what level to what level, who made the change, and when. The log belongs to the owning company, not the partner, because it is a record of your own decisions about your own data, not a shared activity feed. If a sharing setting ever looks wrong, or a partner asks why they can suddenly see something new, the answer is a real record, not somebody's memory of a settings page from three months ago.

The takeaway, for anyone evaluating this kind of product

If you are evaluating any tool that compares two companies' CRM data, ask where the filtering happens. If the honest answer is "in the response the browser already received," the privacy claim does not survive contact with a network inspector. Co-Signal's architecture exists so the honest answer is always the same: one server-side authorization decision, one redaction function, a suppression rule that protects small counts as carefully as it protects full records, and a written history of every time that decision changed.