Skip to content
Documentation GitHub
Platform

Frontend System

The frontend is a single-page React application rendered inside a Tauri webview. It follows a strict “dumb pipe” pattern: all business logic lives in the Rust backend. The React layer handles only UI/UX behavior, delegating every data operation to the backend via @tauri-apps/api/core invoke() calls.

This means:

  • No domain validation in TypeScript (validation happens in Rust use cases)
  • No data transformation beyond display formatting
  • No direct database or filesystem access
  • Supabase JS client used only for auth transport (token bridge), not for data queries

Scope: React application shell, component tree, state management, editor integration, IPC patterns

Dependencies: Tauri framework (IPC bridge), all backend systems via invoke() calls

The frontend operates as an outer Framework layer, analogous to src-tauri/ on the Rust side:

CA LayerFrontend Role
FrameworkReact components, Zustand stores, TipTap setup
Command (Rust)Accessed via invoke() IPC calls
ApplicationN/A — lives in Rust
DomainN/A — lives in Rust
InfrastructureN/A — lives in Rust
apps/desktop/src-react/
├── main.tsx # Entry point (React root, Sentry init, ErrorBoundary)
├── index.css # Global styles
├── app/ # Application shell
│ ├── App.tsx # Root component (workspace init, layout, dialog orchestration)
│ └── SyncProvider.tsx # Auth token bridge + sync lifecycle (React context)
├── stores/ # Zustand state management
│ ├── appStore.ts # Global UI state (navigation, layout, dialogs, toasts)
│ └── authStore.ts # Authentication state (user, token bridge)
├── hooks/ # Custom React hooks (IPC wrappers)
├── components/ # UI components organized by feature area
├── lib/ # Utilities and non-React modules
│ ├── errors.ts # parseCommandError() for Tauri structured errors
│ ├── logger.ts # Structured frontend logging
│ ├── loro-sync/ # TipTap ↔ Loro CRDT bridge
│ └── auth-client.ts # Supabase JS client (transport only)
└── styles/
└── tokens.css # Design token definitions (colors, spacing)

The primary UI store at stores/appStore.ts. Manages all cross-component state:

ConcernStateActions
NavigationselectedPageSlug, selectedPageIdselectPage(), setSelectedPageId()
LayoutsidebarWidth, contextPanelWidth, contextPanelOpensetSidebarWidth(), toggleContextPanel()
Refresh keyspageTreeKey, pageDetailKeyrefreshPageTree(), refreshPageDetail()
DialogsisCreatePageDialogOpen, deletePageSlug, movePageSlug, etc.openCreatePageDialog(), closeDeletePageDialog(), etc.
Toaststoasts[]addToast(), removeToast()
TrashisTrashViewOpen, trashRefreshKeyopenTrashView(), refreshTrash()
AttachmentsisAttachmentManagerOpenopenAttachmentManager()
SettingsisSettingsOpen, settingsSectionopenSettings(), closeSettings()
PropertieshighlightPropertyhighlightPropertyInPanel()

Layout state (sidebar/panel widths) is persisted to localStorage with debounced writes (200ms).

Refresh keys use an incrementing counter pattern: incrementing pageTreeKey causes any component subscribed to it to re-fetch data from the backend.

Authentication state at stores/authStore.ts. The token bridge pattern:

  1. On mount, initTokenBridge() fetches the current session from the Rust backend via invoke('get_auth_session')
  2. Session tokens are passed to the Supabase JS client via authClient.auth.setSession()
  3. Backend-initiated token refreshes are received via Tauri event listener (auth-token-refreshed)
  4. The Rust backend owns the session; the frontend is a mirror for Supabase JS client compatibility

hooks/useWorkspace.tsx provides workspace state via React context. On mount, it calls invoke('initialize_workspace') to load or create the default workspace. Components access it via useWorkspace().

app/SyncProvider.tsx wraps the app with sync lifecycle management:

  • Initializes the auth token bridge
  • Starts/stops the Rust sync engine based on workspace + auth state
  • Exposes isAuthenticated, isAuthLoading, and syncStatus via context

Components live in components/ organized by feature area:

