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 Surreal Db Message Store Laravel Package

symfony/ai-surreal-db-message-store

SurrealDB message store integration for Symfony AI Chat, letting you persist and retrieve conversation messages in SurrealDB via its HTTP integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. This package is a Symfony AI component, not a Laravel one. There is no service provider, no config file, no facade. It ships one job: an implementation of Symfony AI Chat's MessageStoreInterface that talks to SurrealDB over its HTTP /sql endpoint. The README is ~10 lines and the release notes are mostly "no significant changes," so the source in vendor/ is the documentation. Budget an hour to read it before writing a line of app code.

Decide your path before installing anything:

Path What you do When it's right
A. Use the package Pull in symfony/ai-chat + this store, bind it in a Laravel service provider You actually want Symfony AI's agent loop, or the store's schema fits your needs
B. Reimplement Write ~150 lines of Laravel that POST to SurrealDB's /sql endpoint You only want message persistence and don't want Symfony types in your app (most Laravel teams)

Both are legitimate. Path B is often faster than fighting the dependency graph, and SurrealDB's HTTP API is trivial to call from Laravel's Http facade.

Minimal steps (Path A):

composer require symfony/ai-surreal-db-message-store symfony/ai-chat symfony/http-client
  1. Read the store class — vendor/symfony/ai-surreal-db-message-store/src/. Find the constructor: you need to know exactly which arguments it takes (HTTP client, endpoint URL, namespace, database, credentials). Note the table/record names it uses so you can inspect data later.
  2. Read the contract — vendor/symfony/ai-chat/src/**/MessageStoreInterface. Every method there is a method you must either delegate or fake in tests.
  3. Stand up SurrealDB and prove connectivity outside PHP first:
docker run --rm -p 8000:8000 surrealdb/surrealdb:latest \
  start --user root --pass root memory

curl -s -X POST http://localhost:8000/sql \
  -H 'Accept: application/json' \
  -H 'Surreal-NS: app' -H 'Surreal-DB: app' \
  -u root:root \
  --data 'INFO FOR DB;'
  1. Bind it in AppServiceProvider::register():
$this->app->singleton(MessageStoreInterface::class, function ($app) {
    return new SurrealDbMessageStore(
        // exact signature: check vendor source, do not guess
        endpoint: config('ai.surreal.endpoint'),
        namespace: config('ai.surreal.namespace'),
        database: config('ai.surreal.database'),
        username: config('ai.surreal.username'),
        password: config('ai.surreal.password'),
    );
});

First real use case: persist a multi-turn agent conversation so a follow-up request in a new HTTP request sees prior turns. Load history by conversation id → pass to the agent → append the new user message → run → append the assistant reply. That single loop is 90% of what anyone uses a message store for.

Implementation Patterns

Keep Symfony types behind your own interface. This is the single most important pattern with a 0.x, 2-star, zero-dependent package. Define App\Contracts\ChatHistory with load(string $conversationId): Collection and append(string $conversationId, Message $m): void. Implement it once with the Symfony store. Your controllers, jobs, and tests never touch Symfony\AI\.... Swapping to Path B later becomes a one-class change.

Config file with env fallbacks, and validate at boot:

// config/ai.php
'surreal' => [
    'endpoint'  => env('SURREAL_ENDPOINT', 'http://127.0.0.1:8000'),
    'namespace' => env('SURREAL_NS', 'app'),
    'database'  => env('SURREAL_DB', 'app'),
    'username'  => env('SURREAL_USER'),
    'password'  => env('SURREAL_PASS'),
],

Fail loudly in the provider if these are empty in production — a misconfigured endpoint otherwise surfaces as a confusing 401 deep inside a chat request.

Conversation ids are yours, not the store's. Generate with Str::uuid() and store the mapping in your own table alongside user_id and tenant_id. The store persists messages, not conversations, and SurrealDB record ids (messages:⟨...⟩) are not something you want leaking into URLs. Note v0.11.0's changelog: "Fix SurrealDB message store corrupting the message UUID" — round-tripping identifiers has already bitten this package once.

