Skip to content

NagaYu/signalshield

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🛡️ SignalShield

Honor the "No." Keep the signal. Forward zero PII.

A privacy-first proxy that turns consent-rejected conversions into fully anonymized server-side signals — so your attribution survives the cookie apocalypse without ever betraying a user who said no.

License: MIT TypeScript Privacy by Design GDPR CCPA


The 30-second pitch

When a user clicks "Reject" in your cookie banner, the ethical and legal answer is simple: send nothing identifying. But most teams interpret "send nothing identifying" as "send nothing at all" — and then watch their Meta and Google ad models slowly go blind, starved of the aggregate volume they need to optimize.

There is a third path.

A purchase that happened is a fact about your business, not a fact about a person. The amount, the currency, the moment it occurred — these are commercial truths. Stripped of every identifier, they are no longer "personal data" under GDPR Art. 4(1) or CCPA. They are just arithmetic.

SignalShield is the chokepoint that performs that stripping. It accepts a raw conversion event from a consent-rejecting browser, violently discards the IP address, User-Agent, cookies, device fingerprints, and any hashed-or-not identifiers, and forwards only this to Meta CAPI and Google's API:

{ "event_name": "Purchase", "value": 49.99, "currency": "USD", "event_time": 1751193600, "action_source": "system_generated" }

That's it. That's the whole signal. The platforms get aggregate conversion volume to train on. The user gets the privacy they explicitly asked for. You get to sleep at night.


The philosophy: privacy and machine-learning are not enemies

The ad-tech industry has spent a decade insisting that measurement requires identity. That premise is false, and it is the most expensive false premise in modern marketing.

What ML optimization actually needs What the industry pretends it needs
Volume of conversion events A 1:1 user-to-conversion mapping
Aggregate value & timing trends Per-user lifetime profiles
A reliable denominator Cross-site tracking identifiers

Modeled conversions, aggregated reporting, and privacy-enhancing technologies already let the platforms reconstruct campaign-level performance from anonymous volume. What kills performance is not anonymity — it's silence. A model that sees zero events from one-third of your traffic doesn't become more private; it becomes wrong, and it spends your budget being wrong.

SignalShield resolves the false dilemma:

Consent-granted users → run your normal, fully-attributed pixel / CAPI stack. Consent-rejected users → route through SignalShield, which forwards an irreversibly anonymized signal carrying no personal data whatsoever.

You stop choosing between "comply with the law" and "feed the algorithm." You do both.


Architecture

SignalShield is a single, auditable chokepoint. Data can only ever flow downhill — toward less information, never more. The raw request and everything identifying about it is dropped at the door; only four scalar values are ever reconstructed into a new object and sent onward.

flowchart TD
    A["🧑 Consent-REJECTED user<br/>completes a purchase"] -->|"POST /api/shield/conversion<br/>{ value, currency, event_time, ... }"| B

    subgraph SHIELD["🛡️ SignalShield process boundary"]
        direction TB
        B["Express edge<br/>• trust proxy = OFF<br/>• x-powered-by = OFF<br/>• logger reads NO headers/IP/body"]
        B --> C{{"anonymize()<br/>THE CHOKEPOINT"}}
        C -->|"❌ DISCARDED FOREVER"| X["🗑️ IP address<br/>🗑️ User-Agent<br/>🗑️ Cookies / fbp / fbc<br/>🗑️ Device & fingerprint data<br/>🗑️ Emails / phones (hashed or not)<br/>🗑️ Every unrecognized field"]
        C -->|"✅ ALLOWLISTED & VALIDATED"| D["AnonymizedSignal<br/>{ event_name, value,<br/>currency, event_time,<br/>action_source = system_generated }"]
        D --> E{{"assertNoPiiLeak()<br/>runtime invariant:<br/>exactly 5 keys, no more"}}
    end

    E -->|"axios POST · no forwarding headers"| F["📘 Meta Conversions API<br/>(no user_data block)"]
    E -->|"axios POST · no forwarding headers"| G["📕 Google Enhanced Conversions<br/>(no user_identifiers)"]

    style X fill:#ffe0e0,stroke:#cc0000,color:#000
    style D fill:#e0ffe0,stroke:#00aa00,color:#000
    style C fill:#fff3c4,stroke:#d4a000,color:#000
    style E fill:#fff3c4,stroke:#d4a000,color:#000
    style SHIELD fill:#f5f9ff,stroke:#3b82f6,color:#000
