# How ai-memory Keys Data Entries Using the (workspace_id, project_id, path) Tuple

> Discover how ai-memory keys data entries using the (workspace_id, project_id, path) tuple for isolated and consistent storage in SQLite and filesystem wikis.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: internals
- Published: 2026-09-12

---

**ai-memory treats every stored object as uniquely identifiable by the composite triple (WorkspaceId, ProjectId, PagePath), enforcing strict data isolation and consistent addressing across its SQLite store and filesystem-backed wiki.**

The akitaonrails/ai-memory repository implements a robust identity system where the `(workspace_id, project_id, path)` tuple serves as the universal primary key for all persistent data. This three-part identifier guarantees that pages, observations, and handoffs remain strictly scoped to their respective workspaces and projects while enabling seamless cross-component lookups.

## Deconstructing the Composite Key Components

In [`crates/ai-memory-core/src/ids.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/ids.rs), the system defines three distinct newtypes that combine to form the universal addressing scheme.

### WorkspaceId (UUID v7)

The **WorkspaceId** is a UUID version 7 that uniquely identifies a workspace. Generated via `WorkspaceId::new()`, this identifier serves as the top-level namespace for all data, ensuring complete isolation between different workspaces.

### ProjectId (UUID v7)

Nested within a workspace, the **ProjectId**—also a UUID v7 defined in [`ids.rs`](https://github.com/akitaonrails/ai-memory/blob/main/ids.rs)—scopes data to a specific project. When combined with WorkspaceId, it creates a two-level hierarchy that prevents collisions between identically named paths in different projects.

### PagePath (Normalized POSIX Path)

The **PagePath** newtype represents a normalized, POSIX-style relative path. It enforces strict validation: no leading slashes, no backslashes, and no `.` or `..` components. This ensures portable, deterministic addressing across operating systems.

## Database Schema and Primary Key Constraints

All SQLite tables in the store layer—such as `pages`, `observations`, and `handovers`—use the composite `(workspace_id, project_id, path)` as their primary key. As implemented in the store crate, each row contains explicit fields:

- `workspace_id: WorkspaceId`
- `project_id: ProjectId`
- `path: PagePath`

This schema guarantees that data from different workspaces or projects never collides, supporting multi-user concurrency by ensuring every row is uniquely addressable. The domain model in [`crates/ai-memory-core/src/page.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/page.rs) embeds this triple directly, maintaining alignment between business logic and storage layers.

## Filesystem Layout and Path Resolution

The repository mirrors the database addressing scheme on disk. In [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs), the `abs_path` helper constructs absolute filesystem locations using the exact same triple:

```

<wiki_root>/<workspace_id>/<project_id>/<page_path>

```

Every write and read operation routes through this helper, ensuring consistency between the SQLite store and the on-disk representation. The `PagePath` normalization rules guarantee that the filesystem structure remains portable and unambiguous across platforms.

## Practical Implementation Examples

The following Rust code demonstrates how to construct and use the composite key:

```rust
use ai_memory_core::ids::{WorkspaceId, ProjectId, PagePath};

// Create a fresh workspace and project.
let ws_id = WorkspaceId::new();          // e.g. "018d0c5b-8c9f-7e8a-..."
let proj_id = ProjectId::new();          // e.g. "018d0c5c-1a2b-7e8b-..."

// Build a page path (must be portable).
let path = PagePath::new("notes/meeting.md")?;

// The complete key used by the store and wiki.
let key = (ws_id, proj_id, path);

// Example: writing a page via the wiki API.
wiki.write_page(ws_id, proj_id, &path, "Page contents".into()).await?;

```

To retrieve data from the store using this identifier:

```rust
// Querying a page from the store (pseudo-code).
let page = store
    .latest_page_id_by_ids(ws_id, proj_id, path.as_str().to_string())
    .await?;

```

## Benefits of the Triple Key Design

Centralizing identity in the `(workspace_id, project_id, path)` tuple provides three critical advantages:

- **Strict Data Isolation**: Workspaces and projects are completely segregated at the database and filesystem levels, preventing accidental data leakage between tenants.
- **Multi-user Concurrency**: The composite primary key eliminates race conditions by ensuring every write targets a unique, unambiguous row.
- **Cross-Component Consistency**: Whether addressing entities via the store, wiki, or MCP routes, all subsystems use the identical triple, simplifying scope resolution logic throughout the codebase.

## Summary

- The **(workspace_id, project_id, path)** tuple functions as the universal primary key across all ai-memory data stores.
- **WorkspaceId** and **ProjectId** are UUID v7 types defined in [`crates/ai-memory-core/src/ids.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/ids.rs), while **PagePath** enforces normalized POSIX conventions.
- SQLite tables use this composite key to prevent collisions, and the `abs_path` helper in [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) mirrors the structure on disk.
- This design enables strict tenant isolation, safe concurrent access, and consistent addressing between the database and filesystem layers.

## Frequently Asked Questions

### How does ai-memory prevent path traversal attacks?

The **PagePath** newtype in [`crates/ai-memory-core/src/ids.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/ids.rs) validates all inputs to reject paths containing `..` components, leading slashes, or backslashes. This ensures that malicious paths like `../../../etc/passwd` are rejected before reaching the filesystem, keeping data confined to the intended `<wiki_root>/<workspace_id>/<project_id>/` directory.

### Can two projects in different workspaces have identical page paths?

Yes. Because the primary key includes **workspace_id**, **project_id**, and **path**, the database treats `(workspace_a, project_1, "readme.md")` and `(workspace_b, project_2, "readme.md")` as distinct rows. This allows complete namespace independence between workspaces.

### What happens if I try to create a PagePath with a leading slash?

The `PagePath::new()` constructor returns an error. The normalization rules require relative paths without leading slashes to ensure portability and to maintain the invariant that the on-disk layout matches the database key structure exactly.

### Is the composite key used for entity types beyond wiki pages?

Yes. According to the source code, tables for **observations**, **handovers**, and other entities in the store layer all include the `workspace_id`, `project_id`, and `path` fields. This uniform addressing scheme allows any subsystem to locate related metadata using the same triple that identifies the primary page.