Get Glyph
Warning This documentation is still a work in progress. Some details may be out of date depending on the version of Glyph you are using, but it is being actively reviewed and improved.
Documentation AI Assistant Development Licensing

Documentation

Architecture Overview

High-level architecture of Glyph

Overview

Glyph is an offline-first desktop note-taking app built with a hybrid architecture:

  • Frontend: React 19 + TypeScript + Vite + Tailwind 4
  • Backend: Tauri 2 + Rust
  • Editor: TipTap (Markdown)
  • AI: Rig-backed multi-provider chat
  • UI: shadcn/ui + Radix UI + Motion
  • Storage: SQLite index + filesystem

Application Structure

Frontend (src/)

The frontend is a single-page React application with a context-based state management architecture.

// src/main.tsx → App.tsx
<AppProviders>
  <AppShell />
</AppProviders>
// All state managed via React Context
- SpaceContext      // Space path & lifecycle
- FileTreeContext   // Files, tags, active file
- ViewContext       // Active view document
- UIContext         // Sidebar, search, preview state
- EditorContext     // TipTap editor instance

Backend (src-tauri/src/)

The Rust backend handles all filesystem operations, indexing, and AI integration.

lib.rs / main.rs      → Tauri setup, command registration
space/Space lifecycle (open/close/create)
space_fs/Filesystem operations
notes/Note CRUD, frontmatter parsing
index/SQLite FTS + tag indexing
ai_rig/Multi-provider AI runtime
ai_codex/Codex OAuth integration
links/Link preview fetching
database/Database view queries
paths.rs              → join_under() prevents traversal
io_atomic.rs          → Crash-safe atomic writes
net.rs                → SSRF prevention
glyph_paths.rs        → .glyph/ directory helpers

IPC Layer

Communication between frontend and backend uses typed Tauri commands.

Define Rust command

Implement command in src-tauri/src/ module

#[tauri::command]
pub fn space_open(path: String, state: State<SpaceState>) -> Result<SpaceInfo, String> {
  // Implementation
}

Register in lib.rs

Add to Tauri builder’s invoke handler

.invoke_handler(tauri::generate_handler![
  space_open,
  space_close,
  // ...
])

Add TypeScript types

Define in TauriCommands interface

interface TauriCommands {
  space_open: CommandDef<{ path: string }, SpaceInfo>;
  // ...
}

Invoke from frontend

Always use typed invoke() wrapper

import { invoke } from '@/lib/tauri';

const spaceInfo = await invoke('space_open', { path: '/path/to/space' });

Data Flow

File Operations

sequenceDiagram
    participant UI as React Component
    participant CTX as Context
    participant IPC as Tauri IPC
    participant FS as Rust Backend
    participant DB as SQLite Index
    
    UI->>CTX: User opens file
    CTX->>IPC: invoke('space_read_text')
    IPC->>FS: Read from filesystem
    FS->>IPC: File content + etag
    IPC->>CTX: Update active file
    CTX->>UI: Render editor
    FS->>DB: Update index (async)

Search Flow

sequenceDiagram
    participant UI as Search Input
    participant IPC as Tauri IPC
    participant IDX as SQLite FTS
    participant RANK as Hybrid Ranker
    
    UI->>IPC: invoke('search', { query })
    IPC->>IDX: Full-text search
    IDX->>RANK: Raw results
    RANK->>IPC: Scored + ranked
    IPC->>UI: Display results

State Management

React Context Architecture

Glyph uses no global state library (no Redux/Zustand). All state is managed via React Context with the following hierarchy:

// Root-level: Space lifecycle
interface SpaceContextValue {
  spacePath: string | null;
  spaceSchemaVersion: number | null;
  onOpenSpace: () => Promise<void>;
  onCreateSpace: () => Promise<void>;
  closeSpace: () => Promise<void>;
}
// File browser state
interface FileTreeContextValue {
  rootEntries: FsEntry[];
  childrenByDir: Record<string, FsEntry[]>;
  expandedDirs: Set<string>;
  activeFilePath: string | null;
  tags: TagCount[];
}
// TipTap editor instance
interface EditorContextValue {
  editor: Editor | null;
  isEditing: boolean;
  saveState: 'saved' | 'saving' | 'unsaved';
}

File System Layout

Each space is a directory containing:

my-space/
├── notes/                # Markdown files with YAML frontmatter
│   └── example.md
├── assets/               # Content-addressed files (SHA256)
│   └── abc123...def.png
├── cache/                # Link previews, thumbnails
│   ├── links/
│   └── images/
├── .glyph/               # App metadata (not in space root)
│   ├── index.db          # SQLite FTS + tags
│   ├── ai_history.db     # Chat history
│   └── profiles.json     # AI provider configs
└── space.json            # Schema version

Note

The .glyph/ folder stores derived data and can be safely deleted. It will be regenerated on next space open.

Build & Bundle

Development

  • pnpm dev - Vite dev server (frontend only)
  • pnpm tauri dev - Full Tauri app with hot reload

Production

  • pnpm build - TypeScript check + Vite build
  • pnpm tauri build - Create native app bundle
    • macOS: .dmg + .app
    • Windows: .msi + .exe
    • Linux: .deb + .AppImage

Security Architecture

Path Traversal Prevention

All space-relative paths are validated using paths::join_under():

pub fn join_under(base: &Path, rel: &str) -> Result<PathBuf, String> {
  // Rejects ".." components to prevent traversal attacks
}

SSRF Prevention

User-supplied URLs (link previews) are checked before fetching:

pub fn check_user_url(url: &str, allow_private: bool) -> Result<(), String> {
  // Blocks private IPs unless explicitly allowed
}

Atomic Writes

All file writes use crash-safe atomic operations:

pub fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> {
  // Write to temp → sync → rename → sync parent dir
}

Migration Policy

Warning

Glyph uses a hard cutover migration approach. When the space schema changes, old versions cannot open new spaces. Never implement backward compatibility.

Version is stored in space.json:

{
  "version": 1
}