#Laravel
There's a complete example Laravel application at github.com/bootdesk/chat-sdk/tree/main/examples/hello-world-laravel. Refer to it for a working setup with middleware, handlers, file upload converter, and tenant adapter resolver.
#Installation
composer require bootdesk/chat-sdk-laravel
#Configuration
Publish the config file:
php artisan vendor:publish --tag=chat-config
This creates config/chat.php with these options:
| Option | Default | Description |
|---|---|---|
user_name |
'Bot' |
Bot display name |
adapters |
[] |
Per-adapter credentials |
state.store |
'file' |
Cache store for state |
state.prefix |
'chat:' |
Key prefix for state |
handlers |
[] |
Handler classes to register |
concurrency |
'drop' |
Concurrency strategy (drop/queue/debounce/concurrent) |
lock_scope |
'thread' |
Lock scope for concurrency ('thread' or 'channel') |
transcripts |
null |
Transcript config (requires identity resolver) |
return [
'slack' => [
'bot_token' => env('SLACK_BOT_TOKEN'),
'signing_secret' => env('SLACK_SIGNING_SECRET'),
],
'telegram' => [
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
],
'telnyx' => [
'api_key' => env('TELNYX_API_KEY'),
'messaging_profile_id' => env('TELNYX_MESSAGING_PROFILE_ID'),
'public_key' => env('TELNYX_PUBLIC_KEY'),
'from_number' => env('TELNYX_FROM_NUMBER'),
'agent_id' => env('TELNYX_AGENT_ID'),
],
'concurrency' => 'drop',
];
#Instagram Adapter
The Instagram adapter supports two authentication paths:
Path 1 — Facebook Page-linked account (uses graph.facebook.com):
'instagram' => [
'page_access_token' => env('INSTAGRAM_PAGE_ACCESS_TOKEN'),
'app_secret' => env('META_APP_SECRET'),
'verify_token' => env('INSTAGRAM_VERIFY_TOKEN'),
],
Path 2 — Instagram Login (uses graph.instagram.com):
'instagram' => [
'ig_access_token' => env('INSTAGRAM_ACCESS_TOKEN'),
'ig_user_id' => env('INSTAGRAM_USER_ID'),
'app_secret' => env('META_APP_SECRET'),
'verify_token' => env('INSTAGRAM_VERIFY_TOKEN'),
],
The adapter auto-detects which path to use based on which token is provided. The app_secret is your Meta app secret (required for both paths — used for x-hub-signature-256 webhook verification).
#Web Adapter
The WebAdapter provides browser-based chat UI integration. Configure it with a WebAdapterConfig class:
Basic:
use BootDesk\ChatSDK\Web\WebAdapterConfig;
use Illuminate\Support\Facades\Auth;
use Psr\Http\Message\ServerRequestInterface;
class AppWebAdapterConfig extends WebAdapterConfig
{
public function getUser(ServerRequestInterface $request): ?array
{
return Auth::check()
? ['id' => (string) Auth::id(), 'name' => Auth::user()?->name]
: null;
}
}
'web' => [
'user_name' => env('BOT_USERNAME', 'Bot'),
'config' => App\Chat\AppWebAdapterConfig::class,
],
With signature verification:
class AppWebAdapterConfig extends WebAdapterConfig
{
public function getUser(ServerRequestInterface $request): ?array
{
return Auth::check()
? ['id' => (string) Auth::id(), 'name' => Auth::user()?->name]
: null;
}
public function verifySignature(ServerRequestInterface $request): bool|string
{
$signature = $request->getHeaderLine('X-Signature');
$payload = (string) $request->getBody();
$expected = 'sha256=' . hash_hmac('sha256', $payload, config('app.webhook_secret'));
return hash_equals($expected, $signature) ? true : 'Invalid signature';
}
}
'web' => [
'user_name' => env('BOT_USERNAME', 'Bot'),
'config' => App\Chat\AppWebAdapterConfig::class,
'broadcaster' => fn () => app(\BootDesk\ChatSDK\Core\Contracts\BroadcastAdapter::class),
'async_mode' => env('CHAT_WEB_ASYNC_MODE', false),
],
The getUser() method receives the PSR-7 ServerRequestInterface and must return ['id' => string, 'name' => ?string] or null for unauthenticated.
The verifySignature() method receives the PSR-7 request and must return true for valid signatures, or an error message string for invalid. Called before user authentication.
Each adapter is auto-discovered at runtime via class_exists(). Only configured adapters are registered.
#Usage
#The Chat Facade
use Chat;
Chat::onNewMessage(function (Message $message, Thread $thread) {
$thread->post("Echo: {$message->text}");
});
Chat::onSlashCommand(function (SlashCommandEvent $event) {
$event->thread->post("You ran: {$event->command}");
});
#The chat Helper
$chat = chat(); // Returns the Chat singleton
chat()->onNewMessage(function ($message, $thread) {
// ...
});
#Webhook Route
The package ships with a pre-built WebhookController that handles PSR-7 conversion automatically. Register the route in routes/api.php:
use BootDesk\ChatSDK\Laravel\Http\Controllers\WebhookController;
Route::match(['get', 'post'], '/chats/{adapter}', [WebhookController::class, 'handle']);
It accepts both GET (for platform verification challenges like Telnyx) and POST (for actual webhooks). The {adapter} parameter matches the adapter name (slack, telegram, telnyx, etc.).
Under the hood, the controller does the PSR-7 conversion for you:
$psrRequest = $psrHttpFactory->createRequest($request);
$psrResponse = $this->chat->handleWebhook($adapter, $psrRequest);
return (new HttpFoundationFactory)->createResponse($psrResponse);
#Chat Handlers
Create handler classes that implement the ChatHandler contract. They're auto-discovered via config/chat.php:
namespace App\Chat;
use BootDesk\ChatSDK\Core\Chat;
use BootDesk\ChatSDK\Core\Message;
use BootDesk\ChatSDK\Core\Thread;
use BootDesk\ChatSDK\Laravel\Contracts\ChatHandler;
class ChatHandlers implements ChatHandler
{
public function register(Chat $chat): void
{
$chat->onNewMessage(function (Message $message, Thread $thread) {
if ($message->text === 'hello') {
$thread->post('Hi there!');
}
});
$chat->onSlashCommand(function (SlashCommandEvent $event) {
$event->thread->post("You ran: {$event->command}");
});
}
}
Then register it in config/chat.php:
'handlers' => [
\App\Chat\ChatHandlers::class,
],
You can have multiple handlers — each receives the Chat instance in register() and can set up its own listeners.
#Middleware
Register receiving and sending middleware via a ChatHandler class. Implement BootDesk\ChatSDK\Laravel\Contracts\ChatHandler and it's auto-discovered by the service provider:
namespace App\Chat;
use BootDesk\ChatSDK\Core\Chat;
use BootDesk\ChatSDK\Laravel\Contracts\ChatHandler;
class ChatMiddlewareHandler implements ChatHandler
{
public function register(Chat $chat): void
{
$chat
->addReceivingMiddleware(new Middleware\LogReceivedMessage)
->addSendingMiddleware(new Middleware\LogSentMessage);
}
}
The middleware classes implement ReceivingMiddleware or SendingMiddleware:
namespace App\Chat\Middleware;
use BootDesk\ChatSDK\Core\Contracts\Adapter;
use BootDesk\ChatSDK\Core\Contracts\ReceivingMiddleware;
use BootDesk\ChatSDK\Core\Message;
use Illuminate\Support\Facades\Log;
class LogReceivedMessage implements ReceivingMiddleware
{
public function handle(Message $message, Adapter $adapter, callable $next): ?Message
{
Log::info('chat.received', ['text' => $message->text]);
return $next($message);
}
}
namespace App\Chat\Middleware;
use BootDesk\ChatSDK\Core\Contracts\Adapter;
use BootDesk\ChatSDK\Core\Contracts\SendingMiddleware;
use BootDesk\ChatSDK\Core\PostableMessage;
use BootDesk\ChatSDK\Core\SentMessage;
class LogSentMessage implements SendingMiddleware
{
public function handle(string $threadId, PostableMessage $message, Adapter $adapter, string $operation, callable $next): ?SentMessage
{
Log::info('chat.sending', ['text' => $message->getTextContent()]);
return $next($threadId, $message, $adapter, $operation);
}
}
The ChatHandler is registered in config/chat.php:
'handlers' => [
\App\Chat\ChatMiddlewareHandler::class,
\App\Chat\ChatHandlers::class,
],
#Adapters
The service provider auto-discovers which adapters to register based on your config/chat.php. Each adapter's constructor dependencies are resolved from the container:
'github' => [
'auth_token' => env('GITHUB_TOKEN'),
'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'),
],
For multi-tenant systems, implement the AdapterResolver contract. It's checked first — if it returns null, the service provider falls back to config/chat.php. The resolver receives the adapter name and the PSR-7 request, allowing you to return different adapter instances per request:
namespace App\Chat\Helpers;
use BootDesk\ChatSDK\Core\Contracts\Adapter;
use BootDesk\ChatSDK\Core\Contracts\AdapterResolver;
use Psr\Http\Message\ServerRequestInterface;
class TenantAdapterResolver implements AdapterResolver
{
public function resolve(string $name, ?ServerRequestInterface $request = null): ?Adapter
{
// When $request is null (e.g. queued job), fall back to another strategy
$token = $request !== null
? $request->getHeaderLine('X-Tenant-Token')
: $this->resolveTenantFromQueue();
return match ($name) {
'slack' => app()->make(SlackAdapter::class, [
'botToken' => $token,
]),
default => null, // falls back to config/chat.php
};
}
private function resolveTenantFromQueue(): string
{
// e.g. read from serialized job context, database, or cache
return 'default_tenant_token';
}
}
Bind it in a service provider:
$this->app->bind(AdapterResolver::class, TenantAdapterResolver::class);
See the example app for a full implementation.
The HTTP client is auto-bound as Psr\Http\Client\ClientInterface (uses Guzzle by default). Override by rebinding in your service provider:
$this->app->bind(ClientInterface::class, function () {
return new CustomClient;
});
#State
Laravel uses CacheStateAdapter backed by your configured cache driver (Redis, database, file). This provides conversation state, deduplication, modal context, and rate limiting across multiple processes.
#File Upload Converter
For adapters without native file upload support, register a FileUploadConverter in your service provider:
use BootDesk\ChatSDK\Core\Contracts\FileUploadConverter;
$this->app->bind(FileUploadConverter::class, function () {
return new PublicFilesystemToAttachment;
});
#Messaging Window
Platforms like WhatsApp enforce a 24-hour messaging window. The Laravel package provides two middleware classes to handle this:
1. Track — record the last incoming message timestamp:
use BootDesk\ChatSDK\Laravel\Middleware\TrackMessagingWindow;
$chat->addReceivingMiddleware(new TrackMessagingWindow($state));
2. Enforce — block or convert messages when the window expires:
use BootDesk\ChatSDK\Laravel\Middleware\EnforceMessagingWindow;
$chat->addSendingMiddleware(new EnforceMessagingWindow(
state: $state,
templateFallback: fn (PostableMessage $msg) => PostableMessage::text(
'You have a new message waiting. Open the app to reply.'
),
));
When the window has expired and no templateFallback is set, the message is silently dropped. When templateFallback is provided, the original message is replaced with the fallback template.
The adapter must implement AdapterHasMessagingWindow (WhatsApp does by default with a 86400s window). See the architecture guide for the full contract.
#Broadcasting
The Laravel package includes LaravelBroadcastAdapter for real-time event broadcasting via Pusher, Redis, or Laravel's broadcast drivers.
#Configuration
Publish the broadcasting config:
php artisan vendor:publish --tag=chat-broadcasting
Config file: config/chat-broadcasting.php
return [
'enabled' => env('CHAT_BROADCASTING_ENABLED', true),
'default' => env('CHAT_BROADCASTING_DEFAULT', 'pusher'),
'channel_prefix' => env('CHAT_BROADCASTING_CHANNEL_PREFIX', 'chat'),
'thread_channel_type' => env('CHAT_BROADCASTING_THREAD_CHANNEL_TYPE', 'public'),
'user_channel_type' => env('CHAT_BROADCASTING_USER_CHANNEL_TYPE', 'private'),
];
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable broadcasting globally |
default |
'pusher' |
Broadcaster driver (pusher/redis/log/null) |
channel_prefix |
'chat' |
Prefix for all channel names |
thread_channel_type |
'public' |
Thread channel type (public/private/presence) |
user_channel_type |
'private' |
User channel type (private/presence) |
#Channel Types
Both thread channels and user channels can be configured as public, private, or presence:
- Public channels (
Channel): Open to anyone with the channel name - Private channels (
PrivateChannel): Require authentication via Laravel's channel routes - Presence channels (
PresenceChannel): Private + presence features (who's online)
Thread broadcasts (messages posted, edited, deleted, reactions): configured via thread_channel_type (default: public)
User broadcasts (DMs, typing in DMs, streaming): configured via user_channel_type (default: private)
Set in .env:
CHAT_BROADCASTING_THREAD_CHANNEL_TYPE=presence # or public/private
CHAT_BROADCASTING_USER_CHANNEL_TYPE=presence # or private
#Usage with WebAdapter
use BootDesk\ChatSDK\Web\WebAdapter;
use BootDesk\ChatSDK\Laravel\Broadcasting\LaravelBroadcastAdapter;
$broadcaster = app(LaravelBroadcastAdapter::class);
$adapter = new WebAdapter(
userName: 'Bot',
config: new App\Chat\AppWebAdapterConfig,
broadcaster: $broadcaster,
asyncMode: true, // Broadcast events immediately
);
The broadcaster is automatically connected/disconnected via Chat::initialize() and Chat::shutdown().
#Broadcast Events
| Event | Target | Channel Type |
|---|---|---|
MessagePostedEvent |
Thread | Public |
MessageEditedEvent |
Thread | Public |
MessageDeletedEvent |
Thread | Public |
ReactionAddedEvent |
Thread | Public |
ReactionRemovedEvent |
Thread | Public |
TypingStartedEvent |
User (DM) | Private |
StreamingChunkEvent |
User (DM) | Private |
DirectMessageRequestedEvent |
User | Private |
#Client-Side Example (Pusher)
const pusher = new Pusher('your-app-key', {
cluster: 'your-cluster',
});
// Thread channel
const threadChannel = pusher.subscribe('chat.web:user123:conv456');
threadChannel.bind('chat.message.posted', (data) => {
console.log('New message:', data.text);
});
// Private user channel
const userChannel = pusher.subscribe('private-chat.web:user123:conv456.user123');
userChannel.bind('chat.typing.started', (data) => {
console.log('User is typing...');
});