Production — showcase/production
state_stores:
local:
namespace: hookway
memory: {} # Per process; select Redis or PostgreSQL for shared protection.
# Production showcase: gates, HMAC auth, replay protection, sampling, filtering, a
# switch, transform, code, http_request with on_error, and fan-out publishes.
# A second flow is the sync proxy; a third is a CIDR-trusted ingest (no_auth).
# lookup is omitted: the standalone server has no lookup provider binding.
#
# Requires GITHUB_WEBHOOK_SECRET, ARCHIVE_BUCKET, RABBITMQ_URL, KAFKA_BROKER_*,
# POSTGRES_URL, UPSTREAM_BASE, and PROXY_TOKEN in the environment.
server:
addr: ":8080"
read_timeout: "5s"
write_timeout: "10s"
idle_timeout: "60s"
shutdown_timeout: "10s"
publish_timeout: "10s"
max_body_bytes: 10485760
max_inflight_requests: 256
fanout_concurrency: 8
routing:
profiles: {sample: {account_id: "000000000000", credential_mode: sample}}
environments:
sample: {profiles: [sample], accounts: ["000000000000"], destination_prefix: internal}
security:
trusted_proxy_headers: true
trusted_proxies:
- "10.0.0.0/8"
- "192.168.0.0/16"
destinations:
archive:
type: s3
s3:
region: "us-east-1"
bucket: "${ARCHIVE_BUCKET}"
orders:
type: sqs
sqs:
region: "us-east-1"
events:
type: kafka
kafka:
brokers:
- "${KAFKA_BROKER_1}"
- "${KAFKA_BROKER_2}"
acks: "all"
dlq:
type: rabbitmq
rabbitmq:
url: "${RABBITMQ_URL}"
exchange: "hookway.dlq"
exchange_type: "direct"
durable: true
declare_queues: true
warehouse:
type: postgres
postgres:
url: "${POSTGRES_URL}"
table: "webhook_events"
flows:
github:
delivery_failure_policy: skip_failed
nodes:
- {id: in, type: http_trigger, description: "GitHub delivers push and pull request events here.", path: /webhooks/github, methods: [POST]}
- {id: hops, type: loop_guard, description: "Rejects requests that already looped through Hookway.", max_hops: 3}
- {id: rl, type: rate_limit, store: local, description: "Sheds bursts per source IP before the body is read.", rpm: 60, burst: 20, key_by: source_ip}
- {id: body, type: read_body, description: "Reads the raw body, rejecting anything over 1 MiB.", max_bytes: 1048576}
- id: auth
type: auth
description: "Verifies the GitHub HMAC signature over the raw body."
scheme: hmac_sha256_hex
secret: "${GITHUB_WEBHOOK_SECRET}"
signature_header: X-Hub-Signature-256
- {id: replay, type: replay_guard, store: local, description: "Drops redeliveries of an event already seen today.", ttl: 24h, max_entries: 100000}
- {id: env, type: envelope, description: "Builds the JSON event envelope consumers receive.", body_format: json}
- id: shape
type: json_validate
description: "Checks that the GitHub event has the fields used by the following nodes."
schema:
type: object
required: [repository, ref]
properties:
repository: {type: object, required: [private, full_name], properties: {private: {type: boolean}, full_name: {type: string}}}
ref: {type: string}
- {id: archive, type: publish, description: "Archives every verified event to S3, unfiltered.", destination: archive, target: github, create: true}
# Keep every event initially; lower the rate to sample the push branch.
- {id: sample_events, type: sample, description: "Randomly selects events for the push branch; 1 keeps all.", rate: 1}
- id: pushes
type: filter
description: "Keeps only pushes to public repositories."
on_no_match: drop
conditions:
- {source: header, path: X-GitHub-Event, operator: eq, value: push}
- {source: body, path: repository.private, operator: eq, value: false}
- id: map
type: transform
description: "Lifts the repository name into vars.repo for the ack URL."
kind: field_map
mappings:
- {from: body.repository.full_name, to: vars.repo}
- id: snippet
type: code
description: "Tags the payload so consumers can tell it came through Hookway."
timeout: 50ms
script: "return { body: input.body, vars: Object.assign({}, input.vars, { ingested: 'true' }) };"
- id: ack
type: http_request
description: "Acknowledges to the upstream API; a 5xx takes the on_error port."
url: "https://example.test/hooks/{repo}"
timeout: 5s
error_on_status: [5xx]
body: transformed
- id: route
type: switch
description: "Splits main-branch pushes from every other branch."
default: other
cases:
- port: main
when: {source: body, path: ref, operator: eq, value: "refs/heads/main"}
- id: gate
type: if_else
description: "Private repos (or a forced push) go to the order pipeline; the rest go to Kafka."
match: any
conditions:
- {source: body, path: repository.private, operator: eq, value: true}
- {source: header, path: X-Hookway-Force, operator: exists}
- {id: orders, type: publish, description: "Order pipeline queue for private main-branch pushes.", destination: orders, target: github-main, create: true}
- {id: public_main, type: publish, description: "Kafka topic for public main-branch pushes.", destination: events, target: github-public, create: true}
- {id: stream, type: publish, description: "Kafka topic for pushes to every other branch.", destination: events, target: github-pushes, create: true}
- {id: dlq, type: publish, description: "Dead letter queue for failed acknowledgements.", destination: dlq, target: github-failed, create: true}
edges:
- in>hops
- hops>rl
- rl>body
- body>auth
- auth>replay
- replay>env
- env>shape
- shape>archive
- shape>sample_events
- sample_events>pushes
- pushes>map
- map>snippet
- snippet>ack
- ack>route
- ack:on_error>dlq
- route:main>gate
- route:other>stream
- gate:true>orders
- gate:false>public_main
proxy:
nodes:
- {id: in, type: http_trigger, description: "Accepts any method under the upstream proxy prefix.", path: "/upstream/proxy/{rest...}", methods: [ANY]}
- {id: rl, type: rate_limit, store: local, description: "Caps proxy traffic at 120 requests per minute.", rpm: 120}
- {id: body, type: read_body, description: "Buffers the body so it can be signed and forwarded.", max_bytes: 10485760, allow_empty: true}
- {id: auth, type: auth, description: "Checks the shared proxy token in X-Auth.", scheme: exact, secret: "${PROXY_TOKEN}", signature_header: X-Auth}
- {id: env, type: envelope, description: "Leaves the body unparsed so the proxy stays byte-exact.", body_format: raw}
- {id: up, type: http_request, description: "Forwards method, query, and trailing path upstream.", url: "${UPSTREAM_BASE}/{rest}", forward_method: true, forward_query: true, remove_headers: [X-Auth], timeout: 10s}
- {id: resp, type: respond, description: "Returns the upstream response to the caller unchanged.", passthrough: true}
edges: [in>rl, rl>body, body>auth, auth>env, env>up, up>resp]
trusted:
nodes:
- {id: in, type: http_trigger, description: "Internal job events published from inside the VPC.", path: /webhooks/internal/jobs, methods: [POST]}
- {id: allow, type: ip_allowlist, description: "Restricts the endpoint to private network ranges.", cidrs: ["10.0.0.0/8", "192.168.0.0/16"]}
- {id: rl, type: rate_limit, store: local, description: "Modest per-IP cap for internal callers.", rpm: 30, key_by: source_ip}
- {id: body, type: read_body, description: "Reads the raw body, rejecting anything over 1 MiB.", max_bytes: 1048576}
- {id: auth, type: no_auth, description: "Network trust replaces signature verification here."}
- {id: env, type: envelope, description: "Builds the JSON event envelope consumers receive.", body_format: json}
- {id: admission, type: route, description: "Admits the configured receiving environment, ignoring request overrides.", profile: sample, fixed: sample}
- {id: out, type: publish, description: "Inserts each event into the Postgres warehouse table.", destination: warehouse, target: '{route_prefix}-jobs', create: true}
edges: [in>allow, allow>rl, rl>body, body>auth, auth>env, env>admission, admission>out]