diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4a67df9..8c20d16 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,21 +16,17 @@ jobs:
       matrix:
         php: [ 8.1, 8.2, 8.3, 8.4 ]
         laravel: [ 8, 9, 10, 11 ]
-        stability: [ 'prefer-lowest', 'prefer-stable' ]
-        include:
-          - laravel: 8.*
-            testbench: ^8.20
-          - laravel: 9.*
-            testbench: ^8.20
-          - laravel: 10.*
-            testbench: ^8.20
-          - laravel: 11.*
-            testbench: ^9.0
         exclude:
           - php: 8.1
-            laravel: 11.*
+            laravel: 11
+          - php: 8.4
+            laravel: 8
+          - php: 8.4
+            laravel: 9
+          - php: 8.4
+            laravel: 10
 
-    name:  PHP ${{ matrix.php }} L${{ matrix.laravel }} w/ ${{ matrix.stability }}
+    name:  PHP ${{ matrix.php }} L${{ matrix.laravel }}
     steps:
       - name: Checkout code
         uses: actions/checkout@v3
@@ -49,8 +45,7 @@ jobs:
         with:
           timeout_minutes: 5
           max_attempts: 5
-          command: |
-            composer update --prefer-dist --no-interaction --no-progress --${{ matrix.stability }}
+          command: composer update --prefer-dist --no-interaction --no-progress --prefer-stable
 
       - name: Execute tests
-        run: vendor/bin/phpunit --verbose
+        run: vendor/bin/phpunit
diff --git a/composer.json b/composer.json
index e6f4169..d307a28 100644
--- a/composer.json
+++ b/composer.json
@@ -9,13 +9,13 @@
     },
     "require": {
         "php": "~8.1.0|~8.2.0|~8.3.0|~8.4.0",
-        "illuminate/http": "^8.83|^9.33|^10.0|^11.0",
-        "illuminate/support": "^8.83|^9.33|^10.0|^11.0"
+        "illuminate/http": "^8.83|^9.33|^10.0|^11.3",
+        "illuminate/support": "^8.83|^9.33|^10.0|^11.3"
     },
     "require-dev": {
-        "nunomaduro/larastan": "^2.0",
-        "orchestra/testbench": "^7.0|^8.0|^9.0",
-        "phpunit/phpunit": "^9.5.10",
+        "nunomaduro/larastan": "^1.0|^2.0",
+        "orchestra/testbench": "^6.0|^7.0|^8.2|^9.0",
+        "phpunit/phpunit": "^9.5.10|^10.1|^11.3.6",
         "roave/security-advisories": "dev-latest"
     },
     "autoload": {
@@ -34,6 +34,13 @@
     "config": {
         "sort-packages": true
     },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "LemonSqueezy\\PlainUiComponents\\PlainUiComponentsServiceProvider"
+            ]
+        }
+    },
     "minimum-stability": "dev",
     "prefer-stable": true
 }
diff --git a/config/plain.php b/config/plain.php
new file mode 100644
index 0000000..571511c
--- /dev/null
+++ b/config/plain.php
@@ -0,0 +1,13 @@
+<?php
+
+return [
+
+    /**
+     * Your Plain workspace's global HMAC secret.
+     *
+     * This secret can be viewed and (re)generated by Plain workspace admins in Settings → Request signing.
+     * This will be used to verify that request were made by Plain, and not a third party.
+     */
+    'secret' => env('PLAIN_SECRET'),
+
+];
diff --git a/src/PlainUiComponentsServiceProvider.php b/src/PlainUiComponentsServiceProvider.php
new file mode 100644
index 0000000..d7b84b8
--- /dev/null
+++ b/src/PlainUiComponentsServiceProvider.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace LemonSqueezy\PlainUiComponents;
+
+use Illuminate\Support\ServiceProvider;
+
+class PlainUiComponentsServiceProvider extends ServiceProvider
+{
+    /**
+     * Register the service provider.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        $this->mergeConfigFrom(__DIR__.'/../config/plain.php', 'plain');
+    }
+
+    /**
+     * Bootstrap the application services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        if ($this->app->runningInConsole()) {
+            $this->publishes([
+                __DIR__.'/../config/plain.php' => config_path('plain.php'),
+            ]);
+        }
+    }
+}
diff --git a/src/VerifyPlainSignatureMiddleware.php b/src/VerifyPlainSignatureMiddleware.php
new file mode 100644
index 0000000..8ee182a
--- /dev/null
+++ b/src/VerifyPlainSignatureMiddleware.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace LemonSqueezy\PlainUiComponents;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Config;
+use Symfony\Component\HttpFoundation\Response;
+
+class VerifyPlainSignatureMiddleware
+{
+    /**
+     * Handle the incoming request.
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        abort_if(empty($signature = $request->header('plain-request-signature')), 400, 'Missing webhook signature.');
+        abort_if(is_null($secret = Config::get('plain.secret')), 403, 'No webhook secret configured.');
+        abort_unless(hash_equals(hash_hmac('sha256', $request->getContent(), $secret), $signature), 403, 'Invalid signature.');
+
+        return $next($request);
+    }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 3040af9..2c225b9 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -2,7 +2,14 @@
 
 namespace LemonSqueezy\PlainUiComponents\Tests;
 
-class TestCase extends \PHPUnit\Framework\TestCase
+use LemonSqueezy\PlainUiComponents\PlainUiComponentsServiceProvider;
+
+class TestCase extends \Orchestra\Testbench\TestCase
 {
-    //
+    protected function getPackageProviders($app): array
+    {
+        return [
+            PlainUiComponentsServiceProvider::class,
+        ];
+    }
 }
diff --git a/tests/VerifyPlainSignatureMiddlewareTest.php b/tests/VerifyPlainSignatureMiddlewareTest.php
new file mode 100644
index 0000000..5e03a33
--- /dev/null
+++ b/tests/VerifyPlainSignatureMiddlewareTest.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace LemonSqueezy\PlainUiComponents\Tests;
+
+use Illuminate\Support\Facades\Config;
+use Illuminate\Support\Facades\Route;
+use LemonSqueezy\PlainUiComponents\VerifyPlainSignatureMiddleware;
+
+class VerifyPlainSignatureMiddlewareTest extends TestCase
+{
+    /** @test */
+    public function it_blocks_the_request_when_the_plain_signature_header_is_not_provided(): void
+    {
+        Config::set('plain.secret', 'my-plain-secret');
+
+        $called = false;
+        Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
+            $called = true;
+        });
+
+        $response = $this->post('/', ['example' => 'content']);
+
+        $this->assertFalse($called);
+        $response->assertStatus(400);
+    }
+
+    /** @test */
+    public function it_blocks_the_request_when_the_middleware_is_applied_without_a_secret_being_configured(): void
+    {
+        Config::set('plain.secret', '');
+
+        $called = false;
+        Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
+            $called = true;
+        });
+
+        $response = $this->post('/', ['example' => 'content'], [
+            'plain-request-signature' => 'example-signature',
+        ]);
+
+        $this->assertFalse($called);
+        $response->assertStatus(403);
+    }
+
+    /** @test */
+    public function it_blocks_the_request_when_the_plain_signature_header_does_not_match_the_configured_secret(): void
+    {
+        Config::set('plain.secret', 'my-plain-secret');
+
+        $called = false;
+        Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
+            $called = true;
+        });
+
+        $response = $this->post('/', ['example' => 'content'], [
+            'plain-request-signature' => 'example-signature',
+        ]);
+
+        $this->assertFalse($called);
+        $response->assertStatus(403);
+    }
+
+    /** @test */
+    public function it_allows_the_request_when_the_plain_signature_header_matches_the_configured_secret(): void
+    {
+        Config::set('plain.secret', 'my-plain-secret');
+
+        $called = false;
+        Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
+            $called = true;
+        });
+
+        $response = $this->post('/', ['example' => 'content'], [
+            'plain-request-signature' => 'e85ab1c2f80714be422adfc9f446f9c48a018c971df12527fba9b2a2819cd17c',
+        ]);
+
+        $this->assertTrue($called);
+        $response->assertStatus(200);
+    }
+}