How ai-memory Implements Per-Project Isolation Using `workspace_id`, `project_id`, `path` Tuples

The ai-memory system enforces strict data isolation by treating (workspace_id, project_id, path) as a three-field coordinate that serves as the canonical primary key for all domain tables and the on-disk filesystem layout.

Every piece of data in ai-memory—from wiki pages to observations, sessions, and handoffs—is bound to this immutable tuple. This design guarantees that no two projects can collide, and no bug or malicious payload can access data across workspace boundaries. The architecture is implemented across the akitaonrails/ai-memory repository using strongly-typed identifiers and a single-writer pattern.

The Three-Field Coordinate System

The per-project isolation mechanism centers on three strongly-typed fields defined in crates/ai-memory-core/src/ids.rs (lines 80-92):

Field Type Purpose
workspace_id WorkspaceId UUID v7 identifying the top-level workspace container
project_id ProjectId UUID v7 identifying a project within that workspace
path PagePath Normalized POSIX-style path identifying a page within the project

These types are not simple aliases—they're newtype wrappers that prevent accidental mixing of identifiers at compile time. The tuple (WorkspaceId, ProjectId, PagePath) forms the only valid coordinate for any data operation.

Filesystem Isolation via Wiki::project_root

The on-disk wiki layout mirrors the tuple structure exactly. In crates/ai-memory-wiki/src/wiki.rs (lines 81-85), the project_root method constructs the base path:

// From crates/ai-memory-wiki/src/wiki.rs
fn project_root(&self, workspace_id: WorkspaceId, project_id: ProjectId) -> PathBuf {
    self.wiki_root()
        .join(workspace_id.to_string())
        .join(project_id.to_string())
}

This ensures the filesystem hierarchy is:


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

The Wiki::abs_path method is the only sanctioned way to resolve page paths. It concatenates all three tuple components, preventing ad-hoc string joins that could bypass isolation boundaries.

Database Enforcement Through WriterHandle

All writes flow through the single-writer actor in crates/ai-memory-store/src/writer.rs. This actor stamps workspace_id and project_id on every row across all domain tables:

  • pages
  • observations
  • sessions
  • handoffs

The SQLite schema reinforces this at creation time with a UNIQUE(workspace_id, name) constraint on the projects table, ensuring no duplicate (workspace_id, project_id) pairs exist.

Writing a Page with Full Isolation

use ai_memory_core::ids::{WorkspaceId, ProjectId, PagePath};
use ai_memory_wiki::Wiki;

// Generate fresh UUID v7 identifiers
let ws_id = WorkspaceId::new();
let proj_id = ProjectId::new();

// Create Wiki handle (data_dir and writer are pre-configured)
let wiki = Wiki::new(&data_dir, writer);

// Define validated page path
let path = PagePath::new("notes/overview.md")?;

// Write page—tuple is automatically prefixed
wiki.write_page(ws_id, proj_id, path.clone(), "Hello world!".into())?;

// Resulting disk location:
// <data_dir>/wiki/<ws_id>/<proj_id>/notes/overview.md

Querying Project-Scoped Data

use ai_memory_store::ReaderPool;

// Fetch all observations for a specific project tuple
let rows = reader
    .observations_by_ids(ws_id, proj_id, None)?; // None = all paths

Without both ws_id and proj_id, the query cannot execute—the API requires the complete coordinate.

Moving Projects with Re-Stamp Operations

When a project must relocate to a different workspace, ai-memory performs an atomic re-stamp operation. The move_project_workspace method in crates/ai-memory-store/src/ops.rs (around line 5313) updates every row containing the old tuple in a single transaction:

use ai_memory_store::WriterHandle;

// Move project to new workspace
writer.move_project_workspace(
    proj_id,      // project to relocate
    old_ws_id,    // source workspace
    new_ws_id,    // destination workspace
)?;

This operation:

  1. Locks the writer to prevent concurrent modifications
  2. Updates all table rows where (workspace_id, project_id) matches
  3. Updates the on-disk wiki directory structure
  4. Commits as a single atomic transaction

The re-stamp logic ensures data integrity—there is no window where a project exists in two workspaces simultaneously.

Security Guarantees of the Tuple Design

The three-field coordinate provides defense in depth:

  • Type safety: WorkspaceId and ProjectId are distinct types—accidental swapping fails at compile time
  • Schema constraints: SQLite unique constraints prevent duplicate project names within a workspace
  • API enforcement: No public method accepts a bare project_id without its accompanying workspace_id
  • Filesystem isolation: Path resolution requires all three tuple components via Wiki::abs_path

A bug or malicious actor knowing only a project_id cannot affect data in another workspace because the workspace_id component is mandatory for every lookup path.

Summary

  • Per-project isolation in ai-memory relies on a canonical (workspace_id, project_id, path) tuple that serves as the primary key for all data
  • Filesystem layout at crates/ai-memory-wiki/src/wiki.rs mirrors this tuple exactly under <wiki_root>/<workspace_id>/<project_id>/<page_path>
  • Single-writer enforcement through WriterHandle stamps both IDs on every domain row
  • Atomic re-stamp operations in crates/ai-memory-store/src/ops.rs enable safe project relocation
  • Strong typing in crates/ai-memory-core/src/ids.rs prevents identifier confusion at compile time

Frequently Asked Questions

What happens if two projects have the same name in different workspaces?

The SQLite schema enforces UNIQUE(workspace_id, name) on the projects table, not UNIQUE(name). Identical names are permitted across workspaces because the workspace_id component distinguishes them. The tuple (workspace_id, project_id) remains globally unique.

Can a page path escape its project directory through traversal attacks?

No. PagePath::new() in crates/ai-memory-core/src/ids.rs validates and normalizes all paths, rejecting any containing .. components or absolute prefixes. The Wiki::abs_path method concatenates components without interpretation, making directory traversal impossible.

How does the re-stamp operation maintain consistency during a workspace move?

The move_project_workspace method holds exclusive access to the WriterHandle throughout execution. All updates—database rows and filesystem directories—occur within a single SQLite transaction. If any step fails, the entire operation rolls back.

Why use UUID v7 for workspace and project identifiers?

UUID v7 provides time-ordered, k-sortable identifiers that improve database locality and indexing performance while maintaining global uniqueness. The ids.rs implementation leverages this for efficient sequential storage in SQLite B-trees.

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 →