Loading

Why this design is leak-proof

  1. Allowlist, never blocklist. The anonymizer does not "scrub" the incoming object — it constructs a brand-new object from a fixed set of four validated scalars. Anything not explicitly copied simply ceases to exist. A future, never-before-seen identifier field cannot leak through, because there is no code path that copies unknown fields.
  2. The sanitizer never sees the request. anonymize() is handed the parsed JSON body only — never Express's req. IP, headers, cookies, and the socket are physically unreachable from the sanitization path. You cannot leak what you cannot see.
  3. No forwarding headers on egress. The axios client sends only a JSON content type. The original visitor's X-Forwarded-For, User-Agent, and cookies are never reconstructed or relayed upstream.
  4. PII-free logging. The request logger reads only method, path, status, and duration. Upstream error bodies (which can reflect submitted data) are reduced to a bare HTTP status before they ever reach a log line.
  5. Runtime invariant. assertNoPiiLeak() throws if the outgoing signal ever carries anything other than its exactly-five allowed keys — turning a silent regression into a loud crash.

⚡ 10-second quick start

# 1. Install
npm install

# 2. Configure (one platform is enough to start)
cat > .env <<'EOF'
META_ENABLED=true
META_PIXEL_ID=YOUR_PIXEL_ID
META_ACCESS_TOKEN=YOUR_CAPI_TOKEN
ALLOWED_ORIGINS=https://your-store.com
EOF

# 3. Run
npm run dev

Then fire a test conversion:

curl -X POST http://localhost:8787/api/shield/conversion \
  -H "Content-Type: application/json" \
  -d '{ "value": 49.99, "currency": "USD", "event_name": "Purchase",
        "email": "i-will-be-deleted@example.com",
        "ip": "203.0.113.7", "user_agent": "Mozilla/5.0", "fbp": "fb.1.123.456" }'

Watch the terminal. The email, ip, user_agent, and fbp you sent are gone — never logged, never forwarded:

[PRIVACY SAFE] Signal forwarded securely → {"event_name":"Purchase","value":49.99,"currency":"USD","event_time":1751193600,"action_source":"system_generated"}
✓ meta: Signal accepted by Meta Conversions API.

Frontend integration

Call SignalShield only from the code path that runs for consent-rejected users (e.g. your purchase-confirmation page when the CMP returned a "deny"):

// Runs ONLY when the user rejected tracking consent.
await fetch("https://shield.your-store.com/api/shield/conversion", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  // Send whatever you have — SignalShield will keep only value/currency/time.
  body: JSON.stringify({
    value: order.total,
    currency: order.currency,
    event_name: "Purchase",
    event_time: Math.floor(Date.now() / 1000),
  }),
  // No credentials — SignalShield never reads cookies.
  credentials: "omit",
});

💡 You don't have to pre-clean the payload. SignalShield is built to receive a "dirty" body and emit a clean one — that asymmetry is the entire point.


Configuration reference

All configuration is via environment variables (loaded from .env in development).

Core

Variable Required Default Description
PORT no 8787 HTTP port to listen on.
NODE_ENV no development development | production | test.
ALLOWED_ORIGINS recommended (empty) Comma-separated browser origins allowed by CORS. Empty in production = browser requests with an Origin are refused.
MAX_EVENT_AGE_SECONDS no 604800 (7d) A client event_time older than this (or in the future) is replaced by the server clock.

Meta Conversions API

Variable Required Default Description
META_ENABLED false Set true to forward to Meta.
META_PIXEL_ID if enabled Your Meta Pixel / Dataset ID.
META_ACCESS_TOKEN if enabled CAPI access token. Never logged.
META_API_VERSION no v19.0 Graph API version.
META_TEST_EVENT_CODE no Test code; honored only when NODE_ENV != production.

