Skip to content

Latest commit

 

History

63 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fuik

A fish trap for webhooks

Fuik (Dutch for fish trap) is a Rails engine that catches and stores webhooks from any provider. View all events in the admin interface, then create event classes to add your business logic.

Fuik admin interface

Sponsored By Rails Designer

Rails Designer

Quick start

# Install
bundle add fuik
bin/rails generate fuik:install
bin/rails db:migrate

# Point your webhook to
POST https://yourdomain.com/webhooks/stripe

That's it. Webhooks are captured and visible at /webhooks.

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.

Fuik event detail interface

⚠️ 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 .json to 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.rb
  • app/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

Custom job class

Webhook events are processed asynchronously by Fuik::WebhookProcessingJob. When processing raises, the event is marked as failed so you can retry it from the dashboard.

Define a custom job class, then configure Fuik to use it:

# app/jobs/webhook_processing_job.rb
class WebhookProcessingJob < ApplicationJob
  queue_as :webhooks

  retry_on MyApp::WebhookError, attempts: 5
  discard_on ActiveJob::DeserializationError

  def perform(event_class_name, webhook_event)
    event_class_name.safe_constantize.new(webhook_event).process!
  end
end
# config/initializers/fuik.rb
Fuik.configure do |config|
  config.webhook_processing_job_class = "WebhookProcessingJob"
end

The perform method receives two arguments:

  • event_class_name: a string like "Stripe::CheckoutSessionCompleted" that resolves to your event class
  • webhook_event: the Fuik::WebhookEvent record with the payload, headers and status

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:

  1. provider Base class (if configured);
  2. common headers (X-Github-Event, X-Event-Type, etc.);
  3. payload (type, event, event_type);
  4. falls back to "unknown".

Event ID:

  1. provider Base class (if configured);
  2. common headers (X-GitHub-Delivery, X-Event-Id, etc.);
  3. payload (id).
  4. 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.tt with signature verification (if applicable);
  • event class templates with helpful TODO comments.

Contributing

This project uses Standard for formatting Ruby code. Please make sure to run rake before submitting pull requests.

License

The gem is available as open source under the terms of the MIT License.

About

Rails engine that catches and stores webhooks from any provider

Topics

Resources

Stars

94 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Contributors

Languages