diff --git a/.github/workflows/laravel-test.yml b/.github/workflows/laravel-test.yml
index 7ab73788..b4e264b0 100644
--- a/.github/workflows/laravel-test.yml
+++ b/.github/workflows/laravel-test.yml
@@ -26,6 +26,7 @@ jobs:
 
     strategy:
       fail-fast: false
+      max-parallel: 3
       matrix:
         php-versions: ["8.1", "8.2", "8.3"]
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..a563655c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,35 @@
+name: Upload Release assets to GitHub
+
+on:
+  push:
+    tags:
+      - "*"
+
+jobs:
+  mariadb-assets:
+    name: Upload MariaDB assets
+
+    strategy:
+      matrix:
+        mariadb-versions: ["10", "11"]
+
+    uses: ./.github/workflows/upload-db-assets.yml
+    with:
+      db-type: mysql
+      db-version: ${{ matrix.mariadb-versions }}
+
+  postgres-assets:
+    name: Upload PostgreSQL assets
+
+    strategy:
+      matrix:
+        pgsql-versions: ["14", "15", "16"]
+
+    uses: ./.github/workflows/upload-db-assets.yml
+    with:
+      db-type: pgsql
+      db-version: ${{ matrix.pgsql-versions }}
+
+  bundle-assets:
+    name: Upload bundle assets
+    uses: ./.github/workflows/upload-file-assets.yml
diff --git a/.github/workflows/laravel.yml b/.github/workflows/tests.yml
similarity index 100%
rename from .github/workflows/laravel.yml
rename to .github/workflows/tests.yml
diff --git a/.github/workflows/upload-db-assets.yml b/.github/workflows/upload-db-assets.yml
new file mode 100644
index 00000000..73fccefa
--- /dev/null
+++ b/.github/workflows/upload-db-assets.yml
@@ -0,0 +1,81 @@
+name: Publish Release db dump assets
+
+on:
+  workflow_call:
+    inputs:
+      db-type:
+        description: "Database type"
+        required: true
+        type: string
+      db-version:
+        description: "Database version"
+        required: true
+        type: string
+
+permissions:
+  contents: write
+
+env:
+  DB_CONNECTION: ${{ inputs.db-type }}
+  DB_PORT: ${{ inputs.db-type == 'mysql' && '3306' || inputs.db-type == 'pgsql' && '5432' }}
+  DB_USERNAME: radioroster
+  DB_PASSWORD: releasePassword
+  DB_DATABASE: radioroster
+  APP_ENV: production
+  APP_DEBUG: false
+
+jobs:
+  mariadb-release-dump:
+    name: Add ${{ inputs.db-type == 'mysql' && 'MariaDB' || inputs.db-type == 'pgsql' && 'PostgreSQL' }} ${{ inputs.db-version }} dump to release
+    runs-on: ubuntu-latest
+
+    steps:
+      - if: ${{ inputs.db-type == 'mysql' }}
+        name: Setup MariaDB ${{ inputs.db-version }}
+        run: |
+          docker run -d --name mariadb -e MARIADB_RANDOM_ROOT_PASSWORD=true -e MARIADB_DATABASE=${{ env.DB_DATABASE }} -e MARIADB_USER=${{ env.DB_USERNAME }} -e MARIADB_PASSWORD=${{ env.DB_PASSWORD }} --publish 3306:3306 mariadb:${{ inputs.db-version }}
+
+      - if: ${{ inputs.db-type == 'pgsql' }}
+        name: Setup PostgreSQL ${{ inputs.db-version }}
+        run: |
+          docker run -d --name postgres -e POSTGRES_DB=${{ env.DB_DATABASE }} -e POSTGRES_USER=${{ env.DB_USERNAME }} -e POSTGRES_PASSWORD=${{ env.DB_PASSWORD }} --publish 5432:5432 postgres:${{ inputs.db-version }}
+
+      - name: Checkout
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
+
+      - name: Setup PHP 8.1
+        uses: shivammathur/setup-php@e8cd65f444039503a75cf4057d55442fc4316f78
+        with:
+          php-version: "8.1"
+          extensions: pcntl, zip, intl, exif, mbstring, dom, fileinfo, ${{ inputs.db-type == 'mysql' && 'pdo_mysql' || inputs.db-type == 'pgsql' && 'pdo_pgsql' }}
+
+      - name: Install Composer Dependencies
+        run: composer install -q --no-interaction --no-scripts --no-progress --prefer-dist --optimize-autoloader
+
+      - name: Prepare Laravel Application
+        run: |
+          php -r "file_exists('.env') || copy('.env.example', '.env');"
+          php artisan key:generate
+
+      - name: Clear config
+        run: php artisan config:clear
+
+      - name: Run Migrations
+        run: php artisan migrate --force
+
+      - if: ${{ inputs.db-type == 'mysql' }}
+        name: Dump MariaDB ${{ inputs.db-type == 'mysql' && inputs.db-version }} database
+        run: |
+          mkdir -p database/dumps
+          docker exec mariadb mysqldump --user $DB_USERNAME --password=$DB_PASSWORD $DB_DATABASE | gzip > database/dumps/radioroster_${{ github.ref_name }}-mariadb${{ inputs.db-version }}.sql.gz
+
+      - if: ${{ inputs.db-type == 'pgsql' }}
+        name: Dump PostgreSQL ${{ inputs.db-type == 'pgsql' && inputs.db-version }} database
+        run: |
+          mkdir -p database/dumps
+          docker exec postgres sh -c 'export PGPASSWORD=${{ env.DB_PASSWORD }} && pg_dump -Fc -Z 6 -U${{ env.DB_USERNAME }} $DB_DATABASE' > database/dumps/radioroster_${{ github.ref_name }}-postgres${{ inputs.db-version }}.sql.gz
+
+      - name: Upload database dump
+        uses: softprops/action-gh-release@v0.1.15
+        with:
+          files: database/dumps/radioroster_${{ github.ref_name }}${{ inputs.db-type == 'mysql' && '-mariadb' || inputs.db-type == 'pgsql' && '-postgres' }}${{ inputs.db-version }}.sql.gz
diff --git a/.github/workflows/upload-file-assets.yml b/.github/workflows/upload-file-assets.yml
new file mode 100644
index 00000000..00467896
--- /dev/null
+++ b/.github/workflows/upload-file-assets.yml
@@ -0,0 +1,41 @@
+name: Upload bundle file as assets
+
+on:
+  workflow_call:
+
+permissions:
+  contents: write
+
+env:
+  APP_ENV: production
+  APP_DEBUG: false
+
+jobs:
+  build-assets:
+    name: Build assets with PHP ${{ matrix.php-versions }}
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        php-versions: ["8.1", "8.2", "8.3"]
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
+
+      - name: Setup PHP ${{ matrix.php-versions }}
+        uses: shivammathur/setup-php@e8cd65f444039503a75cf4057d55442fc4316f78
+        with:
+          php-version: ${{ matrix.php-versions }}
+          extensions: pcntl, zip, intl, exif, mbstring, dom, fileinfo
+
+      - name: Install Composer Dependencies
+        run: composer install -q --no-interaction --no-scripts --no-progress --prefer-dist --optimize-autoloader
+
+      - name: Compress into Zip file
+        run: zip -r -q -9 -X radioroster_${{ github.ref_name }}-php${{ matrix.php-versions }}.zip .
+
+      - name: Upload zip file as asset
+        uses: softprops/action-gh-release@v0.1.15
+        with:
+          files: radioroster_${{ github.ref_name }}-php${{ matrix.php-versions }}.zip
diff --git a/app/Http/Middleware/JsonOnlyMiddleware.php b/app/Http/Middleware/JsonOnlyMiddleware.php
index 1188a226..2535c71b 100644
--- a/app/Http/Middleware/JsonOnlyMiddleware.php
+++ b/app/Http/Middleware/JsonOnlyMiddleware.php
@@ -15,7 +15,7 @@ class JsonOnlyMiddleware
      */
     public function handle(Request $request, Closure $next): Response
     {
-        if (!empty($request->all()) && !$request->isJson()) {
+        if (!empty($request->getContent()) && !$request->isJson()) {
             return response()->json([
                 'message' => 'Only JSON requests are accepted'
             ], Response::HTTP_BAD_REQUEST);
diff --git a/tests/Feature/Http/Middleware/JsonOnlyMiddlewareTest.php b/tests/Feature/Http/Middleware/JsonOnlyMiddlewareTest.php
index 1aee383b..f29dfa1a 100644
--- a/tests/Feature/Http/Middleware/JsonOnlyMiddlewareTest.php
+++ b/tests/Feature/Http/Middleware/JsonOnlyMiddlewareTest.php
@@ -18,13 +18,19 @@ public function test_non_json_requests_are_rejected(): void
             'password' => bcrypt('ValidPassword'),
         ]);
 
-        $response = $this->post('/api/v1/login', [
-            'body' => '<?xml version="1.0" encoding="UTF-8"?>
+        $response = $this->call(
+            'POST',
+            '/api/v1/login',
+            server: $this->transformHeadersToServerVars([
+                'Accept' => 'text/xml, application/xml, text/plain',
+                'Content-Type' => 'text/xml, application/xml, text/plain',
+            ]),
+            content: '<?xml version="1.0" encoding="UTF-8"?>
                 <root>
                     <email>valid@example.com</email>
                     <password>ValidPassword</password>
                 </root>'
-        ], ['Content-Type' => 'text/xml']);
+        );
 
         $response->assertStatus(400)
             ->assertJson([