Fuik
Rails engine that catches and stores webhooks from any provider
Installation
Add to your Gemfile:
gem "fuik"
Then run:
bundle install
bin/rails generate fuik:install
bin/rails db:migrate
The engine mounts at /webhooks automatically.
Usage
View events
Visit /webhooks to see all received webhooks. Click any event to view all the payload details and copy the payload or download as JSON.
⚠️ The /webhooks path is by default not protected. Set Fuik.configuration.events_controller_parent to a controller that requires authentication.
Dashboard features
- Copy payload as JSON: click a button, payload is in your clipboard
- Download payload as JSON file: keep it for testing, debugging or throw it at your LLM agent, bot or colleague
-
Add
.jsonto any URL: get the raw payload without the UI -
GET /events.json: index as JSON with filter support -
Click any key to get the Ruby accessor path: click any key and get, e.g.
payload["line_items"][0]["product_id"](say what? 🤯)
Add business logic
Generate classes for events you want to process:
bin/rails generate fuik:provider stripe checkout_session_completed
This creates:
app/webhooks/stripe/base.rbapp/webhooks/stripe/checkout_session_completed.rb
Each class is a thin wrapper around your business logic:
module Stripe
class CheckoutSessionCompleted < Base
def process!
User.find_by(id: payload.client_reference_id).tap do |user|
user.activate_subscription!
user.send_welcome_email
# etc.
end
@webhook_event.processed!
end
end
end
The payload method supports dot notation and standard hash methods:
# Dot notation
payload.client_reference_id
payload.customer.email
# Hash syntax (strings/symbols)
payload["client_reference_id"]
payload[:customer_id]
Implement Base.verify! to enable signature verification:
module Stripe
class Base < Fuik::Event
def self.verify!(request)
secret = Rails.application.credentials.dig(:stripe, :signing_secret)
signature = request.headers["Stripe-Signature"]
Stripe::Webhook.construct_event(
request.raw_post,
signature,
secret
)
rescue Stripe::SignatureVerificationError => error
raise Fuik::InvalidSignature, error.message
end
end
end
If Provider::Base.verify! exists, Fuik calls it automatically. Invalid signatures return 401 without storing the webhook.
Configuration
Configure Fuik in config/initializers/fuik.rb:
Fuik.configure do |config|
# config.events_controller_parent = "AdministrationController"
# config.providers_allowed = %w[stripe github postmark]
end
Testing event classes
Unit test your process! methods without a database using the built-in test helper:
# test/test_helper.rb
require "fuik/test_helpers"
class ActiveSupport::TestCase
include Fuik::TestHelpers
end
# test/webhooks/stripe/checkout_session_completed_test.rb
module Stripe
class CheckoutSessionCompletedTest < ActiveSupport::TestCase
test "processes a completed checkout" do
event = build_webhook_event(
provider: "stripe",
event_type: "checkout.session.completed",
payload: { "client_reference_id" => "user_123" }
)
CheckoutSessionCompleted.new(event).process!
assert_equal "processed", event.status
end
test "fails when customer is not found" do
event = build_webhook_event(
provider: "stripe",
event_type: "checkout.session.completed",
payload: {}
)
CheckoutSessionCompleted.new(event).process!
assert_equal "failed", event.status
assert_equal "Customer not found", event.error
end
end
end
Your process! calls @webhook_event.processed! on success and @webhook_event.failed!(error) on failure:
module Stripe
class CheckoutSessionCompleted < Base
def process!
user = User.find_by(id: payload.client_reference_id)
if user.present?
user.activate_subscription!
@webhook_event.processed!
else
@webhook_event.failed!("Customer not found")
end
end
end
end
Provider allowlist
By default:
- Development/test: all providers are allowed
-
Production/staging: only providers in
app/webhooks/are allowed
Configure with Fuik.configuration.providers_allowed:
# Allow all (including production)
Fuik.configuration.providers_allowed = :all
# Explicit allowlist (overrides directory scan)
Fuik.configuration.providers_allowed = %w[stripe github postmark]
Unknown providers return 404 Not Found.
Event type & ID lookup
Fuik automatically extracts event types and IDs from common locations:
Event Type:
- provider Base class (if configured);
- common headers (
X-Github-Event,X-Event-Type, etc.); - payload (
type,event,event_type); - falls back to
"unknown".
Event ID:
- provider Base class (if configured);
- common headers (
X-GitHub-Delivery,X-Event-Id, etc.); - payload (
id). - falls back to MD5 hash of request body.
Custom lookup
Define event_type and/or event_id on your provider’s Base class if Fuik can’t find them on its own:
module CustomProvider
class Base < Fuik::Event
event_type header: "X-Custom-Event"
event_id header: "X-Custom-Id"
end
end
You can also read from the payload body or use a static value:
event_type payload: "event_type" # payload["event_type"]
event_type payload: "data.type" # nested via dot-notation (payload["data"]["type"])
event_type "always_this_value" # static/literal
event_id payload: "id"
Pre-packaged providers
Fuik includes ready-to-use templates for common providers.
Monitoring
Fuik publishes lifecycle events via ActiveSupport::Notifications. Subscribe to track webhook activity:
ActiveSupport::Notifications.subscribe("webhook_received.fuik") do |event|
Rails.logger.info("[Fuik] Received #{event.payload[:provider]}:#{event.payload[:event_type]}")
end
Available events:
| Event | When it fires |
|---|---|
webhook_received.fuik |
Event record created |
webhook_processed.fuik |
process! completed |
webhook_failed.fuik |
process! raised |
webhook_signature_invalid.fuik |
Signature verification failed (401) |
webhook_receive_error.fuik |
Unexpected error during receive (500) |
Add your custom provider
Have a provider template others could use? Add it to lib/generators/fuik/provider/templates/your_provider/ and submit a PR!
Include:
-
base.rb.ttwith signature verification (if applicable); - event class templates with helpful TODO comments.