> ## Documentation Index
> Fetch the complete documentation index at: https://docs.glyphformac.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Space System

> Understanding Glyph's space-based architecture

## What is a Space?

A **space** is Glyph's fundamental organizational unit. Each space is a directory on the user's filesystem containing:

* **Notes** - Markdown files with YAML frontmatter
* **Assets** - Content-addressed files (images, PDFs, etc.)
* **Cache** - Derived data (link previews, thumbnails)
* **Metadata** - Schema version and configuration

<Note>
  Spaces are portable. You can move a space folder anywhere, sync via Dropbox/iCloud, or manage it with Git.
</Note>

## Directory Structure

```
my-space/
├── notes/                    # User-created markdown files
│   ├── daily/                # Daily notes (optional)
│   ├── projects/             # Project notes
│   └── meeting-notes.md
│
├── assets/                   # Content-addressed storage
│   ├── a1b2c3...xyz.png     # SHA256 hash as filename
│   └── f4e5d6...abc.pdf
│
├── cache/                    # Temporary/derived data
│   ├── links/                # Link preview metadata
│   └── images/               # Cached external images
│
├── .glyph/                   # App-managed data (not in space root)
│   ├── index.db              # SQLite FTS + tags + links
│   ├── ai_history.db         # AI chat conversations
│   ├── profiles.json         # AI provider configs
│   └── settings.json         # Space-specific settings
│
└── space.json                # Schema version marker
```

<Warning>
  The `.glyph/` folder is **derived data**. It can be safely deleted and will regenerate on next space open. Do not sync it.
</Warning>

## Space Lifecycle

### Creating a Space

<Steps>
  <Step title="User selects directory">
    User chooses an empty or existing folder via native file picker

    ```typescript src/contexts/SpaceContext.tsx theme={null}
    const onCreateSpace = async () => {
      const { open } = await import('@tauri-apps/plugin-dialog');
      const selection = await open({ directory: true });
      await invoke('space_create', { path: selection });
    };
    ```
  </Step>

  <Step title="Backend initializes structure">
    Rust backend creates directories and schema marker

    ```rust src-tauri/src/space/commands.rs theme={null}
    pub fn space_create(path: String) -> Result<SpaceInfo, String> {
      fs::create_dir_all(path.join("notes"))?;
      fs::create_dir_all(path.join("assets"))?;
      fs::create_dir_all(path.join("cache"))?;
      
      let schema = SpaceSchema { version: CURRENT_SCHEMA_VERSION };
      write_atomic(path.join("space.json"), serde_json::to_vec(&schema)?)?;
      
      Ok(SpaceInfo { root: path, schema_version: CURRENT_SCHEMA_VERSION })
    }
    ```
  </Step>

  <Step title="Frontend updates state">
    SpaceContext tracks current space path

    ```typescript theme={null}
    setSpacePath(spaceInfo.root);
    setSpaceSchemaVersion(spaceInfo.schema_version);
    await setCurrentSpacePath(spaceInfo.root); // Persist to settings
    ```
  </Step>
</Steps>

### Opening a Space

<Steps>
  <Step title="Validate schema version">
    Ensure space is compatible with app version

    ```rust theme={null}
    const CURRENT_SCHEMA_VERSION: u32 = 1;

    if space_schema.version != CURRENT_SCHEMA_VERSION {
      return Err(format!("Incompatible space version: {}", space_schema.version));
    }
    ```
  </Step>

  <Step title="Initialize SQLite index">
    Create or open `.glyph/index.db`

    ```rust src-tauri/src/index/db.rs theme={null}
    pub fn open_db(glyph_dir: &Path) -> Result<Connection, rusqlite::Error> {
      let db = Connection::open(glyph_dir.join("index.db"))?;
      schema::ensure_schema(&db)?; // Create FTS tables
      Ok(db)
    }
    ```
  </Step>

  <Step title="Start filesystem watcher">
    Monitor `notes/` directory for changes

    ```rust src-tauri/src/space/watcher.rs theme={null}
    let watcher = notify::recommended_watcher(move |event| {
      if let Ok(Event { kind: EventKind::Modify(_), paths, .. }) = event {
        for path in paths {
          indexer::reindex_file(&path)?;
        }
      }
    })?;

    watcher.watch(&space_root.join("notes"), RecursiveMode::Recursive)?;
    ```
  </Step>

  <Step title="Rebuild index">
    Scan all notes and populate SQLite FTS

    ```typescript src/contexts/SpaceContext.tsx theme={null}
    await invoke('index_rebuild'); // Async, non-blocking
    ```
  </Step>
</Steps>

### Closing a Space

```typescript src/contexts/SpaceContext.tsx theme={null}
const closeSpace = async () => {
  await invoke('space_close'); // Stop watcher, close DB
  await clearCurrentSpacePath(); // Clear from settings
  setSpacePath(null);
  setSpaceSchemaVersion(null);
};
```

## Space.json Schema

The `space.json` file marks a directory as a Glyph space and tracks schema version.

```json space.json theme={null}
{
  "version": 1
}
```

<ParamField path="version" type="number" required>
  Schema version. Current version is **1**.

  If this doesn't match `CURRENT_SCHEMA_VERSION` in Rust code, the space cannot be opened.
</ParamField>

## Content-Addressed Storage

Assets (images, PDFs, etc.) are stored by **SHA256 hash** to deduplicate files.

