Skip to content

Commit 53438d1

Browse files
phernandezclaude
andauthored
feat: Add SPEC-15 for configuration persistence via Tigris (#343)
Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 032de7e commit 53438d1

File tree

4 files changed

+306
-16
lines changed

4 files changed

+306
-16
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
---
2+
title: 'SPEC-15: Configuration Persistence via Tigris for Cloud Tenants'
3+
type: spec
4+
permalink: specs/spec-14-config-persistence-tigris
5+
tags:
6+
- persistence
7+
- tigris
8+
- multi-tenant
9+
- infrastructure
10+
- configuration
11+
status: draft
12+
---
13+
14+
# SPEC-15: Configuration Persistence via Tigris for Cloud Tenants
15+
16+
## Why
17+
18+
We need to persist Basic Memory configuration across Fly.io deployments without using persistent volumes or external databases.
19+
20+
**Current Problems:**
21+
- `~/.basic-memory/config.json` lost on every deployment (project configuration)
22+
- `~/.basic-memory/memory.db` lost on every deployment (search index)
23+
- Persistent volumes break clean deployment workflow
24+
- External databases (Turso) require per-tenant token management
25+
26+
**The Insight:**
27+
The SQLite database is just an **index cache** of the markdown files. It can be rebuilt in seconds from the source markdown files in Tigris. Only the small `config.json` file needs true persistence.
28+
29+
**Solution:**
30+
- Store `config.json` in Tigris bucket (persistent, small file)
31+
- Rebuild `memory.db` on startup from markdown files (fast, ephemeral)
32+
- No persistent volumes, no external databases, no token management
33+
34+
## What
35+
36+
Store Basic Memory configuration in the Tigris bucket and rebuild the database index on tenant machine startup.
37+
38+
**Affected Components:**
39+
- `basic-memory/src/basic_memory/config.py` - Add configurable config directory
40+
41+
**Architecture:**
42+
43+
```bash
44+
# Tigris Bucket (persistent, mounted at /mnt/tigris)
45+
/mnt/tigris/
46+
├── .basic-memory/
47+
│ └── config.json # ← Project configuration (persistent, accessed via BASIC_MEMORY_CONFIG_DIR)
48+
└── projects/ # ← Markdown files (persistent)
49+
├── project1/
50+
└── project2/
51+
52+
# Fly Machine (ephemeral)
53+
~/.basic-memory/
54+
└── memory.db # ← Rebuilt on startup (fast local disk)
55+
```
56+
57+
## How (High Level)
58+
59+
### 1. Add Configurable Config Directory to Basic Memory
60+
61+
Currently `ConfigManager` hardcodes `~/.basic-memory/config.json`. Add environment variable to override:
62+
63+
```python
64+
# basic-memory/src/basic_memory/config.py
65+
66+
class ConfigManager:
67+
"""Manages Basic Memory configuration."""
68+
69+
def __init__(self) -> None:
70+
"""Initialize the configuration manager."""
71+
home = os.getenv("HOME", Path.home())
72+
if isinstance(home, str):
73+
home = Path(home)
74+
75+
# Allow override via environment variable
76+
if config_dir := os.getenv("BASIC_MEMORY_CONFIG_DIR"):
77+
self.config_dir = Path(config_dir)
78+
else:
79+
self.config_dir = home / DATA_DIR_NAME
80+
81+
self.config_file = self.config_dir / CONFIG_FILE_NAME
82+
83+
# Ensure config directory exists
84+
self.config_dir.mkdir(parents=True, exist_ok=True)
85+
```
86+
87+
### 2. Rebuild Database on Startup
88+
89+
Basic Memory already has the sync functionality. Just ensure it runs on startup:
90+
91+
```python
92+
# apps/api/src/basic_memory_cloud_api/main.py
93+
94+
@app.on_event("startup")
95+
async def startup_sync():
96+
"""Rebuild database index from Tigris markdown files."""
97+
logger.info("Starting database rebuild from Tigris")
98+
99+
# Initialize file sync (rebuilds index from markdown files)
100+
app_config = ConfigManager().config
101+
await initialize_file_sync(app_config)
102+
103+
logger.info("Database rebuild complete")
104+
```
105+
106+
### 3. Environment Configuration
107+
108+
```bash
109+
# Machine environment variables
110+
BASIC_MEMORY_CONFIG_DIR=/mnt/tigris/.basic-memory # Config read/written directly to Tigris
111+
# memory.db stays in default location: ~/.basic-memory/memory.db (local ephemeral disk)
112+
```
113+
114+
## Implementation Task List
115+
116+
### Phase 1: Basic Memory Changes ✅
117+
- [x] Add `BASIC_MEMORY_CONFIG_DIR` environment variable support to `ConfigManager.__init__()`
118+
- [x] Test config loading from custom directory
119+
- [x] Update tests to verify custom config dir works
120+
121+
### Phase 2: Tigris Bucket Structure
122+
- [ ] Ensure `.basic-memory/` directory exists in Tigris bucket on tenant creation
123+
- [ ] Initialize `config.json` in Tigris on first tenant deployment
124+
- [ ] Verify TigrisFS handles hidden directories correctly
125+
126+
### Phase 3: Deployment Integration
127+
- [ ] Set `BASIC_MEMORY_CONFIG_DIR` environment variable in machine deployment
128+
- [ ] Ensure database rebuild runs on machine startup via initialization sync
129+
- [ ] Handle first-time tenant setup (no config exists yet)
130+
- [ ] Test deployment workflow with config persistence
131+
132+
### Phase 4: Testing
133+
- [x] Unit tests for config directory override
134+
- [ ] Integration test: deploy → write config → redeploy → verify config persists
135+
- [ ] Integration test: deploy → add project → redeploy → verify project in config
136+
- [ ] Performance test: measure db rebuild time on startup
137+
138+
### Phase 5: Documentation
139+
- [ ] Document config persistence architecture
140+
- [ ] Update deployment runbook
141+
- [ ] Document startup sequence and timing
142+
143+
## How to Evaluate
144+
145+
### Success Criteria
146+
147+
1. **Config Persistence**
148+
- [ ] config.json persists across deployments
149+
- [ ] Projects list maintained across restarts
150+
- [ ] No manual configuration needed after redeploy
151+
152+
2. **Database Rebuild**
153+
- [ ] memory.db rebuilt on startup in < 30 seconds
154+
- [ ] All entities indexed correctly
155+
- [ ] Search functionality works after rebuild
156+
157+
3. **Performance**
158+
- [ ] SQLite queries remain fast (local disk)
159+
- [ ] Config reads acceptable (symlink to Tigris)
160+
- [ ] No noticeable performance degradation
161+
162+
4. **Deployment Workflow**
163+
- [ ] Clean deployments without volumes
164+
- [ ] No new external dependencies
165+
- [ ] No secret management needed
166+
167+
### Testing Procedure
168+
169+
1. **Config Persistence Test**
170+
```bash
171+
# Deploy tenant
172+
POST /tenants → tenant_id
173+
174+
# Add a project
175+
basic-memory project add "test-project" ~/test
176+
177+
# Verify config has project
178+
cat /mnt/tigris/.basic-memory/config.json
179+
180+
# Redeploy machine
181+
fly deploy --app basic-memory-{tenant_id}
182+
183+
# Verify project still exists
184+
basic-memory project list
185+
```
186+
187+
2. **Database Rebuild Test**
188+
```bash
189+
# Create notes
190+
basic-memory write "Test Note" --content "..."
191+
192+
# Redeploy (db lost)
193+
fly deploy --app basic-memory-{tenant_id}
194+
195+
# Wait for startup sync
196+
sleep 10
197+
198+
# Verify note is indexed
199+
basic-memory search "Test Note"
200+
```
201+
202+
3. **Performance Benchmark**
203+
```bash
204+
# Time the startup sync
205+
time basic-memory sync
206+
207+
# Should be < 30 seconds for typical tenant
208+
```
209+
210+
## Benefits Over Alternatives
211+
212+
**vs. Persistent Volumes:**
213+
- ✅ Clean deployment workflow
214+
- ✅ No volume migration needed
215+
- ✅ Simpler infrastructure
216+
217+
**vs. Turso (External Database):**
218+
- ✅ No per-tenant token management
219+
- ✅ No external service dependencies
220+
- ✅ No additional costs
221+
- ✅ Simpler architecture
222+
223+
**vs. SQLite on FUSE:**
224+
- ✅ Fast local SQLite performance
225+
- ✅ Only slow reads for small config file
226+
- ✅ Database queries remain fast
227+
228+
## Implementation Assignment
229+
230+
**Primary Agent:** `python-developer`
231+
- Add `BASIC_MEMORY_CONFIG_DIR` environment variable to ConfigManager
232+
- Update deployment workflow to set environment variable
233+
- Ensure startup sync runs correctly
234+
235+
**Review Agent:** `system-architect`
236+
- Validate architecture simplicity
237+
- Review performance implications
238+
- Assess startup timing
239+
240+
## Dependencies
241+
242+
- **Internal:** TigrisFS must be working and stable
243+
- **Internal:** Basic Memory sync must be reliable
244+
- **Internal:** SPEC-8 (TigrisFS Integration) must be complete
245+
246+
## Open Questions
247+
248+
1. Should we add a health check that waits for db rebuild to complete?
249+
2. Do we need to handle very large knowledge bases (>10k entities) differently?
250+
3. Should we add metrics for startup sync duration?
251+
252+
## References
253+
254+
- Basic Memory sync: `basic-memory/src/basic_memory/services/initialization.py`
255+
- Config management: `basic-memory/src/basic_memory/config.py`
256+
- TigrisFS integration: SPEC-8
257+
258+
---
259+
260+
**Status Updates:**
261+
262+
- 2025-10-08: Pivoted from Turso to Tigris-based config persistence
263+
- 2025-10-08: Phase 1 complete - BASIC_MEMORY_CONFIG_DIR support added (PR #343)
264+
- Next: Implement Phases 2-3 in basic-memory-cloud repository

src/basic_memory/cli/commands/status.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,25 +129,21 @@ def display_changes(
129129
async def run_status(project: Optional[str] = None, verbose: bool = False): # pragma: no cover
130130
"""Check sync status of files vs database."""
131131

132-
try:
133-
from basic_memory.config import ConfigManager
132+
from basic_memory.config import ConfigManager
134133

135-
config = ConfigManager().config
136-
auth_headers = {}
137-
if config.cloud_mode_enabled:
138-
auth_headers = await get_authenticated_headers()
134+
config = ConfigManager().config
135+
auth_headers = {}
136+
if config.cloud_mode_enabled:
137+
auth_headers = await get_authenticated_headers()
139138

140-
project_item = await get_active_project(client, project, None)
141-
response = await call_post(
142-
client, f"{project_item.project_url}/project/status", headers=auth_headers
143-
)
144-
sync_report = SyncReportResponse.model_validate(response.json())
139+
project_item = await get_active_project(client, project, None, auth_headers)
140+
response = await call_post(
141+
client, f"{project_item.project_url}/project/status", headers=auth_headers
142+
)
143+
sync_report = SyncReportResponse.model_validate(response.json())
145144

146-
display_changes(project_item.name, "Status", sync_report, verbose)
145+
display_changes(project_item.name, "Status", sync_report, verbose)
147146

148-
except (ValueError, ToolError) as e:
149-
console.print(f"[red]✗ Error: {e}[/red]")
150-
raise typer.Exit(1)
151147

152148

153149
@app.command()

src/basic_memory/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,12 @@ def __init__(self) -> None:
251251
if isinstance(home, str):
252252
home = Path(home)
253253

254-
self.config_dir = home / DATA_DIR_NAME
254+
# Allow override via environment variable
255+
if config_dir := os.getenv("BASIC_MEMORY_CONFIG_DIR"):
256+
self.config_dir = Path(config_dir)
257+
else:
258+
self.config_dir = home / DATA_DIR_NAME
259+
255260
self.config_file = self.config_dir / CONFIG_FILE_NAME
256261

257262
# Ensure config directory exists

tests/test_config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,28 @@ def test_disable_permalinks_flag_can_be_enabled(self):
179179
"""Test that disable_permalinks flag can be set to True."""
180180
config = BasicMemoryConfig(disable_permalinks=True)
181181
assert config.disable_permalinks is True
182+
183+
def test_config_manager_respects_custom_config_dir(self, monkeypatch):
184+
"""Test that ConfigManager respects BASIC_MEMORY_CONFIG_DIR environment variable."""
185+
with tempfile.TemporaryDirectory() as temp_dir:
186+
custom_config_dir = Path(temp_dir) / "custom" / "config"
187+
monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(custom_config_dir))
188+
189+
config_manager = ConfigManager()
190+
191+
# Verify config_dir is set to the custom path
192+
assert config_manager.config_dir == custom_config_dir
193+
# Verify config_file is in the custom directory
194+
assert config_manager.config_file == custom_config_dir / "config.json"
195+
# Verify the directory was created
196+
assert config_manager.config_dir.exists()
197+
198+
def test_config_manager_default_without_custom_config_dir(self, config_home, monkeypatch):
199+
"""Test that ConfigManager uses default location when BASIC_MEMORY_CONFIG_DIR is not set."""
200+
monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False)
201+
202+
config_manager = ConfigManager()
203+
204+
# Should use default location
205+
assert config_manager.config_dir == config_home / ".basic-memory"
206+
assert config_manager.config_file == config_home / ".basic-memory" / "config.json"

0 commit comments

Comments
 (0)