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

> Explore the OpenWork Memory Bank's architecture and API for persistent storage. Learn how this MySQL-based layer saves, lists, searches, and deletes text memories with citation contexts.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: architecture
- Published: 2026-08-21

---

**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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/docs/memory-bank-architecture.md).

### MySQL Storage and Full-Text Search

The persistence layer relies on two MySQL tables defined in [`ee/packages/den-db/src/schema/memory.ts`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/ee/packages/den-db/src/schema/memory.ts) implement the dual-table structure with type-safe identifiers:

```typescript
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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/memory.ts) processes creation requests within a database transaction:

```typescript
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:

- **[`ee/packages/den-db/src/schema/memory.ts`](https://github.com/different-ai/openwork/blob/main/ee/packages/den-db/src/schema/memory.ts)** – Defines the `MemoryTable` and `MemoryContextTable` schemas, including the `MEMORY_CONTENT_FULLTEXT_INDEX` constant and id generation logic
- **[`ee/packages/den-db/drizzle/0028_memory_bank.sql`](https://github.com/different-ai/openwork/blob/main/ee/packages/den-db/drizzle/0028_memory_bank.sql)** – Production migration creating tables, indexes, and the full-text search index
- **[`ee/apps/den-api/src/openapi.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/openapi.ts)** – Generates OpenAPI operation IDs that become MCP capability names like `postMemory` and `getMemorySearch`
- **[`ee/apps/den-api/src/mcp/policy.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/mcp/policy.ts)** – Registers Memory Bank endpoints under the MCP `"Memory"` tag for agent discovery
- **[`apps/app/src/react-app/shell/session-memory.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/shell/session-memory.ts)** – Client-side session management hook for memory operations
- **[`apps/app/src/react-app/domains/settings/pages/memory-view.tsx`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/domains/settings/pages/memory-view.tsx)** – Desktop UI component for listing and deleting memories, gated by the `featureFlags.memory` feature flag
- **[`apps/app/src/react-app/domains/settings/pages/memory-utils.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/domains/settings/pages/memory-utils.ts)** – Helper utilities for content escaping, formatting, and toast notifications
- **[`docs/memory-bank-architecture.md`](https://github.com/different-ai/openwork/blob/main/docs/memory-bank-architecture.md)** – Canonical design document describing security decisions, data flows, and the owner-scoping implementation

## 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`](https://github.com/different-ai/openwork/blob/main/memory-view.tsx) implements optimistic deletion with immediate UI updates and toast notifications.