A downloadable desktop version of the Mike AI legal platform. Everything runs on your computer: documents, database, settings, model API calls. No Supabase, no cloud storage, no external login. The only network calls the app makes are to whichever model providers' API keys you configure (Anthropic and/or Gemini).
This fork rewires the upstream web app (Next.js + Express + Supabase + S3) into an Electron desktop app:
- Storage moved from Supabase Postgres to a local SQLite database
(
better-sqlite3) inside a workspace folder you pick. - File storage moved from S3/R2 to the same workspace folder, behind a path-traversal-guarded filesystem layer.
- Auth moved from Supabase Auth to a local scrypt-hashed password + per-launch HS256 JWT.
- Distribution moved from a Cloudflare/OpenNext deploy to a Windows NSIS installer that bundles everything (including LibreOffice for DOCX conversion).
The original Next.js + Express architecture is preserved; an Electron shell spawns both as child processes and gates them behind a lock screen.
- Using the app
- Features
- Architecture
- Security model
- Data layout
- Building from source
- Project layout
- Tech stack
- What changed from upstream
- Known limitations
- License
- Run
Mike-Setup-<version>.exefromdist/. (Unsigned — Windows SmartScreen will warn the first time. Click "More info" → "Run anyway".) - Launch Mike from the Start menu.
- First launch — pick a workspace folder. This is where your
documents, database, and settings will live. Pick somewhere you back
up (e.g. inside
Documents). Mike will refuse to use a folder inside its own install directory. - Set a password (minimum 10 characters). You'll enter it every time you open Mike. It can't be recovered — if you forget it, the workspace data is unreadable. Write it down somewhere safe.
- Settings → Models & API Keys — paste in at least one model provider key (Anthropic and/or Gemini). Links to where to get each key are alongside the form.
The lock screen shows on every launch. Five wrong passwords trigger a 30-second lockout (UX rate-limit only — see Security model).
LibreOffice ships bundled with the installer (~330 MB extracted), so DOCX/DOC files render as PDF previews out of the box. No separate install needed.
Copy the entire workspace folder. That's it — everything (DB, files,
auth hash, settings) lives inside .mike/ and files/ under your
chosen workspace.
Delete the workspace folder (or pick a different one on next launch).
Mike doesn't store anything outside the workspace and Electron's
userData directory (which only holds the path to the last-used
workspace).
- Local-first AI legal assistant: chat with Anthropic Claude or Google Gemini models, with conversation history, tool use, and reasoning streams.
- Document workflows: upload PDF, DOCX, DOC, TXT files; the assistant can read, edit, and produce annotated revisions of them with track-changes-style UI.
- Project organisation: group documents into projects with per-project chats and folder hierarchies.
- Tabular review: bulk-extract structured fields from many documents at once into a spreadsheet-like view.
- Built-in legal workflows: prebuilt prompt templates for common document-review tasks (contract review, NDA triage, etc.).
- DOCX conversion via bundled LibreOffice, so Word files render as PDFs inside the app.
- No telemetry, no analytics, no remote calls beyond the model provider you configured.
┌───────────────────────────────────────────────────────────────┐
│ Electron main process (Node) │
│ ─ Lock screen (HTML) │
│ ─ Workspace + password management │
│ ─ Spawns child processes once unlocked: │
│ • Backend (Express on 127.0.0.1:<random>) │
│ • Frontend (Next.js standalone, dev: localhost:3000) │
│ ─ IPC bridge (preload.js → contextBridge.exposeInMainWorld) │
└───────────────────────────────────────────────────────────────┘
│ contextBridge │ child_process.spawn
▼ ▼
┌─────────────────────┐ ┌──────────────────────────────────────┐
│ Renderer (sandbox) │ │ Backend (Node + Express) │
│ Next.js app │ │ ─ /chat, /projects, /documents, … │
│ ─ supabase shim │ │ ─ JWT auth middleware (HS256) │
│ ─ getApiPort, │◀─┤ ─ better-sqlite3 + migration runner │
│ getToken via IPC │ │ ─ Local-FS storage (path guarded) │
└─────────────────────┘ │ ─ LLM clients (Claude, Gemini) │
│ ─ LibreOffice convert (timeout-bound) │
└──────────────────────────────────────┘
When Mike launches:
- Electron main creates the BrowserWindow with the lock screen
(
electron/lock/lock.html). Renderer is sandboxed, no nodeIntegration, contextIsolation on, with a CSP that locksscript-src/connect-srcto'self'+localhost+ the AI providers. - User picks a workspace and enters their password.
electron/auth.tsverifies against the scrypt hash in<workspace>/.mike/auth.json. - On success, Electron mints two random per-launch secrets:
JWT_SECRET(32 random bytes, hex) — signs the session JWT the renderer presents to the backend.DOWNLOAD_SIGNING_SECRET(another 32 random bytes) — signs non-expiring download URLs for stored files.
- Electron spawns the backend (
backend/dist/index.jsviaprocess.execPathwithELECTRON_RUN_AS_NODE=1) with these secrets in env, plusWORKSPACE_PATH,LOCAL_USER_*, the user's API keys, andPORT=0(OS-assigned port). - Backend writes the assigned port to
<workspace>/.mike/runtime.json; Electron reads it and waits for/healthto return 200 before navigating the renderer. - Renderer loads the Next.js frontend, calls
mike.getApiPort()andmike.getToken()over IPC, attachesAuthorization: Bearer …to every API request.
When the user clicks Sign out (or closes the window):
- Renderer clears its cached bridge state.
- Electron tears down the backend + frontend children, drops both secrets, returns to the lock screen.
The JWT and download secrets never persist across launches. There's no "remember me".
electron/preload.js exposes only this — minimal, all
parameter-validated on the main side:
window.mike = {
getState, pickWorkspace, setPassword, unlock, // lock screen
getToken, getUser, getApiPort, // post-unlock
signOut, changePassword,
};setPassword rejects unless the caller is the lock screen
(event.sender === lockWebContents).
The frontend was originally a Supabase Postgres client. Rather than
rewriting every db.from('x').select(...).eq(...) chain, the backend
ships a query-builder shim at backend/src/db/supabaseShim.ts that
mimics the supabase-js builder API (eq, neq, in, or, ilike, range, order,
limit, contains, single, maybeSingle, RPC) and compiles to SQL against
better-sqlite3. JSONB columns are stored as TEXT and round-tripped
through JSON.stringify/parse automatically (per JSON_COLUMNS_BY_TABLE).
The frontend's supabase.auth.* calls go through frontend/src/lib/supabase.ts,
which is a thin shim over window.mike.getToken/getUser/signOut. The
JWT is cached at module scope on first read (stable for the launch
lifetime) and cleared on signOut.
Bundled via electron-builder's extraResources from vendor/libreoffice/
(populated by scripts/fetch-libreoffice.js, which downloads the
official MSI from The Document Foundation, SHA-256 verifies it, and
extracts via msiexec /a). The backend probes for soffice.exe at
<process.resourcesPath>/libreoffice/program/soffice.exe first, with a
dev-fallback to the repo-root vendor/ copy.
Conversions are wrapped with:
- A 60-second timeout (kills the soffice child if exceeded).
- A 200 MB output cap (rejects any conversion that produces a runaway PDF before returning the buffer).
- An env-scrub that hides
JWT_SECRET,DOWNLOAD_SIGNING_SECRET, and the AI provider API keys from the soffice child.
This is a single-user local desktop app. The threat model is:
- Casual access to the workspace folder (another OS user, a backup that landed somewhere shared, a stolen laptop). The password gate defends against this.
- A compromised renderer (XSS in LLM-rendered markdown, malicious embedded SVG/JS in a chat response). Sandbox + CSP + minimal preload API defend against this.
- A redistribution attack on the LibreOffice MSI. SHA-256 verification at fetch time defends against this.
It is not a defense against:
- An attacker with root/admin on the same machine (they can read RAM, inject DLLs, attach a debugger).
- An attacker who already knows the password.
auth.json in the workspace's .mike/ directory holds:
{
"version": 1,
"algo": "scrypt",
"salt": "<base64, 16 random bytes>",
"hash": "<base64, 64-byte derived key>",
"N": 131072, "r": 8, "p": 1, "keylen": 64
}Parameters tuned for ~400 ms per derive on a modern laptop (~128 MB working set), giving meaningful resistance to offline brute-force if the workspace is ever exfiltrated. Lazy-upgrade: if you raise the params and re-unlock, the hash gets re-derived and rewritten transparently.
backend/src/auth/local.ts implements minimal HS256 JWT helpers backed
by Node crypto — no jsonwebtoken dependency. The verifier checks:
header.alg === "HS256",header.typ === "JWT"- HMAC signature via
crypto.timingSafeEqual payload.expis a finite number, not in the pastpayload.subis a non-empty string
Tokens expire after 24 hours (configurable in electron/main.ts
JWT_TTL_SECONDS); the per-launch secret rotation means they don't
survive a relaunch anyway.
electron/main.ts BrowserWindow:
contextIsolation: truenodeIntegration: falsesandbox: truewebSecurity: true(default, not overridden)
CSP (packaged builds only — dev mode skips it for Turbopack compatibility):
default-src 'self' http://localhost:* ws://localhost:*;
script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:*;
style-src 'self' 'unsafe-inline' http://localhost:*;
img-src 'self' data: blob: http://localhost:*;
font-src 'self' data: http://localhost:*;
connect-src 'self' http://localhost:* ws://localhost:*
https://api.anthropic.com https://generativelanguage.googleapis.com;
frame-src 'none'; object-src 'none'; base-uri 'self';
Any upstream CSP is stripped first so this one applies. DevTools is disabled in packaged builds (and on the lock screen always); F12 / Ctrl+Shift+I are no-ops.
backend/src/lib/storage.ts enforces that every path supplied via API
resolves (post-fs.realpath) inside <workspace>/files/. Path-traversal
attempts (../../etc/passwd-style or symlink-escape) return a 404. All
storage keys are server-generated; user-supplied filenames are only used
for display and download Content-Disposition, never as path components.
<workspace>/.mike/auth-state.json tracks failed-attempt count and
lockout deadline. Documented as a UX rate-limit, not an offline-attack
defense — a user with shell access can edit/delete the file to reset
the counter. The real offline-attack defense is the scrypt parameters
(see DECISIONS.md 2026-05-03).
<your workspace>/
├── .mike/
│ ├── auth.json scrypt password hash + parameters
│ ├── auth-state.json lockout state (UX rate-limit)
│ ├── mike.db SQLite — projects, chats, documents,
│ │ document_versions, tabular_reviews, profiles, …
│ ├── mike.db-wal SQLite WAL journal
│ ├── mike.db-shm SQLite shared-memory file
│ └── runtime.json backend's assigned port + PID (overwritten on launch)
└── files/
├── documents/local-user/<doc-id>/
│ ├── source.<ext> original upload
│ └── versions/<vid>/... edited versions
└── converted-pdfs/<userId>/<docId>.pdf LibreOffice-converted previews
Outside the workspace, Mike writes only to Electron's userData
directory (typically %APPDATA%/Mike/):
%APPDATA%/Mike/
├── config.json { "lastWorkspace": "C:\\Users\\…\\MikeWorkspace" }
└── (no other persistent state)
- Node 20+ (the project pins
@types/nodeto 22.x in devDependencies; any 20+ runtime works). - npm 10+.
- Windows 10/11 with Developer Mode enabled (Settings → Privacy &
Security → For developers). Required for
electron-builderto extract symlinks from the macOS code-sign bundle on first run. After first successful build, the toolkit caches and Developer Mode is no longer required. - Admin shell is also acceptable instead of Developer Mode.
# Install deps for all three workspaces (root, frontend, backend)
npm run install:all# Concurrently runs:
# FRONTEND → next dev (localhost:3000, Turbopack)
# ELECTRON → wait-on :3000, build electron, launch electron .
# Electron's main spawns the backend with tsx watch.
npm run devThe Electron window comes up showing the lock screen. Pick a workspace,
set a password, unlock — the renderer navigates to localhost:3000.
DevTools (F12) works in dev only.
# Chains:
# 1. fetch:libreoffice (idempotent — skips if already extracted)
# 2. build (electron + backend + frontend)
# 3. electron-builder (NSIS installer)
npm run distThe installer ends up at dist/Mike-Setup-<version>.exe (~480 MB with
bundled LibreOffice).
First-time
fetch:libreofficedownloads ~290 MB from https://download.documentfoundation.org and extracts ~1.5 GB intovendor/libreoffice/. Re-runs are no-ops whilevendor/libreoffice/program/soffice.exeexists.
Native modules —
better-sqlite3and@napi-rs/canvasship native binaries that must be built against Electron's Node ABI, not the system Node.electron-builder install-app-depshandles this duringnpm run dist. If you see "wrong NODE_MODULE_VERSION" at runtime, runnpm run rebuild-native.
EPERM during build — if
npm run distfails withEPERM: operation not permitted, unlink … better_sqlite3.node, you have a leftovertsx watchfrom a priornpm run devholding the file. Stop the watcher (Ctrl+C the dev terminal) or kill the orphaned node process and retry.
npm run build:electron # tsc + copy preload.js + lock/
npm run build:backend # tsc + stage-backend (rebuild native modules)
npm run build:frontend # next build + stage-frontendmikelocal/
├── electron/ Electron main process
│ ├── main.ts app lifecycle, IPC handlers, CSP, DevTools gating
│ ├── auth.ts scrypt password hash + verify, lockout state
│ ├── jwt.ts HS256 token signer (mirror of backend verifier)
│ ├── secrets.ts read API keys from workspace
│ ├── workspace.ts pick workspace, atomic file write,
│ │ install-dir guard, realpath
│ ├── backend.ts spawn/wait/stop the Express backend
│ ├── frontend.ts spawn/wait/stop the Next.js standalone server
│ ├── paths.ts resolve dist paths in dev vs packaged
│ ├── preload.js contextBridge → window.mike
│ ├── logging.ts file logging with secret redaction
│ └── lock/ lock-screen HTML/CSS/JS (sandbox-safe)
│
├── frontend/ Next.js 16 app (the renderer)
│ └── src/
│ ├── app/ routes, components, hooks
│ ├── contexts/ AuthContext, UserProfileContext, ChatHistoryContext
│ └── lib/supabase.ts shim over window.mike.* IPC
│
├── backend/ Express API
│ ├── src/
│ │ ├── index.ts app setup, CORS, error handler, listen
│ │ ├── db/
│ │ │ ├── sqlite.ts better-sqlite3 client + WAL pragma
│ │ │ ├── migrate.ts runs migrations/*.sqlite.sql in order
│ │ │ └── supabaseShim.ts query-builder compat (~600 lines)
│ │ ├── auth/local.ts HS256 JWT signer + verifier
│ │ ├── middleware/auth.ts requireAuth, requireApiKey
│ │ ├── lib/
│ │ │ ├── storage.ts local-FS storage with realpath traversal guard
│ │ │ ├── upload.ts multer wrapper (100 MB cap)
│ │ │ ├── convert.ts LibreOffice DOCX→PDF (60s timeout, 200 MB cap)
│ │ │ ├── downloadTokens.ts HMAC-signed download URLs
│ │ │ ├── safeSpawn.ts env-scrubbed child spawn helper
│ │ │ ├── libreofficeStatus.ts bundled-soffice probe
│ │ │ ├── llm/ Claude + Gemini clients, tool dispatch
│ │ │ └── ...
│ │ └── routes/ chat, projects, documents, tabular,
│ │ workflows, user, downloads, files, auth
│ └── migrations/
│ └── 001_sqlite_schema.sql full schema (UUID → TEXT, JSONB → TEXT,
│ RLS dropped, FK enforcement on)
│
├── scripts/ Build helpers
│ ├── copy-electron-assets.js preload.js + lock/ → dist-electron/
│ ├── fetch-libreoffice.js download + verify + extract MSI → vendor/
│ ├── stage-backend.js stage compiled backend + prod node_modules,
│ │ rebuild native modules for Electron ABI
│ ├── stage-frontend.js stage Next.js standalone output
│ └── electron-boot-check.js smoke test that the window opens (manual)
│
├── vendor/ gitignored — populated by fetch-libreoffice
├── dist/ gitignored — installer + electron-builder output
├── dist-electron/ gitignored — compiled electron/*.ts
├── backend/.dist-bundle/ gitignored — staged backend for packaging
│
├── .claude/phases/ per-phase implementation notes (committed)
├── DECISIONS.md architectural decision log (committed)
├── TODO.md deferred fixes / known issues (committed)
├── CODE-REVIEW-0.2.0.md pre-ship code review report (committed)
└── README.md this file
| Layer | Tech |
|---|---|
| Shell | Electron 33 (Chromium 130, Node 20) |
| Renderer | Next.js 16 (Turbopack), React 19, TypeScript 5, Tailwind 4 |
| Backend | Express 4, TypeScript 5, tsx (dev), Node 20 |
| Database | SQLite via better-sqlite3 (synchronous, WAL mode) |
| Auth | Node crypto (scrypt + HS256 HMAC) — no third-party deps |
| File storage | Plain filesystem under <workspace>/files/ |
| LLM | @anthropic-ai/sdk, @google/generative-ai |
| DOCX→PDF | LibreOffice 25.8.6 bundled via libreoffice-convert |
| Packaging | electron-builder + NSIS (Windows) |
| Dev tooling | concurrently, wait-on, cross-env, tsx, electron-rebuild |
See DECISIONS.md for the full reasoning. High points:
- Supabase Auth → local scrypt password (
auth.json) + per-launch random HS256 JWT secret in env, hand-rolledcrypto.HMAC(nojsonwebtokendep). - Supabase Postgres → SQLite (
better-sqlite3) viasupabaseShim.tsthat mimics thesupabase-jsquery builder. Schema port: RLS dropped, JSONB → TEXT, UUID → TEXT, plpgsql triggers →created_at DEFAULT CURRENT_TIMESTAMP. Every route handler enforces its ownWHERE user_id = ?post-RLS-removal. - S3/R2 → local filesystem under
<workspace>/files/, with a realpath-based path-traversal guard instorage.ts. - LibreOffice is now bundled with the installer (was: optional external install in earlier versions). Backend probes the bundled copy first, with a system-PATH fallback for non-Windows dev.
- Cloudflare/OpenNext deploy path removed; frontend uses Next.js
output: 'standalone'for offline packaging. - Windows-first; macOS / Linux installers are an electron-builder
config addition away (
"mac"and"linux"blocks). - No telemetry, no analytics, no error reporting service. Logs stay on disk in the workspace.
- API keys stored in SQLite
user_profiles(same as upstream's Postgres design) rather thansafeStorage.encryptString— gated by the workspace password, so encryption-at-rest beyond that adds complexity without raising the bar against an attacker who already has filesystem access.
Tracked in TODO.md. Highlights:
- Code signing: the
.exeis unsigned. Windows SmartScreen warns on first run. Click "More info" → "Run anyway". - Auto-update: not implemented. New versions require a manual reinstall (workspace data is preserved across reinstalls).
- Multi-user: not supported. Multiple OS users on the same machine should each pick their own workspace folder.
- Logout button: closes the window/returns to lock screen rather than backgrounding the app. To "log out", close and reopen.
- Cross-platform: Windows only for now. macOS / Linux is a config addition.
- Next.js 16.0.3 has a security advisory (CVE-2025-66478). Bumping to the latest 16.x patch is on the to-do list. Lower urgency for this loopback-only deployment but worth tracking.
AGPL-3.0-only — same as upstream. See LICENSE. All derivative source
remains open under the same terms.
- Phases: substantial work is broken into numbered phases under
.claude/phases/PHASE-NN-{active|done}.md. One active phase at a time. See.claude/phases/PHASE-08-done.mdfor the latest example. - Decisions: every non-trivial technical decision goes in
DECISIONS.mdwith the why, not just the what. Append-only. - Commits: Conventional Commits (
feat,fix,refactor, etc.), scope by area (feat(security): ...,fix(chat): ...). - Reviews: pre-ship code reviews land in
CODE-REVIEW-<version>.md. Each finding is either fixed in-session or moved toTODO.mdwith a note on why it was deferred.