This guide walks through setting up Emailibrium for local development.
Run just setup for an interactive wizard that automates these steps.
| Tool | Minimum Version | Install Command |
|---|---|---|
| Rust | 1.97 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Node.js | 26 (LTS) | brew install node@26 or nodejs.org |
| pnpm | 11.5 | npm install -g pnpm@11 |
| Docker | 24.0+ | docs.docker.com/get-docker |
| Docker Compose | v2 | Included with Docker Desktop |
| just | 1.x | brew install just or just.systems |
Check all prerequisites at once:
just setup-prereqsThe ruvector/ submodule must be initialized:
git submodule update --init --recursiveSecrets live in secrets/dev/ (gitignored). The setup script auto-generates
cryptographic secrets and prompts for OAuth credentials.
just setup-secretsThese are created automatically using openssl rand -base64 32:
jwt_secret-- Signs JWT authentication tokensoauth_encryption_key-- Encrypts OAuth tokens at restdb_password-- PostgreSQL password (Docker dev environment)database_url-- PostgreSQL connection string for Docker
OAuth requires registering apps with Google and Microsoft.
- Go to Google Cloud Console > Credentials
- Create a project (or select an existing one)
- Click Create Credentials > OAuth client ID
- Application type: Web application
- Add authorized redirect URI:
http://localhost:8080/api/v1/auth/callback - Copy the Client ID and Client Secret
- Go to Azure App Registrations
- Click New registration
- Name:
Emailibrium Dev - Supported account types: Accounts in any organizational directory and personal Microsoft accounts
- Redirect URI: Web >
http://localhost:8080/api/v1/auth/callback - Under Certificates & secrets, create a new Client secret
- Copy the Application (client) ID from the Overview page and the secret value
OAuth is optional for initial development. You can skip it and configure later.
Emailibrium uses AI for email classification and smart features. It works out of the box with zero configuration.
The default configuration uses:
- Embedding: ONNX Runtime (
all-MiniLM-L6-v2) — runs locally, downloads ~90 MB on first use - Classification: Built-in LLM (
qwen3-1.7b-q4km) — runs locally, downloads ~1.1 GB on first use
No API keys, no external services, no data leaves your machine.
To avoid the first-use download delay:
just download-modelsOr download individually:
# ONNX embedding model (~90 MB)
cd backend && cargo run -- --download-models
# GGUF LLM model (~1.1 GB)
npx tsx scripts/models.ts download --defaultjust diagnoseShows embedding status, LLM model status, Ollama availability, and cloud API keys.
| Want | Set | Notes |
|---|---|---|
| No AI (fastest) | EMAILIBRIUM_GENERATIVE_PROVIDER=none |
Rule-based only |
| Ollama (larger models) | EMAILIBRIUM_GENERATIVE_PROVIDER=ollama |
Requires ollama serve |
| Cloud (GPT-4o, Claude) | EMAILIBRIUM_GENERATIVE_PROVIDER=cloud |
Requires API key |
See Configuration Reference for all options.
Emailibrium supports a tiered AI architecture. Configure providers with:
just setup-ai- Runs fully offline, no API key needed
- Models download automatically on first backend start
- Pre-download with:
emailibrium --download-models - Default model:
all-MiniLM-L6-v2(384-dimension embeddings)
- Install from ollama.com
- Start the server:
ollama serve - Pull a model:
ollama pull llama3.2
API keys are stored in .env.local (gitignored). What each variable actually feeds:
| Provider | Environment Variable | Used for | Get a Key |
|---|---|---|---|
| OpenAI | EMAILIBRIUM_OPENAI_API_KEY |
Cloud embeddings (embedding.provider: cloud) |
platform.openai.com |
| Cohere | EMAILIBRIUM_COHERE_API_KEY |
Cloud embeddings (embedding.provider: cohere) |
dashboard.cohere.com |
| Gemini | EMAILIBRIUM_GEMINI_API_KEY |
Cloud chat/classification (generative.cloud.provider: gemini) |
aistudio.google.com |
For chat/classification with OpenAI or Anthropic, the key variable is
EMAILIBRIUM_CLOUD_API_KEY (set generative.cloud.provider to openai or anthropic) —
there is no dedicated EMAILIBRIUM_ANTHROPIC_API_KEY; Gemini is the only cloud generative
provider with its own variable. See the
Configuration Reference for the
full generative.cloud.* settings.
Choose between Docker and native development.
just setup-docker # Build images, optionally start services
just docker-up-dev # Start with hot-reload
just docker-logs # Tail logs
just docker-down # StopDocker Compose starts: PostgreSQL, Redis, backend (Rust), frontend (React).
just install # Install all dependencies
just dev # Start backend + frontend dev serversNative dev uses SQLite by default (configured in config/environments/config.development.yaml).
Run all validation checks:
just setup-validateThis checks: secrets, backend compilation, frontend build, Docker health, API reachability, and AI model availability.
The backend embeds an MCP server exposing 15 read-only tools over your mailbox. It runs in two modes.
The backend serves MCP at http://localhost:8080/api/v1/mcp whenever it is running. Nothing
extra to start. For Claude Code, add an entry to .mcp.json at the repo root:
{
"mcpServers": {
"emailibrium": {
"type": "http",
"url": "http://localhost:8080/api/v1/mcp"
}
}
}This repo already ships that entry, so Claude Code picks it up once the backend is running. Because the backend is a long-running server, an HTTP entry references it by URL — the client does not spawn it.
For clients that launch a subprocess and speak JSON-RPC over stdin/stdout — Claude Desktop being the common one — start the backend in stdio mode instead:
{
"mcpServers": {
"emailibrium": {
"command": "/absolute/path/to/emailibrium",
"args": ["--mcp-stdio"]
}
}
}In stdio mode the backend serves the same tools over stdin/stdout and does not start the
HTTP server. Logs go to stderr, not stdout — stdout carries only JSON-RPC frames, so
anything written there would corrupt the protocol stream. Your logs have not vanished; check
stderr and data/logs/emailibrium.log.
The mode is an enum, http (default) or stdio, and can be set two ways:
| Source | Form |
|---|---|
| CLI | --mcp-stdio |
| Env var | EMAILIBRIUM_MCP_MODE=stdio |
The CLI flag wins if both are set. Note the two spellings are not symmetric: the flag is a
boolean shorthand, the environment variable takes the mode name. There is no --mcp-mode
flag and no EMAILIBRIUM_MCP_STDIO variable.
The env form suits client configs that set environment rather than arguments:
{
"mcpServers": {
"emailibrium": {
"command": "/absolute/path/to/emailibrium",
"env": { "EMAILIBRIUM_MCP_MODE": "stdio" }
}
}
}With the backend running in HTTP mode, confirm the endpoint is mounted:
curl -sS http://localhost:8080/api/v1/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'A JSON-RPC response means the server is reachable. A connection refused means the backend is not running; a 404 means it started without the MCP routes mounted.
Localhost only.
/api/v1is unauthenticated by design — emailibrium is a local-first single-user app, and MCP inherits that stance. Anyone who can reach port 8080 can read your mail. Keep the port bound to localhost, and treat bearer auth as a prerequisite for any non-localhost deployment rather than an optional hardening step.
- Ensure Docker Desktop is running and has enough disk space
- Try
docker system prune -fto clean old images - Rebuild without cache:
just docker-build-no-cache
- Update Rust:
rustup update - Check the ruvector submodule:
git submodule update --init --recursive
- Install dependencies:
cd frontend && pnpm install - Clear cache:
cd frontend && pnpm store prune
- Check if port 8080 is already in use:
lsof -i :8080 - For Docker: check container logs with
just docker-logs-backend - For native: check
cd backend && just devoutput
- Models download from Hugging Face (~90 MB for all-MiniLM-L6-v2)
- If behind a proxy, set
HTTPS_PROXYenvironment variable - Models are cached in
backend/.fastembed_cache/
- Verify redirect URIs match exactly (including trailing slash)
- Both Google and Microsoft share the same callback route:
http://localhost:8080/api/v1/auth/callback - Check that client ID and secret are correct in
secrets/dev/
| Variable | Default | Description |
|---|---|---|
APP_ENV |
development |
Environment name (development, production) |
BACKEND_PORT |
8080 |
Backend API port |
FRONTEND_PORT |
3000 |
Frontend web port |
RUST_LOG |
emailibrium=info |
Rust log filter |
VITE_API_URL |
http://localhost:8080 |
Frontend API URL |
EMAILIBRIUM_OPENAI_API_KEY |
-- | OpenAI API key (cloud embeddings) |
EMAILIBRIUM_CLOUD_API_KEY |
-- | OpenAI/Anthropic API key (cloud chat/classification) |
EMAILIBRIUM_GEMINI_API_KEY |
-- | Gemini API key (cloud chat/classification) |
EMAILIBRIUM_COHERE_API_KEY |
-- | Cohere embedding API key |
EMAILIBRIUM_EMBEDDING_PROVIDER |
onnx |
Override embedding provider |
EMAILIBRIUM_EMBEDDING_OLLAMA_URL |
http://localhost:11434 |
Ollama server URL (embedding fallback) |
EMAILIBRIUM_GENERATIVE_OLLAMA_BASE_URL |
http://localhost:11434 |
Ollama server URL (chat/classification) |
EMAILIBRIUM_REDIS_ENABLED |
false |
Whether Redis caching is used |
EMAILIBRIUM_REDIS_URL |
redis://127.0.0.1:6379 |
Redis connection URL |
The plain REDIS_URL set on the backend container in docker-compose.yml is not read by the
backend — only the EMAILIBRIUM_-prefixed variables above are. See the
Configuration Reference for the full redis.* settings.