PHPackages                             bootdesk/chat-sdk-laravel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. bootdesk/chat-sdk-laravel

ActiveLibrary[API Development](/categories/api)

bootdesk/chat-sdk-laravel
=========================

Laravel integration for the PHP Chat SDK

0.4.44(2w ago)0198MITPHPPHP &gt;=8.2

Since Jun 4Pushed 1w agoCompare

[ Source](https://github.com/bootdesk/chat-sdk-laravel)[ Packagist](https://packagist.org/packages/bootdesk/chat-sdk-laravel)[ RSS](/packages/bootdesk-chat-sdk-laravel/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (36)Versions (38)Used By (0)

bootdesk/chat-sdk-laravel
=========================

[](#bootdeskchat-sdk-laravel)

Laravel integration for laravel-bootdesk.

Install
-------

[](#install)

```
composer require bootdesk/chat-sdk-laravel
```

Setup
-----

[](#setup)

```
php artisan chat:install
```

This publishes `config/chat.php` to your application.

Configuration
-------------

[](#configuration)

The published `config/chat.php` file contains the following sections:

```
return [

    // The display name your bot uses when posting messages.
    'user_name' => env('BOT_USERNAME', 'Bot'),

    // Platform adapters to enable. Only adapters whose Composer package
    // is installed (class_exists) will be loaded. For multi-tenant
    // setups, omit the platform here and use an AdapterResolver instead.
    'adapters' => [
        // 'slack' => [
        //     'bot_token' => env('SLACK_BOT_TOKEN'),
        //     'signing_secret' => env('SLACK_SIGNING_SECRET'),
        // ],
        // 'telegram' => [
        //     'bot_token' => env('TELEGRAM_BOT_TOKEN'),
        // ],
        // 'whatsapp' => [
        //     'access_token' => env('WHATSAPP_ACCESS_TOKEN'),
        //     'app_secret' => env('WHATSAPP_APP_SECRET'),
        //     'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'),
        //     'verify_token' => env('WHATSAPP_VERIFY_TOKEN'),
        // ],
        // 'discord' => [
        //     'bot_token' => env('DISCORD_BOT_TOKEN'),
        //     'application_id' => env('DISCORD_APPLICATION_ID'),
        //     'public_key' => env('DISCORD_PUBLIC_KEY'),
        // ],
        // 'messenger' => [
        //     'page_access_token' => env('MESSENGER_PAGE_ACCESS_TOKEN'),
        //     'app_secret' => env('MESSENGER_APP_SECRET'),
        //     'verify_token' => env('MESSENGER_VERIFY_TOKEN'),
        // ],
        // 'web' => [
        //     'user_name' => env('BOT_USERNAME', 'Bot'),
        // ],
        // 'github' => [
        //     'auth_token' => env('GITHUB_TOKEN'),
        //     'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'),
        // ],
        // 'linear' => [
        //     'api_key' => env('LINEAR_API_KEY'),
        //     'webhook_secret' => env('LINEAR_WEBHOOK_SECRET'),
        // ],
    ],

    // State persistence backed by the Laravel Cache facade. The cache
    // store is resolved from the facade at runtime — configure it in
    // config/cache.php as usual.
    'state' => [
        'prefix' => env('CHAT_STATE_PREFIX', 'chat:'),
    ],

    // Global handler classes registered on every Chat instance regardless
    // of adapter. Each class must implement a register($chat) method.
    'handlers' => [
        // \App\Chat\GlobalHandlers::class,
    ],

    // Adapter-specific handler groups. Only the matching group is
    // registered per webhook request, alongside global handlers above.
    'handler_groups' => [
        // 'slack' => [
        //     \App\Chat\SlackHandler::class,
        // ],
        // 'telegram' => [
        //     \App\Chat\TelegramHandler::class,
        // ],
    ],

    // How to handle concurrent messages for the same thread.
    // Core strategies: drop (default), queue, debounce, concurrent.
    // Laravel uses QueueConcurrencyHandler to dispatch jobs for async processing.
    'concurrency' => env('CHAT_CONCURRENCY', 'drop'),

    // Scope for distributed locks: 'thread' (default) or 'channel'.
    // Use 'channel' for platforms like WhatsApp/Telegram where
    // conversations are per-channel (one conversation per phone number).
    'lock_scope' => env('CHAT_LOCK_SCOPE', 'thread'),

    // Cross-platform per-user message persistence. Requires an
    // IdentityResolver bound to the container (IdentityResolver::class).
    'transcripts' => null,

];
```

Webhook Routes
--------------

[](#webhook-routes)

Register a webhook route for incoming platform events:

```
// routes/web.php or routes/api.php
use BootDesk\ChatSDK\Laravel\Http\Controllers\WebhookController;

Route::match(['get', 'post'], '/api/webhooks/{adapter}', WebhookController::class);
```

The `{adapter}` segment matches the keys in your `config/chat.php` adapters array (e.g. `slack`, `telegram`, `discord`).

Handlers
--------

[](#handlers)

Create a handler class to respond to messages:

```
// app/Chat/ChatHandlers.php
namespace App\Chat;

use BootDesk\ChatSDK\Core\Chat;
use BootDesk\ChatSDK\Core\MessageContext;
use BootDesk\ChatSDK\Laravel\Contracts\ChatHandler as ChatHandlerContract;

class ChatHandlers implements ChatHandlerContract
{
    public function register(Chat $chat): void
    {
        $chat->onNewMessage('/^hello$/i', function (MessageContext $ctx) {
            $ctx->thread->post('Hey!');
        });

        $chat->fallback(function (MessageContext $ctx) {
            $ctx->thread->post("I don't understand that.");
        });
    }
}
```

Register it in `config/chat.php`:

```
// Global — fires for every adapter
'handlers' => [\App\Chat\ChatHandlers::class],
```

Or scoped to a specific adapter group:

```
'handler_groups' => [
    'slack' => [\App\Chat\SlackHandlers::class],
    'telegram' => [\App\Chat\TelegramHandlers::class],
],
```

When a webhook arrives for `slack`, both `global` and `slack` group handlers are registered. `telegram` group handlers are skipped.

### Multiple Groups per Channel

[](#multiple-groups-per-channel)

Override `resolveGroups` on the `WebhookController` to route channels to different handler groups — even combining multiple groups:

```
use Illuminate\Http\Request;
use Psr\Http\Message\ServerRequestInterface;

class ChannelAwareController extends WebhookController
{
    protected function resolveGroups(string $adapter, Request $request, ServerRequestInterface $psrRequest): array
    {
        $channel = $request->input('channel_id');

        return match ($channel) {
            'C001' => ['slack', 'internal-support'],
            'C002' => ['slack', 'customer-support'],
            default => [$adapter],
        };
    }
}
```

Then `ChatFactory::forGroups(['slack', 'internal-support'])` merges global handlers + handlers from both groups. Groups are serialized as the `chat_groups` PSR-7 attribute — they survive into async queue jobs automatically.

### Handlers that Inspect the Request

[](#handlers-that-inspect-the-request)

Implement `ChatHandlerWithRequest` to access the PSR-7 request during registration:

```
use BootDesk\ChatSDK\Laravel\Contracts\ChatHandlerWithRequest;

class TenantAwareHandler implements ChatHandlerWithRequest
{
    public function register(Chat $chat, ?ServerRequestInterface $request = null): void
    {
        $tenant = $request?->getHeaderLine('X-Tenant') ?? 'default';

        $chat->onNewMessage('/bill/', function (MessageContext $ctx) use ($tenant) {
            // tenant-specific billing flow
        });
    }
}
```

The factory auto-detects which interface the handler implements — existing `ChatHandler` implementations continue working unchanged.

Middleware
----------

[](#middleware)

Middleware intercept messages at different stages. Register in your handler class:

```
use BootDesk\ChatSDK\Core\Contracts\WebhookMiddleware;
use BootDesk\ChatSDK\Core\Contracts\ReceivingMiddleware;
use BootDesk\ChatSDK\Core\Contracts\SendingMiddleware;

class ChatHandlers
{
    public function register(Chat $chat): void
    {
        // Intercept raw webhook before parsing
        $chat->addWebhookMiddleware(new class implements WebhookMiddleware {
            public function handle(ServerRequestInterface $request, callable $next): ResponseInterface {
                logger()->info('Webhook received', ['path' => $request->getUri()->getPath()]);
                return $next($request);
            }
        });

        // Transform inbound messages before handlers
        $chat->addReceivingMiddleware(new class implements ReceivingMiddleware {
            public function handle(Message $message, Adapter $adapter, callable $next): ?Message {
                // Return null to drop the message
                if (str_contains($message->text, 'blocked')) {
                    return null;
                }
                return $next($message);
            }
        });

        // Transform outbound messages before delivery
        $chat->addSendingMiddleware(new class implements SendingMiddleware {
            public function handle(string $threadId, PostableMessage $message, Adapter $adapter, string $operation, callable $next): ?SentMessage {
                logger()->info('Sending message', ['thread' => $threadId, 'operation' => $operation]);
                return $next($message);
            }
        });
    }
}
```

**Operations:** `post`, `edit`, `postEphemeral`

Transcripts
-----------

[](#transcripts)

Per-user message persistence stored via the Laravel cache. Incoming and outgoing messages are auto-recorded when configured.

### Setup

[](#setup-1)

Bind an `IdentityResolver` and set `transcripts` config:

```
// AppServiceProvider::register()
use BootDesk\ChatSDK\Core\Contracts\IdentityResolver;
use BootDesk\ChatSDK\Core\Author;

$this->app->bind(IdentityResolver::class, fn () => new class implements IdentityResolver {
    public function resolve(Author $author): ?string {
        return $author->id;
    }
});
```

```
// config/chat.php
'transcripts' => ['max_messages' => 100, 'ttl_ms' => 2592000000],
```

### Custom implementation

[](#custom-implementation)

Override the `TranscriptsApi` binding in any service provider:

```
$this->app->bind(TranscriptsApi::class, function ($app) {
    return new MyRedisTranscriptsApi(
        state: $app->make(StateAdapter::class),
        config: ['max_messages' => 200],
    );
});
```

### Usage

[](#usage)

```
$transcripts = $chat->getTranscripts();

// List history for a user
$entries = $transcripts->list('user:U123');

// Each entry has: id, text, authorId, threadId, timestamp, direction

// Count
$count = $transcripts->count('user:U123');

// Delete
$transcripts->delete('user:U123');
```

Multi-Tenant Adapter Resolution
-------------------------------

[](#multi-tenant-adapter-resolution)

For multi-tenant applications where each tenant has their own bot credentials, use an `AdapterResolver`:

```
// app/Chat/MultiTenantAdapterResolver.php
namespace App\Chat;

use BootDesk\ChatSDK\Core\Contracts\Adapter;
use BootDesk\ChatSDK\Core\Contracts\AdapterResolver;
use BootDesk\ChatSDK\Slack\SlackAdapter;
use BootDesk\ChatSDK\Telegram\TelegramAdapter;
use Illuminate\Support\Facades\DB;
use Psr\Http\Message\ServerRequestInterface;

class MultiTenantAdapterResolver implements AdapterResolver
{
    public function resolve(string $name, ?ServerRequestInterface $request): ?Adapter
    {
        // Extract tenant from request (header, subdomain, route param, etc.)
        // When called from a job, $request is null - use other context (job payload, auth, etc.)
        $tenantId = $request?->getHeaderLine('X-Tenant-ID')
            ?? $this->getTenantFromContext();

        if ($tenantId === null || $tenantId === '') {
            return null;
        }

        // Load tenant-specific credentials from database
        $config = DB::table('tenant_chat_configs')
            ->where('tenant_id', $tenantId)
            ->where('adapter', $name)
            ->first();

        if (! $config) {
            return null;
        }

        // Instantiate adapter with tenant credentials
        return match ($name) {
            'slack' => new SlackAdapter(
                botToken: $config->credentials['bot_token'],
                httpClient: app(\Psr\Http\Client\ClientInterface::class),
                signingSecret: $config->credentials['signing_secret'] ?? null,
            ),
            'telegram' => new TelegramAdapter(
                botToken: $config->credentials['bot_token'],
                httpClient: app(\Psr\Http\Client\ClientInterface::class),
                secretToken: $config->credentials['secret_token'] ?? null,
            ),
            default => null,
        };
    }
}
```

Register the resolver in a service provider:

```
// app/Providers/AppServiceProvider.php
use BootDesk\ChatSDK\Core\Contracts\AdapterResolver;

public function register(): void
{
    $this->app->bind(
        AdapterResolver::class,
        \App\Chat\MultiTenantAdapterResolver::class
    );
}
```

**Resolution order:** Tenant-specific (resolver) → Global (config). Tenants can override specific adapters while falling back to global defaults for others.

Injecting ChatFactory
---------------------

[](#injecting-chatfactory)

To send messages programmatically, inject `ChatFactory` and get a Chat instance:

```
use BootDesk\ChatSDK\Laravel\ChatFactory;

class MessageController
{
    public function __construct(
        private ChatFactory $chatFactory,
    ) {}

    public function send()
    {
        $chat = $this->chatFactory->default(); // global handlers only
        $chat->thread('slack:C123')->post('Hello!');
    }
}
```

Or for adapter-specific handlers:

```
$chat = $this->chatFactory->forGroup('slack'); // global + slack handlers
$chat->handleWebhook('slack', $psrRequest);
```

For multiple groups:

```
$chat = $this->chatFactory->forGroups(['slack', 'internal-support']); // global + both groups
```

Artisan Commands
----------------

[](#artisan-commands)

CommandDescription`php artisan chat:list`List configured adapters`php artisan chat:install`Publish config fileQueue Processing
----------------

[](#queue-processing)

The package binds `QueueConcurrencyHandler` as the default `ConcurrencyHandler`. It dispatches jobs as follows: `drop` acquires a lock during the webhook (dispatches `ProcessMessageJob` if acquired, drops silently if held — lock released when job finishes); `queue` and `concurrent` dispatch `ProcessMessageJob`; `debounce` dispatches `ProcessDebouncedMessageJob` (unique delayed job). The debounce job caches the latest message and a `:last` timestamp; on re-dispatch it does **not** restore `:last` — preventing infinite re-dispatch loops. `:latest` and `:skipped` restoration is guarded against overwriting concurrent webhook data. `RequiresSyncResponse` adapters always process inline (within the HTTP request) regardless of strategy.

When the original PSR-7 webhook request is available, `QueueConcurrencyHandler` serializes it into a `RequestContext` value object (method, URI, headers, body, query/parsed/server params, cookies, version, **requestAttributes**) and passes it to every dispatched job. Both `ProcessMessageJob` and `ProcessDebouncedMessageJob` reconstruct the PSR-7 request and pass it to `Chat::resolveAdapter()` — so `AdapterResolver::resolve($name, $request)` receives the original request in both sync and queued contexts. The `requestAttributes` field captures PSR-7 `getAttributes()` — extend `WebhookController` to add tenant/context attributes that survive into jobs.

Make sure your Laravel queue worker is running:

```
php artisan queue:work
```

No manual setup is needed beyond configuring your queue driver in `config/queue.php`.

State
-----

[](#state)

State persistence uses the Laravel `Cache` facade. The cache store is resolved from the facade at runtime — configure it in `config/cache.php` as usual.

Error Handling
--------------

[](#error-handling)

Adapter exceptions bubble up to Laravel's exception handler. Register custom handlers in `app/Exceptions/Handler.php`:

```
use BootDesk\ChatSDK\Core\Exceptions\AdapterException;
use BootDesk\ChatSDK\Core\Exceptions\AuthenticationException;
use BootDesk\ChatSDK\Core\Exceptions\RateLimitException;
use Illuminate\Http\Request;

public function register(): void
{
    $this->renderable(function (AuthenticationException $e, Request $request) {
        return response()->json(['error' => 'Unauthorized'], 401);
    });

    $this->renderable(function (RateLimitException $e, Request $request) {
        return response()->json(['error' => 'Rate limited'], 429);
    });

    $this->renderable(function (AdapterException $e, Request $request) {
        Log::error('Chat adapter error', [
            'message' => $e->getMessage(),
            'adapter' => $request->route('adapter'),
        ]);

        return response()->json(['error' => 'Adapter failed'], 500);
    });
}
```

**Exception types:**

- `AuthenticationException` — Invalid credentials/tokens
- `RateLimitException` — Platform rate limit exceeded
- `AdapterException` — Generic adapter errors
- `ResourceNotFoundException` — Adapter/thread not found
- `ValidationException` — Invalid input data

Documentation
-------------

[](#documentation)

Full API documentation:

License
-------

[](#license)

MIT

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance97

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

37

Last Release

14d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3874705?v=4)[Vin](/maintainers/tryvin)[@tryvin](https://github.com/tryvin)

---

Top Contributors

[![tryvin](https://avatars.githubusercontent.com/u/3874705?v=4)](https://github.com/tryvin "tryvin (1 commits)")

### Embed Badge

![Health badge](/badges/bootdesk-chat-sdk-laravel/health.svg)

```
[![Health](https://phpackages.com/badges/bootdesk-chat-sdk-laravel/health.svg)](https://phpackages.com/packages/bootdesk-chat-sdk-laravel)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[spatie/laravel-export

Create a static site bundle from a Laravel app

674146.0k6](/packages/spatie-laravel-export)[moonshine/moonshine

Laravel administration panel

1.3k253.1k86](/packages/moonshine-moonshine)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77922.3M186](/packages/laravel-mcp)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
