Fortemi supports parallel memory archives, allowing you to maintain multiple isolated knowledge bases within a single deployment. Each memory operates as a separate PostgreSQL schema with its own notes, tags, collections, embeddings, links, and templates.
A memory (formerly called "archive") is an isolated namespace for your knowledge base. Think of memories as separate workspaces or projects, each with complete data isolation:
- Work Memory: Professional projects and documentation
- Personal Memory: Private notes and journal entries
- Research Memory: Academic papers and literature reviews
- Client Memories: Separate workspace per client for data isolation
- Complete Isolation: Each memory has its own PostgreSQL schema. Notes, tags, collections, embeddings, and links never cross memory boundaries
- Per-Request Routing: Use the
X-Fortemi-MemoryHTTP header to select which memory to operate on - Federated Search: Search across multiple memories simultaneously with unified result ranking
- Memory Cloning: Deep copy entire memories including all notes, embeddings, and relationships
- Auto-Migration: Memories are automatically updated when new table structures are added
- Capacity Management: System-wide limits and per-memory statistics via overview endpoint
Each memory operates in its own PostgreSQL schema:
Database: matric
├── public (default memory + shared tables)
│ ├── archive_registry (shared)
│ ├── oauth_clients (shared)
│ ├── api_keys (shared)
│ ├── note (default memory data)
│ ├── embedding (default memory data)
│ ├── note_links (default memory data)
│ ├── skos_concepts (default memory data)
│ └── ... (41 per-memory tables + 14 shared tables)
├── archive_work_2026 (custom memory)
│ ├── note
│ ├── note_original
│ ├── embedding
│ └── ... (41 per-memory tables)
└── archive_research (custom memory)
├── note
├── embedding
└── ... (41 per-memory tables)
Note: The default memory uses the public schema. The seed migration (20260208000002_seed_default_archive.sql) creates a registry entry named "default" with schema_name = 'public'.
Shared Tables (14 total):
- Authentication: OAuth clients, API keys, sessions
- System: Job queue, event subscriptions, webhooks
- Registry: Archive metadata, embedding configurations
These tables live in the public schema and are shared across all memories.
Per-Memory Tables (41 total):
- Notes: note, note_original, note_revision
- Embeddings: embedding, embedding_set, embedding_set_member
- Links: note_links
- Tags: tag, tag_note, skos_concepts, skos_labels, skos_relations
- Collections: collection, collection_note
- Templates: template
- Attachments: file_attachment, file_provenance
- Document Types: Custom types per memory
- Versioning: Content history tables
The system uses a deny-list approach: all tables are per-memory except the 14 explicitly shared tables defined in SHARED_TABLES constant. This ensures zero drift when new tables are added - they automatically become per-memory unless explicitly added to the shared list.
Via API:
curl -X POST http://localhost:3000/api/v1/memories \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "work-2026",
"description": "Work-related notes for 2026"
}'Via MCP:
create_memory({
name: "work-2026",
description: "Work-related notes for 2026"
})Memory names must be valid PostgreSQL schema identifiers (lowercase letters, numbers, underscores, hyphens).
Via API:
curl http://localhost:3000/api/v1/memories \
-H "Authorization: Bearer $TOKEN"Response:
{
"memories": [
{
"name": "default",
"description": "Default memory",
"created_at": "2026-01-15T10:00:00Z",
"note_count": 1523,
"size_bytes": 52428800,
"schema_version": 41
},
{
"name": "work-2026",
"description": "Work-related notes for 2026",
"created_at": "2026-02-01T12:00:00Z",
"note_count": 245,
"size_bytes": 8388608,
"schema_version": 41
}
]
}curl http://localhost:3000/api/v1/memories/work-2026 \
-H "Authorization: Bearer $TOKEN"curl -X PATCH http://localhost:3000/api/v1/memories/work-2026 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"description": "Updated description"
}'curl -X DELETE http://localhost:3000/api/v1/memories/work-2026 \
-H "Authorization: Bearer $TOKEN"Deletes the memory's schema and all data within it. This operation is irreversible.
Select which memory to operate on using the X-Fortemi-Memory HTTP header:
# Create note in work memory
curl -X POST http://localhost:3000/api/v1/notes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Fortemi-Memory: work-2026" \
-d '{
"content": "# Project Documentation\n\nInternal documentation for project X."
}'
# Search in work memory
curl "http://localhost:3000/api/v1/search?q=project+documentation" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Fortemi-Memory: work-2026"
# List notes from work memory
curl http://localhost:3000/api/v1/notes \
-H "Authorization: Bearer $TOKEN" \
-H "X-Fortemi-Memory: work-2026"If no header is provided, the request operates on the default memory configured via set_default_archive API. If no default is configured, requests fall back to the public schema.
Default Archive Caching:
- The default archive is cached for 60 seconds (configurable via
DEFAULT_ARCHIVE_CACHE_TTLenvironment variable) to minimize database queries - Setting a new default via the API invalidates the cache immediately
- This provides a balance between performance and responsiveness to configuration changes
The MCP server provides memory management tools with session-based memory context:
Switch the active memory for the current MCP session:
select_memory({ name: "work-2026" })
// All subsequent operations use work-2026 memoryCheck which memory is currently active:
get_active_memory()
// Returns: { name: "work-2026" }List all available memories:
list_memories()Create a new memory:
create_memory({
name: "research",
description: "Academic research notes"
})Delete a memory and all its data:
delete_memory({ name: "old-project" })Search across multiple memories simultaneously with unified result ranking.
Search in non-default memories uses per-schema connection pools with search_path pinned to the target archive. Standard search (GET /api/v1/search) works in any memory selected via the X-Fortemi-Memory header. For cross-archive queries, use federated search (POST /api/v1/search/federated) to search multiple memories simultaneously with unified ranking.
curl -X POST http://localhost:3000/api/v1/search/federated \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"query": "machine learning",
"memories": ["all"]
}'curl -X POST http://localhost:3000/api/v1/search/federated \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"query": "project documentation",
"memories": ["work-2026", "research"]
}'{
"results": [
{
"note_id": "550e8400-...",
"memory": "work-2026",
"score": 0.92,
"title": "Project Documentation",
"snippet": "...machine learning algorithms...",
"tags": ["project", "ml"]
},
{
"note_id": "660e8400-...",
"memory": "research",
"score": 0.85,
"title": "ML Research Papers",
"snippet": "...deep learning techniques...",
"tags": ["research", "ml"]
}
],
"total": 2,
"memories_searched": ["work-2026", "research"]
}- Parallel Execution: Search runs concurrently across all specified memories
- Score Normalization: Scores are normalized to [0,1] range per memory
- Unified Ranking: Results are merged and re-sorted by score
- Memory Attribution: Each result includes its source memory name
MCP Tool:
search_memories_federated({
query: "machine learning",
memories: ["all"]
})Deep copy entire memories including all notes, embeddings, links, and relationships.
curl -X POST http://localhost:3000/api/v1/archives/work-2026/clone \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"new_name": "work-2026-backup",
"description": "Backup of work memory before major refactoring"
}'- Schema Creation: Creates new PostgreSQL schema with
new_name - Table Structure Copy: Creates empty tables using
CREATE TABLE ... (LIKE ... INCLUDING ALL) - FK Dependency Resolution: Orders tables by foreign key dependencies using recursive CTE
- Data Copy: Uses
INSERT INTO new.table SELECT columns FROM old.table(filtering out generated columns) - Relationship Preservation: UUIDs remain identical, preserving all links and embeddings
- FK and Trigger Recreation: Re-creates foreign keys and triggers separately
- Auto-Migration: New memory is automatically at current schema version
Note: The implementation does NOT use session_replication_role = 'replica'. Instead, it copies data in FK dependency order, ensuring referential integrity without requiring superuser privileges.
- Backup before major changes: Clone before bulk deletions or schema migrations
- Testing environments: Clone production memory for testing without affecting live data
- Client project templates: Clone a template memory structure for new clients
- Archival: Create point-in-time snapshots of memories
MCP Tool:
clone_memory({
source_name: "work-2026",
new_name: "work-2026-backup",
description: "Backup before migration"
})Get aggregate statistics across all memories:
curl http://localhost:3000/api/v1/memories/overview \
-H "Authorization: Bearer $TOKEN"Response:
{
"capacity": {
"max_memories": 10,
"current_count": 3,
"available": 7
},
"usage": {
"total_notes": 1768,
"total_size_bytes": 60817408,
"total_size_human": "58.02 MB"
},
"memories": [
{
"name": "default",
"note_count": 1523,
"size_bytes": 52428800,
"size_human": "50.00 MB",
"schema_version": 41
},
{
"name": "work-2026",
"note_count": 245,
"size_bytes": 8388608,
"size_human": "8.00 MB",
"schema_version": 41
}
],
"database": {
"total_size_bytes": 104857600,
"total_size_human": "100.00 MB"
}
}MAX_MEMORIES limits the number of live memories (active schemas in the database). This is not a hard cap on total archives — you can export any memory as a shard, delete it to free a slot, and re-import it later. There is no limit on the number of archived shards stored on disk.
# .env — scale with your hardware
MAX_MEMORIES=10 # Default (Tier 1: 8GB RAM, 10GB disk)
MAX_MEMORIES=50 # Tier 2: 16GB RAM, 100GB disk
MAX_MEMORIES=200 # Tier 3: 32GB RAM, 500GB disk
MAX_MEMORIES=500 # Tier 4: 64GB+ RAM, 1TB+ diskSee Configuration Reference for the capacity formula and detailed sizing by hardware tier.
Swapping memories in and out:
# Export a memory to a shard file (frees the slot after delete)
curl -X POST http://localhost:3000/api/v1/shards/export \
-H "X-Fortemi-Memory: old-project" -o old-project.shard
# Delete the live memory to free a slot
curl -X DELETE http://localhost:3000/api/v1/archives/old-project
# Later: re-import when needed
curl -X POST http://localhost:3000/api/v1/shards/import \
-F "file=@old-project.shard"Attempting to create memories beyond the live limit will fail with HTTP 400:
{
"error": "Memory limit reached. Maximum 10 memories allowed."
}Each memory tracks:
- note_count: Total number of notes
- size_bytes: Estimated size on disk (all tables combined)
- schema_version: Number of tables (for auto-migration tracking)
- last_accessed: Timestamp of last operation (updated automatically)
MCP Tool:
get_memories_overview()Memories are automatically migrated when new table structures are added to the system.
- Schema Version Tracking: Each memory stores its
schema_version(current table count) - On Access Check: When a memory is accessed, the system compares its schema version to the expected version
- Missing Table Detection: If
schema_version < expected, missing tables are created automatically - Create Tables: Uses the same
CREATE TABLEstatements that initialized the default memory - Version Update:
schema_versionis updated to reflect the new table count
Auto-migration runs when:
- A memory is accessed via
X-Fortemi-Memoryheader - MCP selects a memory via
select_memory - Federated search includes a memory
- Memory clone operation completes
- Non-Destructive: Only creates missing tables, never modifies existing ones
- Idempotent: Safe to run multiple times
- Logged: Migration events are logged for debugging
- Fast: Typically completes in <100ms (empty table creation only)
Check if a memory needs migration:
curl http://localhost:3000/api/v1/memories/work-2026 \
-H "Authorization: Bearer $TOKEN"If schema_version < 41 (current expected version), the memory will be auto-migrated on next access.
Each memory has complete isolation of:
- Notes: All note content, revisions, and original versions
- Embeddings: Vector embeddings and embedding sets
- Links: Semantic relationships between notes
- Tags: User tags and SKOS concept taxonomies
- Collections: Folder hierarchies
- Templates: Note templates and their instantiations
- Attachments: File attachments and provenance data
- Document Types: Custom document type definitions
The following data is shared across all memories:
- Authentication: OAuth clients, API keys, user sessions
- Job Queue: Background processing jobs (though jobs operate on specific memories)
- Event Subscriptions: Webhooks and event stream configuration
- System Configuration: Embedding configurations, backup metadata
Operations cannot cross memory boundaries:
- Notes in memory A cannot link to notes in memory B
- Search in memory A will never return notes from memory B (unless using federated search)
- Tags in memory A are separate from tags in memory B (even if identically named)
Exception: Federated search explicitly searches multiple memories and attributes results to their source memory.
- Cross-archive note linking: Notes cannot link across memory boundaries. Use export/import or federated search for cross-memory workflows.
- Embedding generation: Background jobs use archive context from the job payload. Embedding pipelines must be triggered per-archive.
- Cross-archive operations: No API support for copying notes between archives. Use export/import workflow instead.
Existing deployments automatically have a default memory mapped to the public schema. This happens via the seed migration (20260208000002_seed_default_archive.sql) which inserts a registry entry on first startup. No manual migration is required.
- Create New Memories: Create memories for different projects or clients
- Move Notes: Use export/import or manual copying to move notes between memories
- Update Integrations: Add
X-Fortemi-Memoryheader to API calls that should target specific memories - Test Isolation: Verify notes don't cross memory boundaries
- Archive Old Data: Move historical data to archived memories for cleanup
- All API calls without
X-Fortemi-Memoryheader operate on thedefaultmemory - Existing code continues to work without changes
- No data loss or schema migration required
| Endpoint | Method | Description |
|---|---|---|
/api/v1/memories |
GET | List all memories |
/api/v1/memories |
POST | Create new memory |
/api/v1/memories/:name |
GET | Get memory details |
/api/v1/memories/:name |
PATCH | Update memory metadata |
/api/v1/memories/:name |
DELETE | Delete memory |
/api/v1/memories/overview |
GET | Get aggregate statistics |
/api/v1/archives/:name/clone |
POST | Clone memory (deep copy) |
/api/v1/search/federated |
POST | Search across multiple memories |
| Header | Values | Description |
|---|---|---|
X-Fortemi-Memory |
Memory name | Routes request to specified memory (default: configured default or "public") |
| Tool | Description |
|---|---|
list_memories |
List all memories |
get_active_memory |
Get current session's active memory |
select_memory |
Switch active memory for session |
create_memory |
Create new memory |
delete_memory |
Delete memory |
clone_memory |
Clone memory with all data |
search_memories_federated |
Search across multiple memories |
get_memories_overview |
Get capacity and usage statistics |
Backups can be scoped to specific memories:
# Backup work memory only
curl http://localhost:3000/api/v1/backup/knowledge-shard \
-H "X-Fortemi-Memory: work-2026" \
-H "Authorization: Bearer $TOKEN" \
-o work-2026-backup.shard# Restore to a different memory (multipart upload)
curl -X POST http://localhost:3000/api/v1/backup/knowledge-shard/upload?on_conflict=skip \
-H "X-Fortemi-Memory: work-2026-restored" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@work-2026.shard"Database backups include all memories and shared tables:
# Full pg_dump backup
curl http://localhost:3000/api/v1/backup/database \
-H "Authorization: Bearer $TOKEN" \
-o full-backup.sqlSee Backup Guide for comprehensive backup strategies.
Use multiple memories when you need:
- Client isolation: Separate data per client with strict isolation guarantees
- Project separation: Isolate different projects with distinct knowledge domains
- Personal vs professional: Keep personal notes separate from work
- Archival: Move old projects to archived memories without deletion
Don't use multiple memories for:
- Tagging or categorization: Use tags and collections within a single memory
- Temporary organization: Use collections for temporary grouping
- Search filtering: Use strict tag filtering for data isolation within a memory
- Use lowercase with hyphens:
client-acme-2026 - Include dates for temporal organization:
work-2026-q1 - Avoid special characters: stick to letters, numbers, underscores, hyphens
- Keep names short but descriptive:
research-mlnotmachine-learning-research-notes
- Small memories: <10,000 notes, <100MB - Fast operations, minimal overhead
- Medium memories: 10,000-100,000 notes, 100MB-1GB - Good performance with proper indexing
- Large memories: >100,000 notes, >1GB - Consider splitting into multiple memories
- Each memory adds minimal overhead (<1MB) for metadata and indexes
- Search performance scales with memory size, not total number of memories
- Federated search adds latency proportional to number of memories searched
- Memory cloning time scales with source memory size (typical: 1GB/minute)
Symptom: HTTP 404 when accessing a memory
Causes:
- Typo in memory name (names are case-sensitive)
- Memory was deleted
- Memory name not valid PostgreSQL identifier
Fix:
# List all memories
curl http://localhost:3000/api/v1/memories -H "Authorization: Bearer $TOKEN"Symptom: HTTP 400 "Memory limit reached"
Cause: MAX_MEMORIES environment variable limit hit
Fix:
# Increase limit in .env
MAX_MEMORIES=200
# Restart container
docker compose -f docker-compose.bundle.yml down
docker compose -f docker-compose.bundle.yml up -dSymptom: Federated search takes >5 seconds
Cause: Searching too many memories or very large memories
Fix:
- Reduce number of memories searched
- Use specific memory names instead of
["all"] - Optimize individual memory search performance (add embeddings, vacuum database)
Symptom: Queries fail with "relation does not exist"
Cause: Memory hasn't been auto-migrated yet
Fix: Access the memory to trigger auto-migration:
curl http://localhost:3000/api/v1/notes \
-H "X-Fortemi-Memory: memory-name" \
-H "Authorization: Bearer $TOKEN"- Backup Guide - Per-memory backup strategies
- Search Guide - Search modes and federated search
- MCP Server - Memory management via MCP tools
- Configuration Reference - MAX_MEMORIES and other settings
- API Reference - Complete API endpoint documentation
- Architecture - Multi-memory system architecture