Multi-tenant strategy. One SurrealDB namespace/database per tenant means creating databases at signup and managing connection pools — expensive. Prefer a single database with a tenant_id field on each message, and filter in your queries. If you need hard isolation, use one database per environment and a tenant field inside it, not one database per customer.

Write path. Each message is an HTTP round trip (~50–100ms locally, more over a network). Two options:

  • Synchronous — simplest, ordering is guaranteed. Fine for interactive chat where the user is already waiting on the LLM anyway.
  • Queued — dispatch an AppendMessage job on a queue keyed by conversation id (e.g. Queue::route or a per-conversation queue name) so messages for one conversation process in order. A single shared queue will reorder turns under load and produce incoherent history.

Testing seam. Don't hit SurrealDB in tests. Bind a InMemoryChatHistory implementation of your interface in TestCase::setUp(). If you do want integration coverage, gate it behind @group surreal and skip when SURREAL_ENDPOINT is unset. Mocking Symfony's HTTP client is not worth the effort.

Side-channel queries. SurrealDB is genuinely nice for analytics on top of chat data — "most active conversations this week," embedding lookups, sentiment fields. But the package owns its schema, so don't assume you can add arbitrary fields through it. If you need richer records, write them yourself with a second, small SurrealDB client and a shared record convention, and treat the store as read-only history.

Gotchas and Tips

HTTP 200 does not mean success. SurrealDB's /sql endpoint returns 200 with an array where each element has status: "OK" or status: "ERR" plus detail. If you write your own client (Path B) or wrap the store, check per-statement status — never just $response->successful(). This is the #1 source of "it silently saved nothing" bugs.

SurrealQL is not SQL. Statements terminate with ;. SELECT * FROM messages WHERE conversation = $id; uses variables passed in a separate vars body key. USE NS app DB app; can replace the Surreal-NS/Surreal-DB headers, but mixing both is a good way to query the wrong database. Pick one and be consistent.

No migrations. Laravel's php artisan migrate is irrelevant here. Keep an idempotent database/surreal/bootstrap.surql with DEFINE TABLE IF NOT EXISTS, DEFINE INDEX IF NOT EXISTS idx_conv_time ON messages FIELDS conversation_id, created_at, and run it on deploy. Without an index on conversation + timestamp, history loads get slow fast.

Version pinning. Releases run 0.4 → 0.14 in the notes, most saying "no significant changes," one shipping a real bugfix. That's a pre-1.0 API with an unstable surface and almost no users. Pin exactly ("0.14.0", not ^0.14) and read the diff of every bump. Treat upgrades as you would a minor framework upgrade.

Exception translation. Symfony's HTTP client throws Symfony\Contracts\HttpClient\Exception\ExceptionInterface subtypes, which do not map onto Illuminate\Http\Client\*. Wrap every call to the store in a try/catch that converts to a domain exception, or your Handler will render 500s for what should be a 503 with a retry.

Timeouts and retries. Set a connect timeout (2s) and request timeout (5–10s). Retry idempotent reads only; retrying an append can duplicate messages, and SurrealDB has no unique constraint unless you define one yourself.

Debugging recipe. Log the raw query and the raw response body at debug level. Then reproduce in the CLI against the same namespace/database:

surreal sql --endpoint http://127.0.0.1:8000 --ns app --db app -u root -p root
> INFO FOR DB;
> SELECT * FROM messages ORDER BY created_at DESC LIMIT 5;

If the data is there but your app doesn't see it, you have a namespace/database header mismatch, not a code bug.

Auth. Basic auth (-u user:pass) and token auth (Authorization: Bearer <jwt>) both work; tokens expire and scoped tokens restrict what DEFINE can do. If you see 401/403 only in production, it's almost always a token secret or an expired JWT, not the store.

Your real extension point is the binding. There's no event system to hook, no middleware pipeline, no config to tweak. The only customization seam is $this->app->bind(...) — which is exactly why the domain interface in Implementation Patterns matters. Everything else in this package is a black box you should be prepared to replace in an afternoon.

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