How the OpenWork Memory Bank Works for Persistent Storage: Architecture and API Guide

The OpenWork Memory Bank is a durable, user-scoped persistence layer built on MySQL full-text search that stores verified text memories with optional citation contexts, exposing four REST endpoints auto-generated as MCP capabilities for save, list, search, and delete operations.

The OpenWork Memory Bank, implemented in the different-ai/openwork repository, provides a persistent storage solution designed to retain user-verified knowledge beyond ephemeral chat sessions. Unlike simple conversation logs, this system allows users to store arbitrary text memories with structured metadata and retrieve them later via natural-language search queries. The architecture enforces strict owner-scoping at the database level, ensuring that users can only access their own data while leveraging MySQL's native full-text indexing for efficient retrieval.

Core Architecture of the OpenWork Memory Bank

The architecture separates concerns across four distinct layers: the REST API surface, authorization controls, MySQL storage engine with full-text search, and client-side management interface.

API Surface and MCP Integration

The system exposes four primary REST endpoints under the /v1/memory path. According to the OpenAPI generation logic in ee/apps/den-api/src/openapi.ts, these routes are automatically converted into MCP (Model Context Protocol) capabilities with the following operation IDs:

  • postMemory – Corresponds to POST /v1/memory for creating new memories
  • getMemory – Corresponds to GET /v1/memory for listing all user memories
  • getMemorySearch – Corresponds to GET /v1/memory/search for natural-language retrieval
  • deleteMemoryById – Corresponds to DELETE /v1/memory/:id for removing specific memories

These endpoints are registered under the "Memory" tag in ee/apps/den-api/src/mcp/policy.ts, making them discoverable to MCP clients and AI agents.

Authorization and User Scoping

Every request undergoes strict owner scoping enforced at the database query level. The backend automatically applies WHERE user_id = principal.userId and forces scope = 'user' on all operations. Attempting to access a memory ID not owned by the authenticated user returns a 404 Not Found error, preventing information leakage through ID enumeration. This security model is documented in the architecture overview at docs/memory-bank-architecture.md.

The persistence layer relies on two MySQL tables defined in ee/packages/den-db/src/schema/memory.ts:

  • memory – Stores the primary content, user IDs, organization IDs, scope, source, and tags
  • memory_context – Stores optional citation snippets and metadata associated with a memory

Search functionality utilizes MySQL's native FULLTEXT index named MEMORY_CONTENT_FULLTEXT_INDEX, created idempotently via the ensureMemoryFulltextIndex bootstrap function. Queries execute using MATCH … AGAINST … IN NATURAL LANGUAGE MODE combined with the mandatory user filter, returning relevance-ranked results.

Database Schema Definition

The TypeScript schema definitions in ee/packages/den-db/src/schema/memory.ts implement the dual-table structure with type-safe identifiers:

export const MemoryTable = mysqlTable(
  "memory",
  {
    id: denTypeIdColumn("memory", "id").notNull().primaryKey(),
    user_id: denTypeIdColumn("user", "user_id").notNull(),
    org_id: denTypeIdColumn("org", "org_id").notNull(),
    scope: mysqlEnum("scope", ["user","org"]).notNull().default("user"),
    content: text("content").notNull(),
    source: varchar("source", { length: 64 }).notNull(),
    tags: json("tags"),
    created_at: timestamp("created_at"),
    updated_at: timestamps.updated_at,
  },
  (t) => [index("memory_user_id").on(t.user_id)],
);

The memory_context table maintains referential integrity through indexed foreign key relationships:

export const MemoryContextTable = mysqlTable(
  "memory_context",
  {
    id: denTypeIdColumn("memctx", "id").notNull().primaryKey(),
    memory_id: denTypeIdColumn("memory", "memory_id").notNull(),
    citation: json("citation"),
    snippet: text("snippet").notNull(),
    origin: mysqlEnum("origin", ["active_conversation","searched_conversation"]),
    created_at: timestamp("created_at"),
  },
  (t) => [index("memory_context_memory_id").on(t.memory_id)],
);

Production database migrations reside in ee/packages/den-db/drizzle/0028_memory_bank.sql, which creates both tables and the full-text index.

REST API and MCP Endpoint Contracts

The OpenWork Memory Bank implements four distinct operations with specific request/response contracts:

POST /v1/memory

  • Creates a new memory entry and optional context rows in a single transaction
  • Request body: { content: string, tags?: string[], contexts?: Array<{ snippet: string, conversation_id?: string, message_id?: string, origin?: string }> }
  • Response: 201 Created with { id: string }
  • Server overrides any provided scope to 'user' and sets source to 'chat'

GET /v1/memory

  • Lists all memories owned by the authenticated user
  • Response: 200 OK with array of memory objects