Google Enhanced Conversions

Variable Required Default Description
GOOGLE_ENABLED false Set true to forward to Google.
GOOGLE_CUSTOMER_ID if enabled Google Ads customer ID (digits only).
GOOGLE_CONVERSION_ACTION_ID if enabled Conversion action ID.
GOOGLE_DEVELOPER_TOKEN if enabled Google Ads developer token. Never logged.
GOOGLE_ACCESS_TOKEN if enabled OAuth 2.0 bearer token. Never logged.

At least one platform must be enabled, or the server refuses to start.


API

POST /api/shield/conversion

Request body (all extra fields are silently dropped):

Field Type Notes
value number | numeric string Required. Must be positive & finite.
currency string Required. ISO 4217 (e.g. "USD", "JPY").
event_name string Optional. Validated against the allowlist; defaults to "Purchase".
event_time number | numeric string Optional Unix seconds; clamped or replaced by server clock.

Responses

Status Meaning
202 Accepted At least one platform accepted the anonymized signal.
422 Unprocessable Entity The body was malformed (e.g. missing value/currency).
502 Bad Gateway Every enabled platform rejected the signal.

The response body never echoes your submitted request.

GET /healthz

Returns service status and which platforms are enabled. No auth, no PII.


Build & deploy

npm run build      # bundle to ./dist with tsup (ESM, minified, typed)
npm start          # run the compiled server
npm run typecheck  # strict tsc --noEmit, zero-tolerance config

SignalShield is a plain Node HTTP server and runs anywhere: a container, a VM, Fly.io, Railway, Render, or behind your existing reverse proxy. The same anonymization core can be lifted into a Serverless function (Lambda / Cloud Functions / Vercel) by invoking anonymize() + forwardSignal() directly.

Reverse-proxy note: SignalShield deliberately sets trust proxy = false and never reads req.ip. Even if your load balancer injects X-Forwarded-For, SignalShield ignores it. This is intentional — there is no configuration that makes it start trusting client IPs.


Legal & compliance notes

SignalShield is engineering infrastructure that helps you implement a data-minimization strategy — it is not legal advice, and shipping it does not by itself make you compliant.

  • Data minimization (GDPR Art. 5(1)(c)): by forwarding only non-identifying commercial scalars, the signal falls outside the definition of "personal data" (Art. 4(1)) — there is no identifier, online or otherwise, attached.
  • Honor the choice: route through SignalShield only for users who rejected consent, and make sure your CMP logic actually gates this correctly. The tool enforces anonymity on the wire; you must enforce when it is used.
  • Platform terms: Meta and Google each have their own business terms for server-side data. Confirm that sending fully aggregate, system-generated signals is consistent with your agreements and regional configuration.
  • Consult counsel: regional rules (e.g. Japan's amended Telecommunications Business Act, ePrivacy, CCPA/CPRA) evolve. Have your DPO / counsel review your end-to-end flow.

Why teams adopt SignalShield

  • 🔒 Leak-proof by construction, not by vigilance — the allowlist design makes accidental PII egress structurally impossible.
  • 🧾 Auditable in one sitting — a single chokepoint, a runtime invariant, and PII-free logs your security team can actually read.
  • 📈 Recovers lost signal volume from the third of traffic that opts out, without recovering a single identifier.
  • 🪶 Tiny & dependency-light — Express + axios + colorette. No database. No state. Nothing to breach.
  • 🛠️ Strict TypeScriptstrict: true, noUncheckedIndexedAccess, exactOptionalPropertyTypes, the works.

Contributing

Issues and PRs welcome. The one inviolable rule: no change may create a path by which request metadata (IP, User-Agent, cookies, headers) can reach a log line or an outbound request. PRs are reviewed against that invariant first and everything else second.


License

Released under the MIT License.

MIT License

Copyright (c) 2026 SignalShield Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

🛡️ Honor the "No." Keep the signal. Forward zero PII.

About

Privacy-first conversion signal proxy — strips 100% of PII from consent-rejected events and forwards only an anonymized signal (value, currency, event_time) to Meta CAPI & Google Enhanced Conversions.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors