Skip to main content

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
Spaces are portable. You can move a space folder anywhere, sync via Dropbox/iCloud, or manage it with Git.

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
The .glyph/ folder is derived data. It can be safely deleted and will regenerate on next space open. Do not sync it.

Space Lifecycle

Creating a Space

1

User selects directory

User chooses an empty or existing folder via native file picker
src/contexts/SpaceContext.tsx
const onCreateSpace = async () => {
  const { open } = await import('@tauri-apps/plugin-dialog');
  const selection = await open({ directory: true });
  await invoke('space_create', { path: selection });
};
2

Backend initializes structure

Rust backend creates directories and schema marker
src-tauri/src/space/commands.rs
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 })
}
3

Frontend updates state

SpaceContext tracks current space path
setSpacePath(spaceInfo.root);
setSpaceSchemaVersion(spaceInfo.schema_version);
await setCurrentSpacePath(spaceInfo.root); // Persist to settings

Opening a Space

1

Validate schema version

Ensure space is compatible with app version
const CURRENT_SCHEMA_VERSION: u32 = 1;

if space_schema.version != CURRENT_SCHEMA_VERSION {
  return Err(format!("Incompatible space version: {}", space_schema.version));
}
2

Initialize SQLite index

Create or open .glyph/index.db
src-tauri/src/index/db.rs
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)
}
3

Start filesystem watcher

Monitor notes/ directory for changes
src-tauri/src/space/watcher.rs
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)?;
4

Rebuild index

Scan all notes and populate SQLite FTS
src/contexts/SpaceContext.tsx
await invoke('index_rebuild'); // Async, non-blocking

Closing a Space

src/contexts/SpaceContext.tsx
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.
space.json
{
  "version": 1
}
version
number
required
Schema version. Current version is 1.If this doesn’t match CURRENT_SCHEMA_VERSION in Rust code, the space cannot be opened.

Content-Addressed Storage

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

User attaches file

File is selected via file picker
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)' }
2

Backend computes hash

SHA256 hash of file contents determines storage path
src-tauri/src/notes/attachments.rs
let mut hasher = Sha256::new();
io::copy(&mut file, &mut hasher)?;
let hash = hex::encode(hasher.finalize());
let asset_name = format!("{}.{}", hash, extension);
3

Copy if not exists

Only copy file if hash doesn’t already exist
let dest = space_root.join("assets").join(&asset_name);
if !dest.exists() {
  fs::copy(&source_path, &dest)?;
}
4

Return markdown link

Generate relative markdown link
let rel_path = format!("../assets/{}", asset_name);
Ok(AttachmentResult {
  asset_rel_path: format!("assets/{}", asset_name),
  markdown: format!("![]({})", rel_path)
})

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)

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:
#[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)

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
}

Recent Spaces

Glyph tracks up to 20 recently opened spaces, stored in Tauri’s persistent store:
src/lib/settings.ts
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:
src-tauri/src/paths.rs
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:
src-tauri/src/space_fs/read_write/text.rs
#[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

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.

Version Check

src-tauri/src/space/commands.rs
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...
}