Skip to content

2.1 Build and Deployment Guide

Frédéric Clavert edited this page Jul 26, 2026 · 16 revisions

This document explains how to compile, package, and deploy ClioDeck for different platforms.

Table of Contents


Technical Stack

Frontend

  • Electron 40 - Multi-platform desktop
  • React 18 - UI components
  • TypeScript 5 - Type safety
  • CodeMirror 6 - Markdown editor with live rendering
  • Zustand - State management
  • Vite - Build tool

Backend

  • Node.js 20+ - JavaScript runtime
  • better-sqlite3 - SQLite database (vector store)
  • pdfjs-dist - PDF extraction
  • electron-store - Config persistence
  • Python 3.11+ - Analysis services (topic modeling)

LLM & AI

  • A typed provider registry — Ollama, embedded (node-llama-cpp), Anthropic, OpenAI-compatible, Mistral, Gemini — for both generation and embeddings, selected independently
    • Ollama embedding model: nomic-embed-text (768 dimensions) or mxbai-embed-large (not an automatic fallback — an independent choice)
    • Ollama chat model: gemma2:2b (fast, multilingual, but not tool-capable) or any chat model Ollama can run
    • Embedded (no Ollama needed): Qwen2.5-0.5B/1.5B for generation, Nomic Embed Text v2 MoE for embeddings — a full offline RAG workflow needs neither Ollama nor a cloud key
  • BERTopic - Topic modeling and clustering (Python)

Prerequisites

Development

  • Node.js 20+ and npm 10+
  • Python 3.11+ (for better-sqlite3 and Python services)
  • Ollama installed locally for testing (or the embedded model, or a cloud provider — see Embedded LLM Guide)

Platform-Specific

Linux:

sudo apt-get install build-essential python3-dev

macOS:

xcode-select --install

Windows:

  • Visual Studio Build Tools or Visual Studio Community
  • Python 3.11+ with pip

Installation

Installing Dependencies

npm install

This command installs all Node.js dependencies and automatically compiles native modules (better-sqlite3, hnswlib-node, etc.).

Installing Python Dependencies (Topic Modeling)

Python services are used for topic modeling. For the development environment:

cd backend/python-services/topic-modeling
python3 -m venv .venv
source .venv/bin/activate  # Linux/macOS
# Or: .venv\Scripts\activate  # Windows

pip install -r requirements.txt

Development

Development Mode with Hot Reload

npm run dev

This command launches in parallel:

  • Main process TypeScript in watch mode
  • Preload script in watch mode
  • Renderer (React) with Vite hot reload

Start the Application

In another terminal:

npm start

Full Development Mode (All-in-One)

npm run dev:full

Launches build watch AND the application automatically after 3 seconds.

Tests

# Bare `vitest` — in CI (non-interactive) this runs once and exits, but in
# an interactive terminal it enters watch mode, same as npm run test:watch
npm test

# Explicit watch mode
npm run test:watch

# With UI
npm run test:ui

# With coverage
npm run test:coverage

To run the suite once from an interactive terminal, use npx vitest run (see Common how-to in CLAUDE.md) — plain npm test will not exit on its own there.

Linting and Type Checking

# Linter
npm run lint

# Type checking
npm run typecheck

⚠️ npm run lint is currently broken, not just unconfigured-by-choice: the script runs eslint . --ext .ts,.tsx, but there is no ESLint configuration file anywhere in the repo (no .eslintrc*, no eslint.config.*) — running it failed immediately with "ESLint couldn't find a configuration file", and CI deliberately left lint out for that reason. Fixed in RC3's follow-up work: a configuration exists, npm run lint works, and CI runs it. The bar is zero errors; warnings are a known stock.

npm run typecheck also covers less than what CI actually runs: the script itself is just tsc --noEmit against the root tsconfig.json. CI additionally runs npx tsc -p tsconfig.node.json --noEmit and npx tsc -p tsconfig.preload.json --noEmit as separate steps — the main and preload processes are separate composite TypeScript projects with their own configs. Running only npm run typecheck locally will not catch a type error that's only visible from one of those two other configs; run all three commands to match what CI actually checks.


Build and Packaging

Build Without Packaging

npm run build

Compiles:

  • Main process TypeScript → dist/src/main/
  • Preload script → dist/src/preload/
  • Renderer React → dist/src/renderer/

Build with Packaging

All Platforms

npm run build:all

Despite the name, this runs electron-builder with no --linux/--mac/--win flag, which only builds for the current host platform — not literally all three at once. Cross-building for other platforms needs the Docker/Wine approach described under Cross-Platform Packaging below, and macOS/Windows targets can't be cross-built from Linux without it.

Specific Platforms

# Linux (AppImage + deb)
npm run build:linux

# macOS (DMG for Intel and Apple Silicon)
npm run build:mac

# Windows (NSIS installer)
npm run build:win

macOS Build by Architecture

# Intel only
npm run build:mac-intel

# Apple Silicon only
npm run build:mac-arm

# Universal (Intel + Apple Silicon)
npm run build:mac-universal

Build Without Installer (for Testing)

npm run build:dir

Creates an unpackaged executable folder in release/.

Build Structure

cliodeck-app/
├── dist/                    # Compiled code
│   ├── src/
│   │   ├── main/           # Main process JS
│   │   ├── preload/        # Preload script JS
│   │   └── renderer/       # React build
├── release/                # Installers, named after the current package.json version
│   ├── ClioDeck-<version>.AppImage         # Linux — see the note on names below
│   ├── ClioDeck-<version>.dmg              # macOS
│   └── cliodeck_<version>_amd64.deb        # Linux
└── build/                  # Packaging assets
    ├── icon.png
    ├── icon.icns
    └── icon.ico

No Windows entry: npm run build:win exists but no Windows build has ever been published (untested — see User Installation below).

A note on those names — they are not fixed. electron-builder only writes the architecture into the filename when it is not the default (x64). Build on an x86_64 machine and you get ClioDeck-<version>.AppImage; build the same commit on an Apple Silicon Mac and you get ClioDeck-<version>-arm64.AppImage. The linux target in package.json declares no architecture, so it silently inherits the build machine's.

This is how the rc.4 release first shipped Linux binaries that were arm64 only, built on a Mac, while the file names gave nothing away. Never infer an architecture from a filename — read the ELF header:

readelf -h ClioDeck.AppImage | grep Machine     # AppImages are ELF executables
dpkg-deb --field cliodeck.deb Architecture      # what dpkg will check

.github/workflows/build-linux-x64.yml builds the x86_64 artefacts on a ubuntu-latest runner (manual trigger) and runs exactly these checks before anything is attached to a release.

Cross-Platform Packaging

Linux → all OS: Possible with Docker:

docker run --rm -v $(pwd):/project electronuserland/builder:wine \
  bash -c "cd /project && npm install && npm run build:all"

macOS → macOS/Linux/Windows: macOS can build for all platforms natively.

Windows → Windows only: Windows can only build for Windows.


User Installation

User Prerequisites

  1. Ollama installed on the machine
  2. Models downloaded:
    ollama pull nomic-embed-text
    ollama pull gemma2:2b

Linux

AppImage (recommended):

# Asset names carry a platform prefix since rc.4 (`Linux.AppImage.-.`,
# `Mac.Silicon.-.` …). Building a URL by bumping the version number alone
# returns 404 — check the Releases page for the exact name.
# x86_64 below; on arm64 replace `-x86_64` with `-arm64` (`uname -m` tells you).
wget https://github.com/cliodeck/cliodeck-app/releases/download/v1.0.0-rc.4/Linux.AppImage.-.ClioDeck-1.0.0-rc.4-x86_64.AppImage -O ClioDeck.AppImage

# Make executable
chmod +x ClioDeck.AppImage

# Launch
./ClioDeck.AppImage

Debian/Ubuntu (.deb):

# Debian calls x86_64 `amd64`; on arm64 the file is `..._arm64.deb`.
sudo dpkg -i Linux.Debian.-.cliodeck_1.0.0-rc.4_amd64.deb
sudo apt-get install -f  # Fix dependencies if necessary
cliodeck

macOS

  1. Download the DMG file from GitHub Releases
  2. Double-click to mount the disk image
  3. Drag ClioDeck to the Applications folder
  4. Launch from Launchpad or Applications

First launch: If macOS displays "app cannot be opened because it is from an unidentified developer":

xattr -cr /Applications/ClioDeck.app

Or: Right-click → Open → Confirm

Windows

No Windows build is currently published. npm run build:win exists and the code should work on Windows, but this is untested — there is no ClioDeck-Setup-*.exe on the Releases page as of v1.0.0-rc.4. Build from source (see Build and Packaging) if you want to try it.


Initial Configuration

1. Verify Ollama

The first time you open Settings, ClioDeck checks the Ollama connection — this fires when the Settings panel mounts (ConfigPanel.tsx), not automatically at app launch.

If Ollama is not detected:

  1. Install Ollama: https://ollama.ai/download

  2. Start the service:

    # Linux/macOS
    ollama serve
    
    # Windows: Ollama starts automatically as a service
  3. Verify the service is working:

    curl http://localhost:11434/api/tags

2. Download Models

# Embedding model (REQUIRED)
ollama pull nomic-embed-text

# Chat model (RECOMMENDED)
ollama pull gemma2:2b

# Chat alternatives
ollama pull mistral:7b-instruct    # More accurate but heavier

# Tool-capable alternatives (needed if you want Brainstorm to search your
# corpus on its own — gemma2:2b and the Llama 3.x/4.x families cannot):
ollama pull qwen3:8b

3. Zotero Configuration (optional)

To sync with Zotero:

  1. Get an API key:

  2. Configure in ClioDeck:

    • Settings → Zotero Integration
    • User ID: your Zotero user ID (visible in your library URL)
    • API Key: paste the key
    • Test Connection to verify
  3. Sync:

    • Select a Zotero collection
    • Click "Sync"
    • Wait for PDFs and BibTeX file download

Common Issues

Development

"Incompatible architecture" error on macOS

Symptom:

mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64')

Cause: Native modules (better-sqlite3, hnswlib-node) are compiled for the wrong architecture.

Solution:

# Rebuild for your architecture
npm run rebuild:native

# Or specifically:
npm run rebuild:x64      # Intel
npm run rebuild:arm64    # Apple Silicon

npm run rebuild:native runs electron-builder install-app-deps, which handles this — there is no separate after-pack script in the repo.

better-sqlite3 doesn't compile

npm rebuild better-sqlite3 --build-from-source

If the issue persists, verify that Python and build tools are installed.

hnswlib-node doesn't compile

# Install C++ build tools
# macOS:
xcode-select --install

# Linux:
sudo apt-get install build-essential

# Then:
npm rebuild hnswlib-node --build-from-source

Python topic modeling service doesn't start

Port already in use:

# Kill process on port 8001
lsof -ti:8001 | xargs kill -9

Missing Python dependencies:

cd backend/python-services/topic-modeling
source .venv/bin/activate
pip install -r requirements.txt

Production

Ollama doesn't start

Linux:

# Check status
systemctl status ollama

# Start manually
ollama serve

macOS:

# Check if process is running
ps aux | grep ollama

# Start manually
ollama serve

Windows:

# Open services.msc
# Check "Ollama" service

Slow embeddings

  1. Use CPU optimized config:

    • Settings → RAG → Chunking: CPU Optimized
  2. Reduce topK:

    • Settings → RAG → Top K: 5 (instead of 10)
  3. Use a lighter model:

    ollama pull gemma2:2b  # Instead of mistral:7b

Chat doesn't respond

  1. Check Ollama:

    curl http://localhost:11434/api/tags
  2. Verify chat model is installed:

    ollama list
  3. Change model:

    • Settings → LLM → Chat Model
    • Select an installed model

Poorly indexed PDFs

Empty extracted text:

  • The PDF is image-only (no selectable text)
  • Solution: Use external OCR then reimport

Poor extraction quality:

  • The PDF is poorly formatted or corrupted
  • Check the PDF in an external reader

Zotero sync fails

  1. Verify API key is valid
  2. Verify User ID is correct
  3. Check internet connection
  4. Check logs for more details

Debug Logs

ClioDeck does not currently write logs to a file — output goes to the console only. Launch the app from a terminal (or npm start in development) to see it. See the Logging System page for the three logging mechanisms (global console filter, renderer logger, main-process logger) and their env-var behavior.


Distribution and Releases

Code Signing (Production)

macOS

  1. Get an Apple Developer certificate
  2. Configure in package.json:
{
  "build": {
    "mac": {
      "identity": "Developer ID Application: Your Name (TEAM_ID)"
    }
  }
}
  1. Build with signing:
CSC_NAME="Developer ID Application" npm run build:mac

Windows

  1. Get a code signing certificate
  2. Configure and build:
set CSC_LINK=path/to/cert.pfx
set CSC_KEY_PASSWORD=your_password
npm run build:win

GitHub Releases

  1. Create a version tag:
git tag v1.0.0
git push origin v1.0.0
  1. Build and publish:
GH_TOKEN=your_github_token npm run build:all

electron-builder can automatically publish to GitHub Releases if a publish block is configured — package.json does not currently have one; releases are attached to tags manually today. If you add one, point it at cliodeck/cliodeck-app (the current repository — the project moved from inactinique/cliodeck, now archived).

Environment Variables

Build:

  • CSC_LINK: Path to signing certificate
  • CSC_KEY_PASSWORD: Certificate password
  • GH_TOKEN: GitHub token for releases
  • DEBUG: Enable electron-builder debug logs

Runtime:

  • NODE_ENV: development or production
  • OLLAMA_HOST: Ollama URL (default: http://localhost:11434)
  • CLIODESK_DEBUG, CLIODESK_LOG_LEVEL: toggle the global raw console.* filter (src/shared/console-filter.ts). In practice this is only observable in the main process console — the packaged renderer runs fully sandboxed (contextIsolation: true, nodeIntegration: false, sandbox: true), so it has no process global at all, meaning this file's own production check can never fire there; separately, Vite's esbuild.pure setting already strips console.log/info/debug calls out of the production renderer bundle regardless. It does not affect the renderer's own structured logger. See Logging System

Performance and Optimization

Recommended Hardware Configuration

Minimum:

  • CPU: Dual-core
  • RAM: 4 GB
  • Disk: 5 GB free

Recommended:

  • CPU: Quad-core
  • RAM: 8 GB
  • Disk: 10 GB free

Optimal:

  • CPU: 8+ cores
  • RAM: 16 GB
  • Disk: 20 GB free (for Ollama models)

Installer Sizes

As of v1.0.0-rc.4 (measured on the published assets):

  • Linux AppImage: 570 MB (x86_64), 262 MB (arm64)
  • Linux deb: 340 MB (x86_64), 158 MB (arm64)
  • macOS DMG: 263 MB (Apple Silicon), 268 MB (Intel)
  • Windows: not built (no release published)

Why x86_64 is more than twice the size. Not a packaging fault — the same ratio shows up in both formats, so it is the payload. node-llama-cpp ships its GPU backends as per-platform optional dependencies: on macOS arm64 npm installs only mac-arm64-metal (5 MB), while on linux-x64 it pulls linux-x64, linux-x64-cuda, linux-x64-cuda-ext and linux-x64-vulkan. The CUDA ones account for most of the difference.

They are not dead weight: EmbeddedLLMClient calls getLlama() with no argument, so backend detection is automatic and a user with an NVIDIA card gets the acceleration. But every x86_64 user pays the download for it. Excluding the CUDA variants through the files field would roughly halve the artefact at the cost of that acceleration — a trade-off, not an obvious win.

Disk Space per Project

  • App: ~200 MB
  • Ollama models: ~500 MB - 5 GB (depending on models)
  • Vector database: 50-500 MB per project (depending on PDF count)
  • Research journal: 1-5 MB per session, 50-200 MB for a long project

Security and Privacy

Local Data

All data remains local:

  • PDFs and documents: stored in project folder
  • Embeddings and indexes: local SQLite (.cliodeck/brain.db)
  • LLM and models: local Ollama, the embedded model, or whichever cloud provider you configure (see Technical Stack above)

No data is sent to external servers by default (local Ollama or the embedded model, plus local storage) — but several optional, user-configured features do send data out, and this line's previous "except Zotero" wording undersold that, directly contradicting the "whichever cloud provider you configure" bullet just above:

  • Zotero sync — bibliography metadata and PDFs, if configured.
  • A cloud LLM provider (Anthropic, OpenAI-compatible, Mistral, Gemini) — if you pick one instead of Ollama/embedded, your prompts and retrieved context go to that provider's API.
  • Archive connectors (Gallica, HAL, Europeana) — any query you run through these MCP tools goes to the respective external API; Europeana additionally requires an API key.

API Key Storage

All sensitive keys (src/main/services/secure-storage.ts) — Zotero API key, Europeana API key, and every cloud LLM provider key (Anthropic, OpenAI, Mistral, Gemini) — go through the same mechanism: Electron's safeStorage API encrypts the value, and a separate electron-store instance persists the encrypted bytes. safeStorage itself is backed by:

  • Linux: GNOME Keyring / KWallet
  • macOS: Keychain
  • Windows: Credential Manager (DPAPI)

⚠️ Undocumented fallback: if safeStorage.isEncryptionAvailable() returns false — e.g. on a Linux setup with no keyring service running — secure-storage.ts silently stores every one of those keys in plain text instead, logging only a console.warn to the main-process console. Nothing surfaces this in the UI, so a user on a keyring-less Linux install would have no way to know their API keys aren't encrypted unless they launched ClioDeck from a terminal and read the log.


Useful Scripts

# Full cleanup
npm run clean

# Reinstall all dependencies
rm -rf node_modules package-lock.json
npm install

# Rebuild native modules
npm run rebuild:native

# Check types
npm run typecheck

# Linter
npm run lint

# Build preview
npm run preview

Resources


Note for daily development: Simply use npm run dev in one terminal and npm start in another.

Clone this wiki locally