components/
├── layout/ # App shell structure (AppShell, Sidebar)
├── editor/ # Editor and related components
│ ├── InklingsEditor.tsx # Main TipTap editor component
│ ├── Toolbar.tsx # Editor toolbar
│ ├── GridLayout.tsx # Multi-editor grid layout (per-cell LoroDoc)
│ ├── extensions/ # TipTap extensions (WikiLink, PropertyRef, ImageBlock, etc.)
│ ├── LayoutPicker.tsx # Layout selection UI
│ └── InsertBlockMenu.tsx # Block type insertion menu
├── context/ # Right-panel context components (backlinks, properties, graph)
├── tags/ # Tag management
├── settings/ # Settings panels
├── attachments/ # Attachment browser
├── auth/ # Auth UI (sign-in, sign-out)
├── sync/ # Sync status indicators
├── CommandPalette/ # Command palette (search + actions)
├── IconPicker/ # Icon selection (code-split, lazy-loaded)
├── FirstLaunch/ # Onboarding tour
└── [Dialog components] # Create, Delete, Move, Rename, Import dialogs

The editor is the most complex frontend subsystem. It uses TipTap (ProseMirror-based) with Loro CRDT for persistent undo/redo and collaborative editing.

The bridge between TipTap and Loro consists of three files:

  • LoroSyncExtension.ts — TipTap extension that registers the ProseMirror plugins
  • loroHelpers.ts — Utility functions for LoroDoc snapshot export/import
  • index.ts — Public API re-exports

The extension registers two ProseMirror plugins from loro-prosemirror:

  1. LoroSyncPlugin — Bidirectional sync: ProseMirror transactions update the LoroDoc, and LoroDoc changes update ProseMirror state
  2. LoroUndoPlugin — Loro-backed undo/redo that persists across sessions (replaces ProseMirror’s built-in history)
Page load:
Backend (LoroDoc BLOB) ──invoke──> InklingsEditor
├── Has snapshot? → Reuse existing LoroDoc (preserves CRDT history)
└── No snapshot? → Create fresh LoroDoc from markdown text
Content change:
ProseMirror transaction
→ LoroSyncPlugin updates LoroDoc
→ onContentChange(markdown, loroBytes)
→ Parent calls invoke('save_block_content', { markdown, loroBytes })
Page navigation:
React key={slug} forces full editor remount → clean LoroDoc per page

GridLayout.tsx renders a CSS Grid with one editor per cell. Each cell has its own independent LoroDoc — they never share a document. Cells load/save via get_block_content_snapshot_by_id / save_block_content_by_id.

ExtensionTypePurpose
WikiLinkInline node (atom)[[Display|slug]] with optional #heading
PropertyRefInline node (atom){{name:value}} property references
ImageBlockBlock nodeImage blocks with metadata
ImageBlockDropPluginDrag-drop and paste handling for images
AttachmentNodeInline nodeGeneric attachment references
AttachmentUploadPluginFile upload handling

Every data operation follows the same pattern:

import { invoke } from "@tauri-apps/api/core";
const result = await invoke<ReturnType>("command_name", { param1, param2 });

Error handling uses the structured CommandError type from the Rust backend:

import { parseCommandError } from "../lib/errors";
try {
await invoke("create_page", { title, parentSlug });
} catch (err) {
const message = parseCommandError(err);
addToast(message);
}

The Rust backend serializes errors as { type: "Validation", data: { message: "..." } }, which parseCommandError() handles for all variants (NoWorkspace, Validation, Internal, NotFound, Permission, Unauthenticated).

The app wraps components in a specific provider order:

<ErrorBoundary> — Top-level crash boundary
<WorkspaceProvider> — Workspace initialization + context
<TourProvider> — First-launch onboarding state
<SyncProvider> — Auth token bridge + sync lifecycle
<AppContent /> — Main application UI
</SyncProvider>
<TourOverlay /> — Onboarding overlay (outside SyncProvider)
<ToastContainer /> — Toast notifications (outside SyncProvider)
</TourProvider>
</WorkspaceProvider>
</ErrorBoundary>

Within a workspace, AppContent uses CommandPaletteProvider to scope keyboard shortcuts and palette state.

Two levels of error boundaries provide graceful degradation:

  1. ErrorBoundary (top-level) — Catches catastrophic failures, shows full-page error
  2. FeatureErrorBoundary (per-feature) — Wraps sidebar, editor, and context panel individually. A crash in one panel does not take down the others
  • Dumb pipe principle: The frontend contains zero business logic. This eliminates an entire class of consistency bugs between frontend and backend validation.
  • Zustand over Redux: Zustand provides simpler API with less boilerplate. Single store with action-grouped slices.
  • Refresh key pattern: Rather than complex cache invalidation, incrementing a key causes React to re-fetch. Simple and reliable.
  • Per-cell LoroDoc isolation: Grid layout cells each maintain independent CRDTs. Sharing a LoroDoc across cells would create cross-cell undo/redo interference.
  • Token bridge pattern: The Rust backend owns the auth session. The frontend mirrors it solely for Supabase JS client compatibility (Realtime WebSocket).

Was this page helpful?