PxlPeak Public Resources

Open reference notes for SMB AI implementation — voice agents, RAG chatbots, n8n workflows. Maintained by https://pxlpeak.com

View the Project on GitHub veyis/pxlpeak-public-resources

n8n Webhook Security — Production Patterns

Most n8n tutorials ship public webhooks with zero auth. That’s fine for personal automation, dangerous for client work. This is what we use at PxlPeak in production.

The default problem

An n8n Webhook node exposes a URL like https://n8n.example.com/webhook/abc123. Out of the box:

For a Shopify order-processing workflow, a misused webhook can create duplicate orders, trigger repeat charges, or spam your fulfillment queue. This has happened to clients who came to us after the incident.

Layer 1 — Signed headers from the source system

If the webhook source is a known platform (Shopify, Twilio, Stripe, Vapi), always verify the signature the platform sends.

Shopify example (in an n8n Code node directly after the Webhook trigger):

const crypto = require('crypto');

const secret = $env.SHOPIFY_WEBHOOK_SECRET;
const hmacHeader = $input.first().json.headers['x-shopify-hmac-sha256'];
const rawBody = $input.first().json.rawBody; // configure Webhook node to pass raw body

const digest = crypto
  .createHmac('sha256', secret)
  .update(rawBody, 'utf8')
  .digest('base64');

if (digest !== hmacHeader) {
  throw new Error('Invalid Shopify webhook signature');
}

return $input.all();

Twilio, Stripe, Vapi all have equivalents. Use them. Do not skip this because “it’s just internal.”

Layer 2 — Shared secret for your own services

For internal-to-internal webhooks (your app calling n8n), use a shared secret in a header. Reject missing or wrong headers before any real work happens.

const expected = $env.INTERNAL_WEBHOOK_SECRET;
const provided = $input.first().json.headers['x-internal-secret'];

if (!provided || !crypto.timingSafeEqual(
  Buffer.from(provided),
  Buffer.from(expected)
)) {
  throw new Error('Unauthorized');
}

Use timingSafeEqual — a naive === is vulnerable to timing attacks. The difference is microseconds but the exploit is real.

Layer 3 — IP allowlisting

If the source is a fixed platform with a published IP range (Shopify, Twilio), put an IP allowlist at the reverse proxy (Nginx, Cloudflare) layer before n8n.

Do not rely on n8n alone for this. Defense in depth.

Layer 4 — Rate limiting

A compromised token can still hammer your webhook. Rate-limit at the proxy. For SMB clients a reasonable default is 60 requests/minute per source IP; higher-volume clients need tuning.

Layer 5 — Payload validation

Before running any business logic, validate the payload has the expected shape.

const required = ['order_id', 'customer_email', 'line_items'];
const body = $input.first().json;

for (const field of required) {
  if (!(field in body)) {
    throw new Error(`Missing required field: ${field}`);
  }
}

If the payload is malformed, fail fast. Don’t half-process.

Layer 6 — Idempotency

Every webhook handler we write assumes the same event may arrive twice. Use the event ID (Shopify: order.id, Twilio: MessageSid) as an idempotency key. Store seen IDs in Redis or Postgres for 24h minimum.

const eventId = $input.first().json.order_id;

// Check Redis for recent occurrence
const seen = await redis.get(`webhook:shopify:${eventId}`);
if (seen) {
  return { status: 'already_processed', event_id: eventId };
}

await redis.set(`webhook:shopify:${eventId}`, '1', 'EX', 86400);

// proceed with real work

This is the single most common production-n8n bug we fix at PxlPeak — clients whose agency set up webhooks without idempotency, then got duplicate orders during a Shopify retry burst.

Layer 7 — Error handling that doesn’t leak

On errors, never echo back the internal state. Return a generic 500 and log the detail to your observability system.

try {
  // ...
} catch (err) {
  console.error(`[webhook-error] ${err.message}`, { event_id: eventId });
  return { status: 'error' };
}

An error message like PostgreSQL: password authentication failed for user "n8n_prod" at host "internal-db.example.com" handed to a random caller is a handoff of your infrastructure.

Layer 8 — Secrets in environment, not workflow

Never hardcode a secret inside a workflow node. Use n8n’s environment variable mechanism or, better, a secrets manager (Doppler, 1Password, AWS Secrets Manager). Rotate quarterly.

The production mindset

Ask for every new webhook:

Most “just get it working” implementations skip these. Four of them are one-liners to add. Add all eight.


Need production-grade n8n built for you? PxlPeak deploys automation workflows for SMB service businesses in 5 business days. Client owns the n8n instance and all credentials. Zero vendor lock-in. Book a 15-min intro.