GET /v1/memory/search

  • Performs natural-language full-text search using the q parameter
  • Query parameters: ?q=search_query&limit=number
  • Response: 200 OK with { results: Array<{ id, content, tags, score, contexts? }> }
  • Returns empty array rather than 404 when no matches exist

DELETE /v1/memory/:id

  • Removes a specific memory and cascades deletion to associated memory_context rows
  • Response: 204 No Content on success

Client and Server Implementation Examples

Saving Memories from the Client

The following TypeScript example demonstrates authenticating and persisting a memory via the REST API:

async function saveMemory(
  token: string,
  payload: {
    content: string;
    tags?: string[];
    contexts?: { snippet: string; conversation_id?: string; message_id?: string; origin?: string }[];
  }
) {
  const resp = await fetch(`${process.env.OPENWORK_API_URL}/v1/memory`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(payload),
  });
  
  if (!resp.ok) throw new Error(`Save failed: ${resp.status}`);
  return await resp.json(); // Returns { id: string }
}

Natural Language Search Implementation

To retrieve relevant memories using full-text search:

async function searchMemories(token: string, query: string, limit = 20) {
  const url = new URL(`${process.env.OPENWORK_API_URL}/v1/memory/search`);
  url.searchParams.set("q", query);
  url.searchParams.set("limit", String(limit));

  const resp = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${token}` },
  });
  
  const { results } = await resp.json();
  return results; // Each result includes relevance score for ranking
}

Server-Side Transaction Handler

The route handler in ee/apps/den-api/src/routes/memory.ts processes creation requests within a database transaction:

app.post(
  "/v1/memory",
  describeRoute({ tags: ["Memory"], summary: "Save a memory" }),
  jsonValidator(saveMemorySchema),
  async (c) => {
    const user = c.get("user");
    const body = c.req.valid("json");
    const memoryId = createDenTypeId("memory");

    // Force user scope and chat source for security
    await db.insert(MemoryTable).values({
      id: memoryId,
      user_id: user.id,
      org_id: c.get("activeOrganizationId"),
      scope: "user",
      content: body.content,
      source: "chat",
      tags: body.tags ?? null,
    });

    // Insert contexts transactionally
    if (body.contexts?.length) {
      const ctxRows = body.contexts.map((ctx) => ({
        id: createDenTypeId("memctx"),
        memory_id: memoryId,
        citation: ctx.citation ?? null,
        snippet: ctx.snippet,
        origin: ctx.origin ?? null,
      }));
      await db.insert(MemoryContextTable).values(ctxRows);
    }

    return c.json({ id: memoryId }, 201);
  },
);

Key Source Files in the Repository

The implementation spans multiple packages within the different-ai/openwork codebase:

Summary

  • The OpenWork Memory Bank uses a dual-table MySQL schema separating core content (memory) from optional citation contexts (memory_context)
  • All operations enforce user scoping at the database level, automatically filtering by user_id and forcing scope = 'user' regardless of client input
  • Full-text search leverages MySQL's FULLTEXT index with natural language mode matching, accessible via the getMemorySearch MCP capability
  • The system stores memories as plain text (v0) with server-enforced source = 'chat' metadata, with future versions planned to add encryption-at-rest
  • Four REST endpoints (POST, GET, GET /search, DELETE) provide complete CRUD operations auto-generated as MCP tools
  • Client implementation includes a feature-flagged desktop UI panel for memory management and natural-language retrieval

Frequently Asked Questions

How does user authorization work in the OpenWork Memory Bank?

Every database query automatically includes WHERE user_id = principal.userId filters. The server rejects requests for memory IDs not owned by the authenticated user with a 404 Not Found response, preventing unauthorized access through ID enumeration. Additionally, the system forces the scope column to 'user' regardless of any client-provided values, ensuring strict data isolation between users.

What search algorithm powers the memory retrieval?

The system uses MySQL's built-in full-text search with a dedicated MEMORY_CONTENT_FULLTEXT_INDEX on the memory.content column. Queries execute using MATCH … AGAINST … IN NATURAL LANGUAGE MODE, which performs lexical matching and returns relevance scores used for ranking results. According to the architecture documentation, future versions may introduce vector embedding support, but the current implementation relies entirely on MySQL's native full-text capabilities.

Can I store structured data or only plain text?

While the content field stores plain text, you can attach structured metadata via the tags JSON column and the memory_context table. The contexts array in the POST body accepts objects with snippet, citation, conversation_id, and origin fields, allowing you to preserve references to source conversations alongside the primary text memory. This hybrid approach supports rich citations while maintaining the simplicity of full-text search.

How do I delete a memory and ensure all associated data is removed?

Use the DELETE /v1/memory/:id endpoint or the deleteMemoryById MCP capability. The database schema defines cascading deletes that automatically remove associated rows from the memory_context table when a parent memory is deleted. The endpoint returns 204 No Content upon successful deletion, and the desktop UI in memory-view.tsx implements optimistic deletion with immediate UI updates and toast notifications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →