Skip to main content

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>

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

IPC Layer

Communication between frontend and backend uses typed Tauri commands.
1

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
}
2

Register in lib.rs

Add to Tauri builder’s invoke handler
src-tauri/src/lib.rs
.invoke_handler(tauri::generate_handler![
  space_open,
  space_close,
  // ...
])
3

Add TypeScript types

Define in TauriCommands interface
src/lib/tauri.ts
interface TauriCommands {
  space_open: CommandDef<{ path: string }, SpaceInfo>;
  // ...
}
4

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:
// 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():
src-tauri/src/paths.rs
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:
src-tauri/src/net.rs
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:
{
  "version": 1
}