symfony/ai-cloudflare-message-store
Cloudflare KV-backed message store for Symfony AI Chat. Persist and retrieve chat messages in Cloudflare Workers KV, with support for KV namespace operations like bulk get and bulk update, enabling scalable storage for AI conversation history.
Install the Package (via Composer):
composer require symfony/ai-cloudflare-message-store
Note: Requires Symfony AI Chat (symfony/ai-chat). If not using Symfony, see Implementation Patterns for Laravel-native workarounds.
Configure Cloudflare KV:
.env:
CLOUDFLARE_ACCOUNT_ID=your_account_id
CLOUDFLARE_KV_NAMESPACE_ID=your_namespace_id
CLOUDFLARE_API_TOKEN=your_api_token
Bind the Store in Laravel:
// config/services.php
'cloudflare_kv' => [
'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
'namespace_id' => env('CLOUDFLARE_KV_NAMESPACE_ID'),
'api_token' => env('CLOUDFLARE_API_TOKEN'),
],
First Use Case: Persist a Chat Message
use Symfony\Component\Ai\Chat\MessageStoreInterface;
use Symfony\Component\Ai\Chat\Message;
// Bind the store to Laravel's container (if using Symfony AI Chat)
$container->set(MessageStoreInterface::class, fn() => new \Symfony\Ai\CloudflareMessageStore\CloudflareKvMessageStore(
config('services.cloudflare_kv')
));
// Example usage
$store = $container->get(MessageStoreInterface::class);
$message = new Message('user', 'Hello, AI!');
$store->save($message, 'chat_123'); // 'chat_123' is a conversation ID
Conversation Initialization:
// Start a new chat (generate a UUID for the conversation ID)
$conversationId = Str::uuid()->toString();
Saving Messages:
$store = app(MessageStoreInterface::class);
$store->save(new Message('user', 'What is Laravel?'), $conversationId);
$store->save(new Message('ai', 'Laravel is a PHP framework...'), $conversationId);
Loading a Chat History:
$messages = $store->findAll($conversationId);
// $messages = [Message, Message, ...]
Bulk Operations (Edge-Optimized):
// Fetch all messages for multiple conversations at once
$conversationIds = ['chat_123', 'chat_456'];
$messages = $store->bulkFindAll($conversationIds);
// Update multiple messages in a single request
$updates = [
'chat_123' => [new Message('ai', 'Updated response...')],
'chat_456' => [new Message('user', 'New question?')],
];
$store->bulkSave($updates);
Hybrid Storage for Metadata:
Use KV for message content and a database (e.g., MySQL) for metadata (e.g., conversations table with id, user_id, created_at).
// Example: Store conversation metadata in DB, messages in KV
$conversation = Conversation::create(['user_id' => auth()->id()]);
$store->save(new Message('user', 'Hello'), $conversation->id);
Fallback for Offline Use: Implement a local cache (e.g., Redis) as a fallback:
use Illuminate\Support\Facades\Cache;
$messages = $store->findAll($conversationId) ?? Cache::get("offline_chat_{$conversationId}");
Event-Driven Workflows: Dispatch Laravel events after saving messages to trigger side effects (e.g., analytics, notifications):
$store->save($message, $conversationId);
event(new ChatMessageSaved($conversationId, $message));
Serialization for Complex Objects: Override serialization for Eloquent models or custom objects:
use Symfony\Component\Serializer\SerializerInterface;
$serializer = app(SerializerInterface::class);
$messageData = $serializer->serialize($message, 'json');
$store->saveFromString($messageData, $conversationId);
Symfony Dependency:
MessageStoreInterface with Laravel’s Message facade or Illuminate\Queue.// In a Laravel service
class CloudflareKvMessageStore implements \Illuminate\Contracts\Queue\ShouldBeQueued
{
public function store($message, $conversationId) {
// Use Cloudflare KV client directly
}
}
Key Collisions:
message_{id}) can lead to collisions. Use a structured format:
// Good: user:{id}:conversation:{id}:message:{id}
$key = "user:{$userId}:conversation:{$conversationId}:message:{$messageId}";
Bulk Operation Limits:
bulk_get/bulk_update has a 100-key limit per request. For large datasets:
$batchSize = 50;
$batches = array_chunk($conversationIds, $batchSize);
foreach ($batches as $batch) {
$store->bulkFindAll($batch);
}
Cold Starts:
Cost Monitoring:
Enable Cloudflare KV Logging:
Add this to your .env to debug API calls:
CLOUDFLARE_KV_DEBUG=true
Logs will appear in Laravel’s storage/logs/laravel.log.
Handle API Errors Gracefully:
Cloudflare KV may return 429 Too Many Requests or 401 Unauthorized. Implement retries:
use Symfony\Component\Ai\Exception\MessageStoreException;
try {
$store->save($message, $conversationId);
} catch (MessageStoreException $e) {
if ($e->getCode() === 429) {
sleep(1); // Retry after delay
retry();
}
throw $e;
}
Test Locally with Mock KV: Use the Cloudflare KV Emulator for local testing:
docker run -p 25000:25000 cloudflare/cloudflare-kv-emulator
Update .env to point to the emulator:
CLOUDFLARE_KV_URL=http://localhost:25000
Custom Key Generator: Override the default key format:
$store->setKeyGenerator(fn($conversationId, $messageId) => "custom_prefix:{$conversationId}_{$messageId}");
Add TTL for Ephemeral Messages: Use KV’s expiration feature:
$store->save($message, $conversationId, 3600); // Expires in 1 hour
Integrate with Laravel Queues: For async operations, wrap the store in a queue job:
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class SaveChatMessage implements Queueable, SerializesModels
{
public function handle() {
$store = app(MessageStoreInterface::class);
$store->save($this->message, $this->conversationId);
}
}
Add Analytics Middleware: Track message operations:
How can I help you explore Laravel packages today?