<Steps>
  <Step title="User attaches file">
    File is selected via file picker

    ```typescript theme={null}
    const result = await invoke('note_attach_file', {
      note_id: 'notes/example.md',
      source_path: '/Users/me/Downloads/diagram.png'
    });
    // Returns: { asset_rel_path: 'assets/a1b2...xyz.png', markdown: '![](../assets/a1b2...xyz.png)' }
    ```
  </Step>

  <Step title="Backend computes hash">
    SHA256 hash of file contents determines storage path

    ```rust src-tauri/src/notes/attachments.rs theme={null}
    let mut hasher = Sha256::new();
    io::copy(&mut file, &mut hasher)?;
    let hash = hex::encode(hasher.finalize());
    let asset_name = format!("{}.{}", hash, extension);
    ```
  </Step>

  <Step title="Copy if not exists">
    Only copy file if hash doesn't already exist

    ```rust theme={null}
    let dest = space_root.join("assets").join(&asset_name);
    if !dest.exists() {
      fs::copy(&source_path, &dest)?;
    }
    ```
  </Step>

  <Step title="Return markdown link">
    Generate relative markdown link

    ```rust theme={null}
    let rel_path = format!("../assets/{}", asset_name);
    Ok(AttachmentResult {
      asset_rel_path: format!("assets/{}", asset_name),
      markdown: format!("![]({})", rel_path)
    })
    ```
  </Step>
</Steps>

### Benefits

* **Deduplication** - Same image used in 10 notes = 1 file on disk
* **Integrity** - Hash mismatch = corrupted file
* **Immutability** - Content can't change without changing hash

## State Management

### Backend State (`src-tauri/src/space/state.rs`)

```rust theme={null}
use std::sync::Mutex;

pub struct SpaceState {
  pub current: Mutex<Option<CurrentSpace>>,
}

pub struct CurrentSpace {
  pub root: PathBuf,
  pub schema_version: u32,
  pub db: Connection,
  pub watcher: RecommendedWatcher,
}
```

Accessed via Tauri's state management:

```rust theme={null}
#[tauri::command]
fn space_get_current(state: State<SpaceState>) -> Result<Option<String>, String> {
  let current = state.current.lock().unwrap();
  Ok(current.as_ref().map(|c| c.root.display().to_string()))
}
```

### Frontend State (`src/contexts/SpaceContext.tsx`)

<CodeGroup>
  ```typescript State Shape theme={null}
  interface SpaceContextValue {
    spacePath: string | null;           // Current space root path
    spaceSchemaVersion: number | null;  // Schema version
    lastSpacePath: string | null;       // Last opened space (for "Continue")
    recentSpaces: string[];             // Recent space paths (max 20)
    isIndexing: boolean;                // Index rebuild in progress
    settingsLoaded: boolean;            // Settings loaded from disk
  }
  ```

  ```typescript Actions theme={null}
  // User-triggered actions
  onOpenSpace: () => Promise<void>;           // Show folder picker
  onOpenSpaceAtPath: (path: string) => Promise<void>; // Open specific path
  onContinueLastSpace: () => Promise<void>;   // Reopen last space
  onCreateSpace: () => Promise<void>;         // Create new space
  closeSpace: () => Promise<void>;            // Close current space
  startIndexRebuild: () => Promise<void>;     // Manual index rebuild
  ```
</CodeGroup>

## Recent Spaces

Glyph tracks up to 20 recently opened spaces, stored in Tauri's persistent store:

```typescript src/lib/settings.ts theme={null}
import { Store } from '@tauri-apps/plugin-store';

const store = new Store('settings.json');

export async function setCurrentSpacePath(path: string) {
  await store.set('currentSpacePath', path);
  
  // Update recent spaces
  const recent = (await store.get<string[]>('recentSpaces')) || [];
  const updated = [path, ...recent.filter(p => p !== path)].slice(0, 20);
  await store.set('recentSpaces', updated);
  await store.save();
}
```

## Path Safety

### Preventing Path Traversal

All user-provided paths are validated to prevent traversal attacks:

```rust src-tauri/src/paths.rs theme={null}
pub fn join_under(base: &Path, rel: &str) -> Result<PathBuf, String> {
  let normalized = PathBuf::from(rel)
    .components()
    .filter(|c| !matches!(c, Component::ParentDir))
    .collect::<PathBuf>();
  
  let joined = base.join(&normalized);
  
  if !joined.starts_with(base) {
    return Err("Path traversal detected".to_string());
  }
  
  Ok(joined)
}
```

Usage:

```rust src-tauri/src/space_fs/read_write/text.rs theme={null}
#[tauri::command]
pub fn space_read_text(
  path: String,
  state: State<SpaceState>
) -> Result<TextFileDoc, String> {
  let current = state.current.lock().unwrap();
  let space = current.as_ref().ok_or("No space open")?;
  
  // Safe path join - rejects "../../../etc/passwd"
  let abs_path = paths::join_under(&space.root, &path)?;
  
  let text = fs::read_to_string(abs_path)?;
  // ...
}
```

## Schema Migration

<Warning>
  Glyph uses a **hard cutover migration policy**. When schema version changes:

  * Old app versions **cannot** open new spaces
  * New app versions **cannot** open old spaces
  * Users must export data and re-import

  This is intentional to keep the codebase simple.
</Warning>

### Version Check

```rust src-tauri/src/space/commands.rs theme={null}
const CURRENT_SCHEMA_VERSION: u32 = 1;

pub fn space_open(path: String) -> Result<SpaceInfo, String> {
  let schema_path = PathBuf::from(&path).join("space.json");
  let schema: SpaceSchema = serde_json::from_str(&fs::read_to_string(schema_path)?)?;
  
  if schema.version != CURRENT_SCHEMA_VERSION {
    return Err(format!(
      "Space schema version {} is not compatible with app version {} (requires version {})",
      schema.version,
      env!("CARGO_PKG_VERSION"),
      CURRENT_SCHEMA_VERSION
    ));
  }
  
  // Continue with space open...
}
```
