Docket is a local desktop AI legal platform. Documents, database, settings, indexing, search, and OCR stay on your computer. No Supabase, no cloud storage, no external login. Network calls are limited to model providers you explicitly configure for chat or embeddings; local Ollama and MLX/OpenAI-compatible servers can keep those workflows offline too.
Docket builds on mikelocal, the Electron desktop edition of Mike, the open-source AI legal platform. See License & attribution for provenance and licensing details.
Docket extends that desktop base with self-contained local project
folders (app-level state under Electron userData, project data under
each project's .docket/ directory), hybrid document indexing and
search with optional embeddings, local OCR for scanned PDFs and images,
a full PDF annotation workflow
(highlights, comments, citation promotion, PDF export), annotation- and
document-scoped chat, and a bundled-LibreOffice installer — see
the feature overview below.
The original Next.js + Express architecture is preserved; an Electron shell starts a local desktop session, then lets you open or create local folder-backed projects.
Four things Docket is built around, ordered by what they need to run.
Read this before judging the hardware requirements. Opening a folder, indexing it, and searching it are pure CPU work and stay that way. Everything with a model behind it — semantic embeddings and every assistant answer — is not: in practice you want a machine with a GPU (a discrete NVIDIA/AMD card, or an Apple Silicon Mac whose unified memory the model can use), or you point Docket at an external API instead. A 26B-class local model on a CPU-only box is technically possible and practically unusable. See Requirements by feature for the full matrix.
Every screenshot below is a real, unretouched frame from the app, captured by an automated harness against synthetic pleadings in a fictional jurisdiction (Harborview Logistics LLC v. Cascade Freight Systems, Inc., State of Franklin). No real client documents are involved anywhere in this README.
No import step and no copying: you open a folder that already exists on
your disk, and Docket indexes it in place. Documents stay where they
are, the index and all project data live in that folder's .docket/
directory, and the folder can be re-scanned when files change.
Indexing builds a SQLite FTS5 lexical index plus a trigram index for substring and CJK recall, so keyword, substring and exact search all work with no model configured and no network access.
Turn embeddings on and the same index also answers by meaning. Vectors are computed and stored on your machine, in the project's own database; lexical and semantic hits are merged with reciprocal rank fusion, and every result carries a badge saying why it matched.
This is the first feature that needs a model. The default is
batiai/qwen3-embedding:0.6b via Ollama — small as models go, but
embedding a real corpus means running it over every chunk, so plan on
a GPU-equipped machine or an OpenAI-compatible embeddings endpoint.
The 22 chunks below are a four-document toy; a real matter is not.
Embeddings are also optional: with none configured, search silently
falls back to the CPU-only keyword path.
The query below is "blame for schedule slippage". None of blame, schedule or slippage appears anywhere in the four documents; all four hits are semantic.
Highlight text in the PDF viewer in whatever colour you like. Comments and highlights are ordinary CPU-only annotation work — no model involved:
Then say what the colour meant, in the chat message itself:
I highlighted Harborview's key allegations in yellow in
01-complaint.pdf. Read my annotations, then read02-answer.pdf, and build a markdown table with three columns: the allegation paragraph, what Harborview alleges, and Cascade's corresponding response. Cite the source for each row.
Nothing was configured before this prompt. The assistant read the annotations, matched each highlighted allegation to the defendant's paragraph-by-paragraph response in the answer, and cited both sides.
The model was Ollama gemma4:26b-a4b-it-q4_K_M, running locally —
no API key and no network, but not on a CPU-only machine. At 26B
parameters this needs GPU or unified-memory acceleration to answer in a
usable amount of time; on hardware without it, connect an external API
instead (Requirements by feature):
If you get tired of restating it, you can pin a colour's meaning to the project instead. This is the optional path — as the panel itself says, "A meaning stated in a chat message still takes priority for that turn."
Every citation in an answer is clickable. Clicking one opens the cited document at the cited passage and paints it in the viewer — below, the citation behind the paragraph 10 row has opened the complaint and marked the quoted sentence:
Picking a colour promotes that transient citation into a saved
annotation you keep. Export PDF then writes the annotations into
the PDF bytes as standard /Highlight annotation objects, so Preview,
Acrobat, Foxit or Zotero open and edit them like any other PDF
markup — the export from the run above carries all three highlights,
with their colour and quoted text, on page 3.
Annotating and exporting are CPU-only; only producing the citation in the first place needs a model.
To summarise the four features above:
- Features 1 and 4 are CPU-only. Opening and indexing a folder, keyword/substring/exact search, OCR, highlights, comments, promoting a citation, and the annotated PDF export all run on an ordinary CPU, offline, with no model configured. The only part of feature 4 that needs a model is producing the citation in the first place.
- Features 2 and 3 need a model, and realistically the hardware to run it. Either an accelerated machine — a discrete GPU, or an Apple Silicon Mac — or an external API (Anthropic Claude, Google Gemini, any OpenAI-compatible endpoint) that you configure yourself. Docket will not reach for a network model on its own.
- One feature is hard-gated on a GPU: LLM-generated document outlines are only attempted when Docket detects hardware acceleration, to avoid multi-minute stalls. Without it you keep the heuristic outline, which is what the viewer screenshots above show.
- What Docket does
- New in Docket
- Using the app
- Features
- Requirements by feature
- Architecture
- Security model
- Data layout
- Building from source
- Project layout
- Tech stack
- Known limitations
- License
Docket's additions on top of the mikelocal desktop base. Every screenshot below is captured from the app's automated E2E harness against synthetic fixture documents — no real files.
A project-local hybrid index: SQLite FTS5 unicode61 lexical search
plus a trigram index for substring/Korean-CJK recall, optional
configurable embedding providers (Ollama and OpenAI-compatible
endpoints, including local MLX servers) with Float32 vector rows, a
resumable background embedding queue, and RRF-merged, document-grouped
results with visible match reasons (backend/src/lib/indexing/).
Search results show why each document matched — keyword, substring, exact, or semantic — with highlighted snippets:
Scanned PDFs and image documents (PNG, JPG/JPEG, TIFF, BMP,
and WebP) are recognized locally during indexing. On macOS Docket uses
Apple Vision first; a bundled local PP-OCR/ONNX pipeline is available as
the cross-platform fallback. OCR failures stop locally and are never
rerouted to a cloud model or external OCR API.
Recognized text participates in the same keyword, substring, and semantic search paths as ordinary document text. Docket also stores the normalized bounding box for each recognized region. When a scanned-PDF citation has no PDF text layer to match, clicking the citation retrieves those local regions and paints the cited passage in the existing PDF highlight overlay.
Text-selection highlights and comments stored as app-owned metadata, with a color palette plus custom colors, annotation editing and deletion, and automatic outline generation for PDFs and DOCX with a navigation pane:
Two annotation flows stand out:
- Citation promotion — when the assistant cites a passage, the citation renders as a blue click-through highlight in the viewer, and one click promotes it into a saved annotation you keep.
- Round-trip export — exporting embeds the annotations into the PDF
bytes (
pdf-lib), so external readers (Foxit, Preview, Acrobat, Zotero) can open and edit them.
Every annotation is also listed under its document for review and bulk management:
The assistant ships with a document-analysis toolset — reading documents, searching the project index, and fetching your saved annotations — that the model calls while answering. Retrieval is backed by the local index (search + chunk reads with bounded full-document escalation), and the assistant reasons over your annotations including by highlight color ("summarize what I marked in red") when you ask. Chats can be scoped to a user-selected subset of documents, and answers from weaker local models are sanitized before they land in the chat.
And a real answer from a fully local model (Ollama Gemma 4 on an Apple Silicon laptop) — grounded in the indexed document through tool calls, with numbered citations that click through to the exact passage in the viewer:
Highlight colors carry meaning that only the person marking up the documents knows — green for undisputed facts, red for contested points. The color legend makes that mapping persistent per project, so it doesn't have to be restated every chat turn.
Each of the eight color families (red, orange, yellow, green, blue, purple, pink, gray) takes a free-text meaning of up to 120 characters and, optionally, binds to a party role (plaintiff, defendant, appellant, …) or a side (A/B). Saved entries are injected into that project's chat as a standing instruction, so "summarize the undisputed facts" resolves to the green family without naming the color. When an entry carries a party binding, the assistant passes it through to the annotation tools and the server enforces that party scope.
The legend is declarative, not inferred: Docket never reads highlight content to guess what a color means, and colors left blank stay unmapped. A meaning stated in a chat message overrides the legend for that turn only, so reusing a color ad hoc doesn't corrupt the saved mapping.
Edit it under Color legend on a project's Documents tab. Entries
live in that project's .docket/project.db, alongside the annotations
they describe.
Local models take their time, so Docket doesn't make you watch them work. An in-progress chat answer keeps generating when you navigate to another page — the stream survives the route change, and returning to the chat mid-answer reattaches to the live response. Semantic embedding has always run in the background; both now report back when they finish.
- Completion notifications — when an answer or an embedding run finishes (or fails) while you're elsewhere, Docket shows an in-app toast if the window is focused, or a system notification if the app is in the background. Clicking either focuses the app and jumps to the finished chat or project.
- Nothing fires when you're already looking — notifications are suppressed for the page that shows the result, including any page inside the project whose embedding completed.
- Stop still means stop — the Stop button cancels generation exactly as before, silently; only navigation no longer does.
- One answer at a time — local inference slows down when shared, so starting a question in another chat while one is generating is blocked with a "go to running chat" shortcut instead of degrading both.
- Manually pausing an embedding run stays silent — failure notifications are reserved for runs that actually stopped on errors.
- Self-contained project folders — the workspace picker and
app-level password are replaced by a projects registry: app state
under Electron
userData, each project's documents, versions, indexes, chats, and annotations under that project folder's.docket/directory. Startup opens directly without a login gate. - Local model providers — Ollama and MLX/OpenAI-compatible routing for chat and embeddings alongside Anthropic/Gemini.
- LibreOffice bundled with the installer (mikelocal detected an external install at runtime). Backend probes the bundled copy first, with a system-PATH fallback for non-Windows dev.
- API keys moved from SQLite columns to Electron
safeStorage.encryptStringin app data. - macOS support (Apple Silicon verified) in addition to mikelocal's Windows-first target.
- Rebrand to Docket with automatic data migration from pre-rebrand
Mike/.mikedirectories.
- Run
Docket-Setup-<version>.exefromdist/. (Unsigned — Windows SmartScreen will warn the first time. Click "More info" → "Run anyway".) - Launch Docket from the Start menu.
- Open
Docket-<version>-<arch>.dmgfromdist/and drag Docket into Applications, or unzipDocket-<version>-<arch>-mac.zipand run the.appdirectly. - Unsigned, no Apple notarization: the first launch will be blocked
by Gatekeeper. Right-click (or Control-click)
Docket.app→ Open → confirm in the dialog that appears. This is a one-time step; normal double-click launches work afterwards. If Gatekeeper still refuses, clear the quarantine flag from Terminal:xattr -cr /Applications/Docket.app
- If you built the app yourself with
npm run diston this Mac, macOS does not quarantine locally-built artifacts, so it launches with no warning at all — the Gatekeeper prompt above only applies to.app/.dmg/.zipfiles that were downloaded or otherwise transferred from elsewhere.
Neither platform's build is code-signed or notarized (see Known limitations).
- First launch — Docket opens directly to the app. Existing registered
projects appear on the Projects tab. Create or open a project by
choosing a local folder; that folder is the project root and holds the
project's source files, metadata, index, and vector DB under
.docket/. - 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.
There is no Docket-specific password step. The local desktop session starts directly on launch; use your OS account, disk encryption, and normal file permissions to protect app data and project folders.
Windows: 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.
macOS / Linux: LibreOffice is not bundled (bundling is Windows-only
by design — see scripts/fetch-libreoffice.js). DOCX→PDF conversion
falls back to a system-installed LibreOffice if one is found at
/Applications/LibreOffice.app, /usr/local/bin/soffice,
/usr/bin/soffice, or on PATH. Without one, DOCX files still upload
and the assistant can still read/edit them — only the rendered PDF
preview is unavailable. Install
LibreOffice separately
if you need DOCX preview rendering.
Back up each project folder. A project is self-contained: original files, project SQLite data, generated files, index metadata, and vector DB files live under that folder.
App-level local state is separate and lives in Electron userData
(typically %APPDATA%/Docket/). It holds the project registry, app-level
chat/profile state, runtime files, logs, and OS-encrypted API key blobs.
It does not copy project source files, indexes, embeddings, or vector DBs.
Delete the project folder for project-level data. To clear app-level
registry/profile/session state, delete Electron's userData directory.
- Local-first AI legal assistant: chat with a fully local LLM (Ollama or any OpenAI-compatible server such as LM Studio) or with cloud Anthropic Claude / Google Gemini models — with conversation history, tool use, and reasoning streams either way.
- Document workflows: upload PDF, DOCX, DOC, TXT, PNG, JPG/JPEG, TIFF, BMP, and WebP 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.
- Background completion: chat answers keep generating while you work elsewhere in the app, with in-app and OS notifications when an answer or embedding run finishes — click through to the result.
- 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.
Docket is local-first: the core document workflows run entirely on an ordinary CPU, fully offline, with no model configured. AI features need a model — either a local LLM or a cloud API key.
On GPUs, plainly. Nothing in Docket refuses to start without one, and only LLM outline generation is hard-gated on detecting hardware acceleration. But "runs on CPU" is not the same as "usable on CPU": a local model of the size you would actually want for legal work (the screenshots in What Docket does used a 26B model) needs a GPU or Apple Silicon unified memory to answer in a reasonable time, and embedding a real corpus means running the embedding model over every chunk of every document. If your machine has no acceleration, use an external API for the model-backed rows below and keep the CPU-only rows entirely local.
| Feature | CPU only | Local LLM or API needed |
|---|---|---|
| Projects, folders, per-project organisation | ✅ | — |
| Document upload & viewing (PDF, DOCX, DOC, TXT, PNG/JPEG, TIFF, BMP, WebP) | ✅ | — |
| Local OCR for scanned PDFs and images | ✅ | — |
| DOCX/DOC → PDF conversion (bundled LibreOffice) | ✅ | — |
| Highlights, comments, annotations | ✅ | — |
| Keyword / full-text search (SQLite FTS5 + trigram, BM25) | ✅ | — |
| Heuristic document outline | ✅ | — |
| Backups & data portability | ✅ | — |
| Assistant chat (tool use, reasoning streams) | — | Local LLM on a GPU / Apple Silicon machine (Ollama, or any OpenAI-compatible server such as LM Studio) or cloud API (Anthropic Claude, Google Gemini, OpenAI-compatible) |
| Document read / edit / annotated revisions via chat | — | Same as assistant chat |
| Tabular review (bulk structured extraction) | — | Same as assistant chat |
| Built-in legal workflows (contract review, NDA triage, …) | — | Same as assistant chat |
| Semantic search (embeddings) | — | Embedding model via Ollama (default batiai/qwen3-embedding:0.6b) or an OpenAI-compatible embeddings endpoint. The model is small, but it runs over every chunk of every document — budget for acceleration on a real corpus |
| LLM-generated document outline | — | Local LLM plus a detected GPU — on CPU-only machines Docket skips LLM outline generation by design and keeps the heuristic outline |
Notes:
- Semantic search degrades gracefully. With embeddings disabled or no embedding model reachable, project search automatically falls back to the CPU-only keyword search — indexing and search maintenance keep working without any "semantic hardware".
- Everything in the CPU-only rows works with no API key and no network access. That is the part of Docket a CPU-only laptop runs in full: folders, indexing, keyword search, OCR, annotations, export, backups.
- The model-backed rows are where hardware starts to matter. Every one of them is satisfied either by a local model on an accelerated machine or by an external API — Docket does not care which, and never picks for you.
- The hard GPU gate exists to avoid multi-minute stalls: LLM outline generation over long documents is only attempted when hardware acceleration is detected.
┌───────────────────────────────────────────────────────────────┐
│ Electron main process (Node) │
│ ─ Starts a local desktop session directly │
│ ─ Tracks app-level local state in Electron userData │
│ ─ Spawns child processes for the session: │
│ • 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) │
└─────────────────────┘ │ ─ Local OCR (Vision / PP-OCR ONNX) │
│ ─ LLM clients (local or configured) │
│ ─ LibreOffice convert (timeout-bound) │
└──────────────────────────────────────┘
When Docket launches:
- Electron main creates the BrowserWindow and automatically starts an
app-level local session. Renderer is sandboxed, no nodeIntegration,
contextIsolation on, with a CSP that locks
script-src/connect-srcto'self'+localhost+ the AI providers. - The frontend opens directly to Projects. Registered projects are local
folders; adding a new project opens/registers that folder and stores that
project's originals, metadata, indexes, and vector rows under the project
folder's
.docket/directory. - 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, plus app data / active project paths,LOCAL_USER_*, the user's API keys, andPORT=0(OS-assigned port). - Backend writes the assigned port to
<app-data>/.docket/runtime.json; Electron reads it and waits for/healthto return 200 before navigating the renderer. - Renderer loads the Next.js frontend, calls
docket.getApiPort()anddocket.getToken()over IPC, attachesAuthorization: Bearer …to every API request.
When the user closes the window, Electron tears down the backend + frontend children and drops both secrets.
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.docket = {
getToken, getUser, getApiPort, // active local session
pickSourceFolder, // app-internal local folder/project import
};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.docket.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:
- 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).
- Someone who can read Electron app data or a project folder directly. Docket no longer has its own password gate, so use OS account isolation, full-disk encryption, and normal backup hygiene for local data privacy.
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 before the main app is loaded); F12 / Ctrl+Shift+I are no-ops.
backend/src/lib/storage.ts enforces that every storage key resolves
(post-fs.realpath) inside the active managed files root: app data for
app-level records or <project>/.docket/files/ for project records.
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.
%APPDATA%/Docket/ Electron userData; platform-specific on macOS/Linux
├── .docket/
│ ├── app.db SQLite — project registry, app chats, profiles
│ ├── runtime.json backend's assigned port + PID
│ ├── secrets.enc OS-encrypted model API keys
│ └── logs/ per-launch backend/frontend logs
└── files/ app-level generated/downloadable files
<project folder>/
├── original matter files PDFs/DOCX/TXT/etc. owned by the project
├── .docket/
│ ├── project.db SQLite — project documents, chats, reviews,
│ │ source folder metadata, index metadata, …
│ ├── project.db-wal SQLite WAL journal
│ ├── project.db-shm SQLite shared-memory file
│ ├── files/ generated renditions, versions, exports
│ └── vector/ local vector DB / embedding artifacts
└── ...
Legacy installs may still have config.json.lastWorkspace; Docket reads
that only as a one-time migration hint for the old all-in-one layout.
Installs from before the Mike → Docket rebrand migrate automatically: a
missing/empty %APPDATA%/Docket is populated by renaming the old
%APPDATA%/Mike dir, and each project's .mike/ dir is renamed to
.docket/ the first time the app touches it.
- Open Folder / Create Project registers the selected local folder as a project root and scans supported files already inside it.
- Add File copies the selected external file into the project folder, then indexes it from the project-owned copy.
- Add Folder in v1 does not attach an external source folder by reference. Docket copies supported files from that external folder into the project folder instead.
- Project-local source roots are stored as
project:<relativePath>. The oldworkspace:<relativePath>prefix is accepted only for legacy migrated records.
- Node —
package.jsonpins"engines": { "node": ">=20 <25" }, and.nvmrcpins22. Usenvm use(or match.nvmrcwith your version manager of choice) before installing. This isn't just a style preference:better-sqlite3and@napi-rs/canvasare native modules built against a specificNODE_MODULE_VERSIONABI, so running scripts under a stray global Node (e.g. a Homebrew-installed Node 26 sitting ahead of your version manager onPATH) fails at runtime withERR_DLOPEN_FAILED/ "wrong NODE_MODULE_VERSION" even thoughnpm installitself succeeds silently. - npm 10+.
- Windows 10/11: 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. - macOS: Xcode Command Line Tools (
xcode-select --install) ifbetter-sqlite3/@napi-rs/canvasdon't have a prebuilt binary for your Node version + CPU architecture and need to compile from source.
# Install deps for all three packages (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 starts an app-level local session automatically and opens
the renderer at localhost:3000/projects. Registered projects are local
folders; add/open project folders from the Projects screen.
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 distWindows output: dist/Docket-Setup-<version>.exe (~480 MB with
bundled LibreOffice).
macOS output (no explicit mac.target is configured, so
electron-builder emits its defaults — DMG + ZIP — for whichever
architecture you build on):
dist/Docket-<version>-<arch>.dmgdist/Docket-<version>-<arch>-mac.zipdist/mac-<arch>/Docket.app(unpacked, runs directly)
The DMG/ZIP are ~160 MB — smaller than the Windows build because LibreOffice isn't
bundled on macOS (see Using the app). Verified on
Apple Silicon (arm64); Intel Macs should produce an x64 build via the
same command but that hasn't been separately verified. Neither output
is code-signed or notarized — see the macOS Gatekeeper note under
Using the app for what that means for anyone you
share a built .dmg/.zip with.
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. This step is a no-op on macOS/Linux (LibreOffice bundling is Windows-only).
Native modules —
better-sqlite3,@napi-rs/canvas, andonnxruntime-nodeship native binaries that must match the active Node or Electron ABI. Development startup verifies them throughscripts/ensure-dev-native-modules.js;electron-builder install-app-depshandles packaging duringnpm run dist. The macOS Apple Vision helper is compiled separately byscripts/build-vision-ocr.jsand is not tracked in Git.
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-frontenddocket-desktop/
├── electron/ Electron main process
│ ├── main.ts app lifecycle, IPC handlers, CSP, DevTools gating
│ ├── jwt.ts HS256 token signer (mirror of backend verifier)
│ ├── secrets.ts read OS-encrypted API keys from app data
│ ├── appData.ts app config paths, app-data layout,
│ │ atomic file write
│ ├── 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.docket
│ ├── logging.ts file logging with secret redaction
│
├── frontend/ Next.js 16 app (the renderer)
│ └── src/
│ ├── app/ routes, components, hooks
│ ├── contexts/ AuthContext, UserProfileContext, ChatHistoryContext
│ └── lib/supabase.ts shim over window.docket.* 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
│ │ │ ├── ocr/ local OCR engines, model management,
│ │ │ │ preprocessing, and region matching
│ │ │ ├── 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
│
└── 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 HS256 HMAC session JWT — no third-party deps |
| File storage | Plain filesystem under app data and <project>/.docket/files/ |
| OCR | Apple Vision (macOS) + local PP-OCR models through ONNX Runtime |
| LLM | @anthropic-ai/sdk, @google/generative-ai |
| DOCX→PDF | LibreOffice 25.8.6 bundled via libreoffice-convert |
| Packaging | electron-builder + NSIS (Windows) / DMG + ZIP (macOS) |
| Dev tooling | concurrently, wait-on, cross-env, tsx, electron-rebuild |
Current highlights:
- Code signing: neither build is signed. The Windows
.exeis unsigned (SmartScreen warns on first run — "More info" → "Run anyway"). The macOS.app/.dmg/.ziphas no Apple Developer ID signature or notarization, so Gatekeeper blocks the first launch of any copy that was downloaded or transferred from elsewhere (see Using the app for the right-click-Open workaround). Locally-built macOS artifacts run without any prompt since macOS only quarantines files it considers "downloaded". Getting rid of the prompt for distributed builds requires an Apple Developer Program membership ($99/yr) plusnotarize: truein the electron-buildermacconfig — not set up in this repo. - Auto-update: not implemented. New versions require a manual reinstall (app data and project folders are preserved across reinstalls).
- Multi-user: not supported. Multiple OS users on the same machine should each use their own OS account and app data directory.
- Logout button: local desktop uses quit/relaunch as the session reset path rather than backgrounding the app.
- Cross-platform: Windows and macOS (Apple Silicon, verified) both
build and run via
npm run dist. Intel Mac (x64) should work through the same electron-builder default but hasn't been separately verified. Linux is not yet configured (nolinuxtarget beyond the icon reference inpackage.json). - OCR accuracy: recognition quality depends on scan resolution, skew, language mix, and source contrast. Search remains available when individual regions are imperfect, but important quoted text should still be checked against the displayed scan.
- 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.
Docket is a derivative of
mikelocal (forked at commit
b89e5763b57b6139e0393642df7462c81668a216), the Electron desktop
edition of Mike, which is itself a derivative of
Mike, an open-source AI
legal platform. All three are licensed under AGPL-3.0-only. See
LICENSE for the full text. All derivative source remains open under
the same terms.
Docket has modified the mikelocal sources continuously since 2026-06-25. Roughly 32,000 lines of the upstream tree survive unmodified; the Next.js + Express architecture, the chat and tabular review UI, and the workflow system are upstream work.
- Original Mike portions © the Mike contributors.
- Desktop port portions (Electron shell, SQLite/filesystem/auth rewiring, Windows packaging) © the mikelocal contributors.
- Docket modifications (project folders, document indexing and hybrid search, local OCR, PDF annotation workflow, annotation- and document-scoped chat, local providers, rebrand) © 2026 the Docket contributors.
- Built-in workflows — every built-in workflow except
Issue-by-Issue Comparison and New Arguments Across Brief Sequence
is Open Legal Products' work, reproduced with light edits (tool names
and citation requirements adapted to Docket's retrieval tools) from
mike-workflows
under the MIT license. The workflow UI credits the author
per workflow; the MIT notice is reproduced in
THIRD-PARTY-NOTICES.md. - Parts of the product design were inspired by Docufinder; Docket contains no Docufinder code.
- "Mike" is the upstream project's name; Docket is an independent derivative and is not affiliated with or endorsed by the Mike or mikelocal projects.
- "Docket" is an ordinary word in the legal field, and unrelated products, companies, and trademarks share the name. This project is an independent, non-commercial open-source project and is not affiliated with, endorsed by, sponsored by, or otherwise connected to any of them.
LICENSE covers Docket's own source. The installer also bundles other
people's work with its own notice requirements — Electron and Chromium,
LibreOffice (MPL-2.0, Windows only), the PaddleOCR models fetched at run
time (Apache-2.0), the libvips shared libraries behind sharp
(LGPL-3.0-or-later), and ~866 npm packages. All of it is itemised in
THIRD-PARTY-NOTICES.md, regenerated with:
npm run notices










