Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ai Cloudflare Message Store Laravel Package

symfony/ai-cloudflare-message-store

Cloudflare KV message store integration for Symfony AI Chat, backed by Cloudflare's KV API with bulk read and write support.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the Symfony AI Chat / message-store documentation before this package's README — the README here is essentially a pointer to the main symfony/ai monorepo, so the real integration contract (the message store interface, conversation/session semantics, how the store is injected into the agent) lives there. Treat this repo as an adapter, not a framework.

Minimal path:

  1. Confirm you actually have Symfony AI Chat in the app. This package is a bridge for symfony/ai-chat's message store abstraction. Laravel does not ship that abstraction, so without symfony/ai-chat there is nothing to bind it to. If your AI layer is openai-php/laravel, prism, or a custom client, you are not adopting this package — you are copying its idea (KV as message backend) and reimplementing it against your own contract. Be honest about that fork upfront.
  2. If you do adopt Symfony AI Chat, install both and let Symfony AI's own factory/configuration pick the store up:
    composer require symfony/ai-chat symfony/ai-cloudflare-message-store
    
    Pin exactly — this package is 0.x (v0.14.0 at time of writing) with several releases marked "no significant changes", so minor bumps carry API churn risk with no semver guarantee.
  3. Provision Cloudflare KV: create a namespace (wrangler kv namespace create CHAT_MESSAGES or the dashboard), then an API token scoped to KV Storage: Edit for that account. Put CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_KV_NAMESPACE_ID, and CLOUDFLARE_API_TOKEN in .env, and mirror them into config/services.php so config:cache works.
  4. First use case to prove out: persist a single conversation's message history keyed by conversation ID, then reload it in a second request and hand it back to the agent as context. That round-trip is the whole value proposition — do it before wiring anything else.

Where to look first in the source: the store class itself (how it keys messages, whether it uses bulk get/update, what it serializes) and the Symfony AI factory registration. Those two files tell you everything the README doesn't.

Implementation Patterns

Bind the store through the container, not by instantiating it. Even in Laravel, resolve the store as a singleton from a service provider so tests can swap it. If you're bridging into Laravel's own abstractions, define a narrow contract of your own:

// app/Contracts/ChatMessageStore.php
interface ChatMessageStore
{
    public function append(string $conversationId, array $messages): void;
    public function load(string $conversationId): array;
    public function forget(string $conversationId): void;
}

Then have one adapter wrap the Symfony store. Everything else in the app depends on your interface, which means the Cloudflare dependency is a single swappable class rather than a leak across the codebase.

Key design matters more than the store. Cloudflare KV is a flat keyspace with no query language, so decide the key scheme before you write a line. A workable scheme:

conv:{conversationId}:meta          -> JSON: user_id, agent, created_at, last_message_at
conv:{conversationId}:msgs:{seq}    -> JSON: single message or small batch
user:{userId}:conversations         -> JSON: index of conversation IDs

The index key is what saves you — without it, "list this user's conversations" is impossible in KV. Keep the index append-only and rewrite it on new conversations; it is small and read-hot, which suits KV.

Use bulk operations deliberately. The package exposes Cloudflare's bulk endpoints specifically to avoid N round trips when loading a long transcript. Load a conversation by reading the index/metadata key, then bulk-getting the message keys in one call. Batch size is capped by Cloudflare (roughly 100 keys per bulk get); chunk your reads above that. For writes, append the new turn(s) via bulk update rather than one call per message.

Hybrid storage is the pragmatic default. Store the searchable, joinable stuff — conversation metadata, user mapping, token counts, cost attribution — in MySQL/Postgres, and keep only message payloads in KV. You get SQL for dashboards and analytics, KV for the hot read path, and you sidestep KV's lack of secondary indexes entirely. This is the pattern most Laravel teams will actually end up at.

Queue the writes when latency matters. The agent response shouldn't wait on a KV round trip. Dispatch a job after the response is streamed:

