# OmniRoute's Persistent Conversational Memory System Architecture Explained

> Explore OmniRoute's dual-store memory architecture, leveraging SQLite for persistence and Qdrant for vector search with FTS5 for rapid full-text retrieval. Understand its unique design.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: architecture
- Published: 2026-08-14

---

**OmniRoute implements a dual-store memory architecture combining SQLite for canonical persistence and Qdrant for vector search, with FTS5 enabling fast full-text retrieval.**

The **persistent conversational memory system** in OmniRoute (diegosouzapw/OmniRoute) gives AI assistants long-term recall across sessions. Every user message and assistant response is durably stored, searchable by keyword, and retrievable by semantic similarity. This article breaks down the five-layer architecture that makes this possible.

---

## Schema and Validation Layer

OmniRoute enforces data integrity through a **Zod-based schema** defined in [`src/shared/schemas/memory.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/schemas/memory.ts). The `MemoryEntry` type validates every memory record before it reaches the database.

A memory entry includes:

- `id` – Unique identifier (UUID)
- `conversationId` – Groups messages into conversation threads
- `role` – `"user"` or `"assistant"`
- `content` – The message text
- `createdAt` / `updatedAt` – Timestamps for TTL and ordering
- `embeddingId` – Optional foreign key linking to Qdrant vector
- `metadata` – Extensible JSON field for feature flags

This schema acts as the **contract** between the API layer and storage backends. Invalid payloads are rejected at the boundary before any database write occurs.

---

## SQLite: The Source of Truth

The canonical store lives in a local SQLite database (`~/.omniroute/memory.db`). In [`src/lib/db/memoryVec.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/memoryVec.ts), the `getDbInstance()` helper manages connection pooling and migrations.

Two storage mechanisms coexist in SQLite:

1. **Primary `memory` table** – Stores the full message record
2. **FTS5 virtual table (`memory_fts`)** – Enables full-text search via SQLite's built-in extension

When a row inserts into `memory`, a database trigger automatically populates `memory_fts`. This **synchronous indexing** guarantees keyword search results remain consistent with the primary data.

The FTS5 implementation supports:

- Prefix matching (`weather*`)
- Phrase queries (`"machine learning"`)
- `NEAR` operators for proximity search

Because SQLite is embedded, the memory survives process restarts, container replacements, and network partitions. A single file encapsulates the entire conversation history.

---

## Qdrant: Vector Embeddings for Semantic Search

For **semantic similarity retrieval**, OmniRoute delegates to Qdrant, a self-hosted vector database. The same [`memoryVec.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/memoryVec.ts) module exports a client wrapper that:

- Upserts embeddings with `embeddingId` as the point ID
- Executes similarity searches with `client.search(collection, ...)`
- Handles batch operations for conversation-wide deletion

The embedding flow works as follows:

1. User requests vector search (or sets `embed: true` on insert)
2. Text is sent to the configured **embedding provider** (OpenAI, Ollama, etc.)
3. Resulting vector is normalized and stored in Qdrant
4. `embeddingId` in SQLite references the Qdrant point

Qdrant is **not** the source of truth. If vectors are lost, a background job re-embeds messages from SQLite. This **rebuildability** decouples durability from vector operations.

---

## REST API Endpoints

The memory system exposes a unified interface through [`src/app/api/v1/memory/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/memory/route.ts). This Next.js API route handles:

| Method | Operation |
|--------|-----------|
| `POST` | Create a new memory entry |
| `GET` | Retrieve entries (with optional `?text=` or `?vector=` query) |
| `DELETE` | Remove entries by ID or conversation ID |

Endpoints translate HTTP parameters into validated `MemoryEntry` objects, then delegate to the service layer. Response payloads include both the SQLite record and, for vector queries, the Qdrant similarity score.

---

## Service Layer: Unified Retrieval

The high-level API lives in [`src/lib/memory/index.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/memory/index.ts). These helpers orchestrate cross-store operations so chat handlers need not understand the dual-backend architecture.

Key exports include:

- **`addMemoryEntry(entry)`** – Persists to SQLite, optionally embeds to Qdrant
- **`searchMemoryByText({ conversationId, query, limit })`** – Queries FTS5, returns ranked results
- **`searchMemoryByVector({ conversationId, query, topK })`** – Generates embedding, searches Qdrant, merges with SQLite metadata
- **`deleteConversationMemory(conversationId)`** – Cascading delete across both stores

The service layer also enforces **PII handling** via feature flags:

- `PII_REDACTION_ENABLED` – Masks sensitive data before storage
- `PII_RESPONSE_SANITIZATION` – Filters retrieved memories in responses

Both default to `false`, requiring explicit opt-in.

---

## Practical Usage Examples

### Store a new message

```typescript
import { addMemoryEntry } from '@/lib/memory';

await addMemoryEntry({
  conversationId: 'conv-123',
  role: 'user',
  content: 'What is the weather in Paris?',
  embed: true  // Triggers vector embedding
});

```

### Search by keyword

```typescript
import { searchMemoryByText } from '@/lib/memory';

const hits = await searchMemoryByText({
  conversationId: 'conv-123',
  query: 'weather',
  limit: 5,
});
// Returns: Array of MemoryEntry with FTS5 rank

```

### Semantic similarity search

```typescript
import { searchMemoryByVector } from '@/lib/memory';

const similar = await searchMemoryByVector({
  conversationId: 'conv-123',
  query: 'forecast for tomorrow',  // Embedded at query time
  topK: 3,
});
// Returns: MemoryEntry objects with similarity scores

```

### Delete conversation history

```typescript
import { deleteConversationMemory } from '@/lib/memory';

await deleteConversationMemory('conv-123');
// Removes from SQLite, Qdrant, and FTS5 in one atomic operation

```

---

## Key Design Decisions

- **SQLite as source of truth** – Guarantees durability regardless of vector store health
- **FTS5 for text, Qdrant for vectors** – Optimizes each workload for the right engine
- **Optional embeddings** – Reduces latency and cost for scenarios not needing semantic search
- **Foreign key linkage** – SQLite `embeddingId` enables cross-store consistency checks
- **Feature-flag safety** – PII controls are explicit, not default-enabled

---

## Summary

- OmniRoute's **persistent conversational memory** combines **SQLite** (canonical storage + FTS5 full-text search) with **Qdrant** (vector embeddings for semantic retrieval)
- The **Zod schema** in [`src/shared/schemas/memory.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/schemas/memory.ts) validates all entries at the API boundary
- **[`src/lib/db/memoryVec.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/memoryVec.ts)** manages both SQLite operations and Qdrant client interactions
- **[`src/app/api/v1/memory/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/memory/route.ts)** exposes REST endpoints memory CRUD and search
- **[`src/lib/memory/index.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/memory/index.ts)** provides unified helpers that orchestrate cross-store operations
- Embeddings are **optional and rebuildable**, with SQLite serving as the durable source of truth

---

## Frequently Asked Questions

### How does OmniRoute handle memory during container restarts?

All messages persist to a local SQLite file at `~/.omniroute/memory.db`. Since SQLite is embedded and file-based, the database survives process restarts, container recreation, and host migrations without additional configuration. The Qdrant vector store can be entirely rebuilt from SQLite if needed.

### What is the difference between `searchMemoryByText` and `searchMemoryByVector`?

**`searchMemoryByText`** uses SQLite's built-in FTS5 virtual table for keyword-based retrieval—fast, exact, and substring matching. **`searchMemoryByVector`** generates an embedding from the query text, searches Qdrant for nearest neighbors, and returns semantically similar messages even without keyword overlap. The service layer can merge both result sets for hybrid retrieval.

### Can I disable vector embeddings to reduce latency?

Yes. The `embed` parameter in `addMemoryEntry` defaults to false. When omitted, only SQLite storage occurs. This is useful for high-throughput scenarios where semantic search isn't required, cutting embedding API costs and insertion latency.

### Where are the PII controls implemented?

PII redaction and sanitization are gated by `PII_REDACTION_ENABLED` and `PII_RESPONSE_SANITIZATION` flags in [`src/lib/memory/index.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/memory/index.ts). When enabled, sensitive data is masked before SQLite insertion and/or filtered from retrieval results. Both flags default to false, requiring explicit environment configuration to activate.