This is a unified Electron application containing a Svelte-based UI and the integrated Flint note server. The project uses a single package structure for simplified development and building.
The app uses Automerge for local-first data storage with CRDT-based data structures:
- Entry point:
src/renderer/src/App.svelte - Data storage: Automerge with IndexedDB (
src/renderer/src/lib/automerge/) - State management: Unified state module in
state.svelte.ts
See docs/AUTOMERGE-MIGRATION.md for architecture details and history.
flint-ui/
├── package.json # Single package configuration
├── src/
│ ├── main/ # Electron main process
│ ├── preload/ # Electron preload scripts
│ ├── renderer/ # Svelte UI application
│ │ ├── index.html # Main HTML file
│ │ └── src/ # Svelte source code
│ │ ├── components/ # Svelte components
│ │ ├── services/ # API and service layers
│ │ ├── stores/ # Svelte stores
│ │ └── utils/ # Utility functions
│ └── server/ # Integrated note server
│ ├── api/ # Server API layer
│ ├── core/ # Core note logic
│ ├── database/ # Database management
│ ├── server/ # Server handlers
│ ├── types/ # Type definitions
│ └── utils/ # Server utilities
├── sync-server/ # Separate Bun project for cloud sync
│ ├── src/
│ │ ├── index.ts # Express server entry point
│ │ ├── db.ts # SQLite database (bun:sqlite)
│ │ ├── auth/ # Bluesky ATProto OAuth, sessions, invite codes
│ │ └── sync/ # Sync implementation
│ │ ├── lean-sync-server.ts # WebSocket sync (one-doc-at-a-time)
│ │ ├── doc-storage.ts # Document binary storage
│ │ ├── file-storage.ts # Binary file storage (PDFs, images, etc.)
│ │ ├── file-routes.ts # REST API for file upload/download
│ │ ├── vault-access.ts # Document access control
│ │ ├── document-registration.ts # Document registration API
│ │ └── diagnostics.ts # Server diagnostics endpoints
│ └── tests/
├── docs/ # Project documentation
└── [config files] # Build configs, TypeScript, etc.
npm run build- Build the complete applicationnpm run dev- Start development servernpm run lint- Run linter on all source codenpm run typecheck- Run TypeScript checkingnpm run format- Format code across all filesnpm run clean- Clean build artifactsnpm run test- Run tests in watch modenpm run test:run- Run tests once
docs/FLINT-OVERVIEW.md- Design philosophy and core conceptsdocs/ARCHITECTURE.md- Electron system architecture documentationdocs/FLINT-NOTE-API.md- Server API documentationdocs/LEAN-SYNC-SERVER.md- Lean sync server architecture and wire protocol
src/main/- Electron main process with AI and note servicessrc/preload/- Preload scripts for secure IPCsrc/renderer/- Svelte UI applicationsrc/server/- Integrated note server with API, database, and core logic
sync-server/- Separate Bun project (not part of the Electron build)- Runtime: Bun, deployed to Fly.io
- Auth: Bluesky ATProto OAuth with session cookies and invite codes
- DB: SQLite via
bun:sqlite(data/flint-sync.db) - Dev:
cd sync-server && bun run dev - Tests:
cd sync-server && bun test - See
docs/LEAN-SYNC-SERVER.mdfor full architecture details
Custom one-doc-at-a-time sync replaces automerge-repo's Repo on the server (~800MB → ~1MB peak for 2k notes). Speaks the same CBOR wire protocol as automerge-repo — client requires zero changes.
- Uses
Automerge.receiveSyncMessage()/generateSyncMessage()low-level API - Document cache with 30s TTL and WASM memory management (
Automerge.free()) - Per-user per-document locks for concurrent access safety
- Sync state memory cache (write-through to SQLite
sync_statestable) - Real-time fan-out: changes pushed to all other connections for the same user
- Server-initiated sync: after client's initial burst, server pushes docs the client hasn't synced
Content-addressed binary file storage for PDFs, EPUBs, images, webpages:
PUT /api/files/:fileType/:hash— Upload with SHA-256 hash verificationGET /api/files/:fileType/:hash— Download with immutable cachingGET /api/files/manifest/:vaultId— List files for a vault- Conversation JSON storage at
/api/files/conversation/:vaultId/:conversationId
vault_access— Maps users to vault document URLscontent_doc_access— Maps users to content document URLssync_states— Persisted Automerge sync states per (user, doc, peer)file_metadata— Content-addressed file registryconversation_metadata— Conversation file registrysessions,allowed_users,invite_codes— Auth tables
website/- Static website directory (deployed to Cloudflare Pages)index.html- Main landing page
-
use modern svelte 5 syntax
$state,$props,$derived,$derived.by- use
onclicketc. -- avoidon:click - events should be via props -- do not use
createEventDispatcher
-
when creating summaries of work being done put them in the
docs/directory -
when creating new ts files in the renderer prefer creating .svelte.ts files so they can use runes
-
avoid
anytype -
before running linting and typechecking after editing a bunch of files run
npm run formatto fix up formatting -
don't run the development server
-
we currently have users so make sure to handle backward compatibility or migration concerns
The project uses Vitest for testing with a structured approach:
- Tests are located in
tests/directory (separate fromsrc/) - Test files follow naming convention:
*.test.tsor*.spec.ts - Structure mirrors source code:
tests/server/api/,tests/server/core/, etc.
- Vitest with Node.js environment
- Global test functions available (
describe,it,expect,beforeEach,afterEach) - Isolated test environments with temporary directories and databases
TestApiSetupclass provides isolated test environments- Automatic cleanup of test data and temporary files
- Database testing with real SQLite instances in temporary locations
Test files have relaxed ESLint rules allowing:
anytypes for mocking and flexibility- Functions without explicit return types
- Unused variables for partial test implementations
- Non-null assertions and empty functions for test scenarios
npm run test- Interactive watch modenpm run test:run- Single run with coverage
CRITICAL: Always use $state.snapshot() when sending Svelte reactive objects through IPC
-
Svelte's
$stateobjects contain internal reactivity metadata that breaks structured cloning -
Before any
window.api?.someMethod(data)call, wrap reactive data:$state.snapshot(data) -
This applies to: stores, reactive variables, derived values, any Svelte runes
-
Error symptoms: "An object could not be cloned" when calling IPC methods
-
Standard pattern:
// ❌ WRONG - Direct state serialization fails await window.api?.saveData(this.reactiveState); // ✅ CORRECT - Use $state.snapshot for IPC const serializable = $state.snapshot(this.reactiveState); await window.api?.saveData(serializable);
CRITICAL: Always use clone() when assigning objects inside docHandle.change() blocks
-
Automerge documents use proxies internally. When you try to insert an object that already exists in an Automerge document into another location, you get:
RangeError: Cannot create a reference to an existing document object -
Use the
clone()andcloneIfObject()utilities from./utilsto create fresh plain objects -
Error symptoms: "Cannot create a reference to an existing document object" during state mutations
-
Standard pattern:
import { clone, cloneIfObject } from './utils'; // ❌ WRONG - Direct assignment may reference existing document objects docHandle.change((doc) => { doc.config = externalConfig; doc.items = externalItems; }); // ✅ CORRECT - Use clone() for objects/arrays docHandle.change((doc) => { doc.config = clone(externalConfig); doc.items = clone(externalItems); }); // ✅ CORRECT - Use cloneIfObject() in loops with mixed types for (const [key, value] of Object.entries(props)) { note.props[key] = cloneIfObject(value); } // ✅ OK - Spread for simple string arrays (primitives don't need cloning) doc.tags = [...externalTags];
-
when planning migrations or breaking changes don't use progressive rollout strategy since we don't have that capability yet
-
we we need to deal with breaking changes to the DB schema we have version aware migration code in the migration-manager
-
do not try to run the app (e.g. npm run dev). ask the user to run it if you need to check something
The app runs both as an Electron desktop app and as a web app (PWA). The web version has additional mobile/touch considerations:
src/renderer/src/lib/platform.svelte.ts-isWeb()detects web vs Electronsrc/renderer/src/stores/deviceState.svelte.ts- Reactive viewport detection:isMobile: < 768pxisTablet: 768px - 1024pxisDesktop: > 1024pxuseMobileLayout: Combines mobile detection for UI decisions
On mobile, the sidebar is a full-screen drawer that slides in from the left:
- Sidebar renders outside
app-layouton mobile (avoids CSS transform issues withposition: fixed) - Main content slides right to reveal sidebar underneath
- Edge swipe from left opens drawer (when closed)
- Selecting an item auto-closes drawer
Key files:
src/renderer/src/stores/sidebarState.svelte.ts-mobileDrawerOpenstatesrc/renderer/src/lib/gestures.svelte.ts- Touch swipe utilitiessrc/renderer/src/components/MainView.svelte- Mobile layout integration
- Long-press to drag: On mobile, drag-to-reorder requires 300ms long-press (allows normal scrolling)
- Prevent native menus: Use
-webkit-touch-callout: noneand document-levelcontextmenuprevention - No tap highlight: Use
-webkit-tap-highlight-color: transparent - Hover states: Wrap in
@media (hover: hover)to avoid sticky hover on touch - Active states: Prevent flash with
@media (hover: none) { .item:active { background: transparent } } - Always-visible controls: Show action buttons (like "...") always on touch devices
For notch/dynamic island support:
- Viewport:
<meta name="viewport" content="..., viewport-fit=cover"> - CSS variables:
--safe-area-top,--safe-area-bottom, etc. (defined in base.css) - Apply padding to full-screen elements:
padding-top: var(--safe-area-top, 0px) - Dynamic theme-color: Update
<meta name="theme-color">based on visible content
/* Only show hover on devices with hover capability */
@media (hover: hover) {
.item:hover {
background: var(--bg-hover);
}
}
/* Prevent :active flash on touch */
@media (hover: none) {
.item:active {
background: transparent;
}
}
/* Always show controls on touch devices */
@media (hover: none) {
.action-button {
display: flex;
}
}src/renderer/src/components/MobileFAB.svelte- Context-sensitive floating action buttonsrc/renderer/src/components/MobileDrawer.svelte- Drawer wrapper componentsrc/renderer/src/stores/deviceState.svelte.ts- Device/viewport detection