---
title: "Store service: Node.js native ingress"
description: "Store service: Node.js native ingress for the complete private reference application."
slug: "/docs/v1/guides/store-service/node"
section: "Webhooks and integrations"
order: 795
audience: "all"
level: "advanced"
last_updated: "2026-10-18"
version: "v1"
related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"]
draft: false
---
This complete Node.js 18+ server relays native provider notifications to the [shared PHP store service](/docs/v1/guides/store-service). The PHP service remains required for provider verification, persistence, worker and mail. It is not a standalone Node payment worker or a Node requirement for PHP shared hosting.

## What you will learn

Read and run the exact Store service: Node.js native ingress with the complete shared application, without exposing private credentials or claiming an unverified payment.

## Configure and run

Set private KDP_STORE_SERVICE_URL to the dedicated PHP service's HTTPS origin followed by /provider-event. Set optional KDP_STORE_RELAY_PORT; default 8118. Put your own HTTPS reverse proxy in front of this loopback listener and preserve raw bodies/headers. Register that public proxy URL with the provider. Do not use a redirect or expose any provider/API credential in JavaScript sent to a browser.

```bash
node --check stores/relay.mjs
node stores/relay.mjs
```

Expected: listening:true and loopback_only:true. Forward only POST /provider-event; a complete, verified upstream durable receipt returns 202 with accepted:true. A failed/oversized/invalid/uncertain upstream receipt returns a refusal, never false success. The worker cron runs independently on PHP hosting.

## Complete source

```javascript
#!/usr/bin/env node
// Optional Node18+ native HTTP ingress for the complete PHP store service.
// This is NOT a pure Node payment worker: service.php authenticates and commits.
// Set KDP_STORE_SERVICE_URL (HTTPS /provider-event), KDP_STORE_RELAY_PORT (8118).
// Run: node relay.mjs. Expected: only verified durable upstream receipts return202.
import http from 'node:http';

const target = new URL(process.env.KDP_STORE_SERVICE_URL ?? '');
const loopback = process.env.KDP_STORE_ENVIRONMENT === 'sandbox' && process.env.KDP_STORE_ALLOW_LOOPBACK === '1' && target.hostname === '127.0.0.1' && target.protocol === 'http:';
if ((target.protocol !== 'https:' && !loopback) || target.username || target.password || target.search || target.hash || target.pathname !== '/provider-event') throw new Error('Private service URL required');
const allowed = new Set(['content-type', 'stripe-signature', 'paddle-signature', 'x-signature', 'x-wc-webhook-signature', 'x-wc-webhook-topic', 'x-shopify-hmac-sha256', 'x-shopify-topic', 'x-shopify-shop-domain', 'x-shopify-webhook-id', 'x-fs-signature', 'paypal-auth-algo', 'paypal-cert-url', 'paypal-transmission-id', 'paypal-transmission-sig', 'paypal-transmission-time']);
const reply = (response, status, body) => { response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', Connection: 'close' }); response.end(body); };
const server = http.createServer((request, response) => {
  if (request.method !== 'POST' || request.url !== '/provider-event') return reply(response, 404, '{"accepted":false}');
  if (!/^\d+$/.test(request.headers['content-length'] ?? '') || request.headers['transfer-encoding']) return reply(response, 400, '{"accepted":false}');
  if (Number(request.headers['content-length']) > 262144) return reply(response, 413, '{"accepted":false}');
  const headers = {}, seen = new Set();
  for (let i = 0; i < request.rawHeaders.length; i += 2) {
    const name = request.rawHeaders[i].toLowerCase();
    if (!allowed.has(name)) continue;
    if (seen.has(name)) return reply(response, 401, '{"accepted":false}');
    seen.add(name); headers[name] = request.rawHeaders[i + 1];
  }
  const chunks = []; let length = 0;
  request.on('data', chunk => { length += chunk.length; if (length <= 262144) chunks.push(chunk); });
  request.on('error', () => response.destroy());
  request.on('end', async () => {
    if (length !== Number(request.headers['content-length']) || length > 262144) return reply(response, 400, '{"accepted":false}');
    try {
      const upstream = await fetch(target, { method: 'POST', headers, body: Buffer.concat(chunks), redirect: 'manual', signal: AbortSignal.timeout(12000) });
      // Bound allocation while reading, not only after an arbitrarily large body.
      const reader = upstream.body?.getReader();
      if (!reader) throw new Error('Missing receipt');
      const parts = []; let received = 0;
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          received += value.byteLength;
          if (received > 4096) { await reader.cancel(); throw new Error('Invalid receipt'); }
          parts.push(Buffer.from(value));
        }
      } finally { reader.releaseLock(); }
      const bytes = Buffer.concat(parts, received);
      const receipt = JSON.parse(bytes.toString('utf8'));
      if (upstream.status === 202 && receipt.accepted === true && typeof receipt.duplicate === 'boolean') return reply(response, 202, JSON.stringify({ accepted: true, duplicate: receipt.duplicate }));
      return reply(response, [400, 401, 409, 413, 415, 429].includes(upstream.status) ? upstream.status : 503, '{"accepted":false}');
    } catch { return reply(response, 503, '{"accepted":false}'); }
  });
});
server.requestTimeout = 5000; server.headersTimeout = 5000; server.keepAliveTimeout = 1000;
server.listen(Number(process.env.KDP_STORE_RELAY_PORT ?? 8118), '127.0.0.1', () => console.log('{"listening":true,"loopback_only":true}'));
```

## Test and operating boundary

This exact ingress was executed for all eight provider contracts and both one-time/recurring profiles where supplied. It preserves the original bytes and allowlisted authentication headers and refuses duplicate relevant headers. The response stream is bounded during reading. Local test overrides require [Sandbox](/docs/v1/start/glossary#sandbox) plus literal HTTP 127.0.0.1 and explicit consent flags; remove them in deployment. A public HTTPS proxy/merchant account was not used in these local contract tests.


## Prerequisites

Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated Sandbox [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application.


## Expected result

Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting [license](/docs/v1/start/glossary#license) with the same buyer email; no full key appears in public replies.


## Common mistakes

Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker.


## Security notes

Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed.


## Troubleshooting

For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly.


## Next steps

Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live).


## Related

- [Generic store trust boundary](/docs/v1/guides/any-store)
- [API idempotency](/docs/v1/api/idempotency)
- [Private service setup](/docs/v1/guides/store-service)
- [Manual key delivery](/docs/v1/guides/manual-issuance)