dispatch(new PersistChatTurn($conversationId, $assistantMessage))->afterResponse();

Reads stay synchronous (that's where KV's edge latency pays off); writes go async. Add a retry/backoff policy on the job, since transient Cloudflare 5xx are a real category.

Set TTLs, don't rely on cleanup jobs. Cloudflare KV supports per-key expiration. Give message keys and conversation metadata a TTL aligned with your retention policy (e.g. 90 days) — it costs nothing and removes an entire class of "why is this namespace 40GB" problems. Just make sure the index and the message keys agree on expiry, or you get dangling IDs.

Testing: never hit Cloudflare from the suite. Implement your contract with an in-memory or database-backed fake and bind it in TestCase::setUp():

$this->app->instance(ChatMessageStore::class, new InMemoryChatMessageStore());

Reserve one integration test, tagged and excluded from CI by default, that exercises the real adapter against a scratch namespace.

Gotchas and Tips

Eventual consistency will bite you in tests and demos. A write to KV is not immediately visible in all locations. If your flow is "save assistant reply → reload page → render history", a fast reload from a different edge can miss the message. Mitigate by keeping the last N turns in the session or a short-lived Redis cache, and by not asserting read-after-write in acceptance tests.

No queries, no transactions, no ordering guarantees. Anything that smells like "find messages where…" needs a secondary store. Plan the metadata table early; retrofitting an index over an existing namespace means a full key scan, which is painful and slow.

The 64KB per-value limit is a hard edge. Long tool outputs, base64 images, and multi-megabyte RAG contexts will blow through it. Truncate tool results before persisting, store attachments in R2/S3 and keep only the reference in the message, and consider one-message-per-key rather than per-conversation blobs so a single giant turn doesn't poison the whole history.

Credential handling. CLOUDFLARE_API_TOKEN in .env is fine for dev but is a real secret in production — pull it from your platform's secret manager (Forge/Vapor/ECS/SSM) and make sure it never lands in config:cache-ed files or logs. Scope the token to KV on one account, and use a separate namespace per environment so a staging bug can't touch production history.

Config caching and env access. If the Symfony store reads $_ENV/getenv() directly, php artisan config:cache environments can surprise you. Resolve credentials in a Laravel config file and pass them explicitly into the store's constructor when you register it in the provider.

Serialization is your problem. Chat messages are objects, not scalars; whatever the store's default serializer does may not round-trip your message types (tool calls, function results, custom metadata). Lock this down with a test that asserts a full message — role, content, tool call payload — survives store/load intact. This is the single most likely place for silent data loss.

Cost shape favors reads but bulk calls are not free. KV pricing is per-operation, and a naive load of a 200-message conversation as 200 individual gets is 200 operations each time the agent runs. Bulk get and sensible conversation-windowing (last 10–20 turns in context, older turns fetched only on demand) keep this linear-but-small. Log operation counts per request in staging for a week and look at the distribution.

Watch the version pin and the release notes literally. Multiple 0.x releases describe themselves as "no significant changes", which tells you the project is still stabilizing under the Symfony monorepo's release train. Pin ~0.14.0, read the changelog before bumping, and keep the adapter thin so a breaking rename costs you one file.

Debugging visibility is thin. There's no Laravel package here — no facade, no config file, no log channel. Wrap the store in a decorator that logs key, operation type, byte size, latency, and outcome; that decorator is also where you put circuit-breaking so a Cloudflare outage degrades to "chat has no memory" instead of "chat 500s". Cloudflare's own dashboard metrics are the other half of the picture — check them against your logged operation counts monthly.

Escape hatches to plan for. Because your app depends on your ChatMessageStore contract rather than this package, migrating to Redis, DynamoDB, or a plain messages table is a one-class change. Keep the serialization format documented (JSON, versioned by a schema field on each value) so a bulk export/import is a weekend job instead of a rewrite.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin