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.
Entry Point
Context Providers
// src/main.tsx → App.tsx
< AppProviders >
< AppShell />
</ AppProviders >
Backend (src-tauri/src/)
The Rust backend handles all filesystem operations, indexing, and AI integration.
Core Modules
Safety Modules
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
IPC Layer
Communication between frontend and backend uses typed Tauri commands .
Define Rust command
Implement command in src-tauri/src/ module src-tauri/src/space/commands.rs
#[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
Search Flow
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:
SpaceContext
FileTreeContext
EditorContext
// Root-level: Space lifecycle
interface SpaceContextValue {
spacePath : string | null ;
spaceSchemaVersion : number | null ;
onOpenSpace : () => Promise < void >;
onCreateSpace : () => Promise < void >;
closeSpace : () => Promise < void >;
}
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
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:
src-tauri/src/io_atomic.rs
pub fn write_atomic ( path : & Path , contents : & [ u8 ]) -> io :: Result <()> {
// Write to temp → sync → rename → sync parent dir
}
Migration Policy
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: