kauffinger/livewire-chat-kit
A Livewire chat kit for Laravel with ready-made chat UI components: real-time messaging, conversation lists, and message threads. Drop-in Blade components let you add a complete chat interface to any Livewire app with minimal setup.
Install and publish everything first, before you write a line of UI.
composer require kauffinger/livewire-chat-kit
php artisan vendor:publish --provider="Kauffinger\LivewireChatKit\ChatKitServiceProvider"
php artisan migrate
Then set your provider key and run php artisan config:clear. Prism reads credentials from config/prism.php — confirm the published config points at the provider you actually have a key for rather than the default.
Where to look first, in this order:
config/livewire-chat-kit.php (or similarly named) — model bindings, provider/model names, streaming on/off, and route/prefix settings are almost always here.conversations, messages, or prefixed with the package vendor name) before you design anything around them. If they collide with a messages table you already have, rename now.resources/views/vendor/..., the views live in vendor/kauffinger/livewire-chat-kit/resources/views and you should publish them (--tag=views) rather than fight the vendor copy.Chat/ChatPanel component that owns send(), streaming, and persistence.First use case: a single-user assistant panel behind auth on an internal page. Don't start with multi-tenant threads; start by proving the stream works end-to-end.
// routes/web.php
Route::middleware('auth')->get('/assistant', AssistantPage::class)->name('assistant');
{{-- resources/views/pages/assistant.blade.php --}}
<x-layout>
<livewire:chat-kit::chat :conversation="$conversation" />
</x-layout>
Create the conversation in your page component's mount() so the chat is always scoped to the current user — don't let the kit create unattached conversations.
Verify the layout has @fluxAppearance / @fluxScripts (Flux) and that the page is Tailwind-compiled (see Gotchas — this bites almost everyone on first install).
Wrap, don't route directly to the package component. Create your own thin Livewire component or full-page component that composes the kit's component. That gives you one place to inject the system prompt, enforce ownership, and swap the model:
final class AssistantPage extends Component
{
public Conversation $conversation;
public function mount(): void
{
$this->conversation = Conversation::query()->firstOrCreate(
['user_id' => auth()->id(), 'name' => 'default'],
);
abort_unless($this->conversation->user_id === auth()->id(), 403);
}
}
Scope every query by owner. Kits at this maturity level typically resolve a conversation by ID from the request. Add an explicit abort_unless in your wrapper and, if the kit exposes a resolveConversation-style hook or a conversation public property, bind only pre-authorized instances. Treat the component like any other endpoint that accepts an ID.
Prompt/persona belongs in your app, not the vendor config. If the kit reads a system prompt from config, prefer overriding it per-conversation in your wrapper and passing it down. Config-level prompts become a merge conflict on every upgrade.
Streaming UI. The kit streams into a named target (typically $this->stream(to: 'response', content: $stream) inside send()). Keep your Blade side simple: one element with the matching wire:stream target id, a wire:loading indicator, and wire:loading.remove on the send button. Don't add wire:poll — it fights the open stream.
Persist the assistant message after the stream completes, not inside the loop. If you're extending the kit, write the final message row in a finally-ish position so a mid-stream provider error doesn't leave a half-message. Store token counts while you're there; you'll want them.
Prism tools / function calling are where the kit earns its keep. Register tools in your own service and hand them to the kit's Prism call site (or override the generation method). Keep tool classes in app/Ai/Tools/ and authorize inside each tool — the model will happily try to query another user's records.
History truncation is your job. Assume the kit sends the full message history. Add a windowing step (last N turns or a token budget) in your wrapper before dispatch, or a long conversation will quietly blow your context limit and your bill.
Testing: assert on persisted state, not on the stream. Use Prism::fake([...]), call ->call('send') (or the kit's method), then assert the assistant Message row exists and assertSee the rendered text. Streaming assertions are flaky; persistence assertions aren't.
vendor/, none of its utility classes are generated until you add @source "../../vendor/kauffinger/livewire-chat-kit/resources/views"; to your CSS entry. The symptom is a fully unstyled chat with correct markup. This is the #1 first-install failure.flux:editor, flux:chart, flux:calendar, date pickers, command palette) and replace them with Free equivalents before wiring anything else.proxy_buffering off; and X-Accel-Buffering: no in nginx, and ensure no middleware buffers the response. Also raise max_execution_time — a stream holds the worker for the whole generation.composer.json and read its changelog before composer update. Breaking changes in Prism surface as silent empty responses, not exceptions.^ constraint.loadMigrationsFrom, vendor:publish may produce nothing and you'll assume it silently failed. Run php artisan migrate:status to confirm the tables actually appeared.RateLimiter::for('chat', ...) and call it at the top of send(). The kit almost certainly doesn't do this, and one stuck browser tab retrying is a real invoice.vendor/ and avoid patching the kit's send() if you can wrap it — those are the two things that make the next update painful.How can I help you explore Laravel packages today?