A local archive you can talk to.
Point Lochive at your files — a folder, a drag-and-drop, a link — and ask questions about them. Answers cite the exact passage they came from, so you can check the source instead of trusting the model.
It runs entirely on your own machine or your own server. No account, no upload to anyone else's cloud, and by default no API key: the embedding model and the language model both run locally. Your documents stay where you put them.
Open source, MIT licensed.
This is the reason Lochive exists. You have documents you'd never paste into ChatGPT — a contract, a medical report, an employee file, your own private notes — but you still want to ask questions about them and get straight answers.
Lochive does that with nothing leaving your machine. Both models run locally: the embedding model (which reads your files) and the language model (which answers) are downloaded once and run on your own hardware. There's no account, no API key required, and no upload to anyone's cloud.
1. Drop your files into a folder — or drag them into the app.
2. Ask: "What's the notice period in the Acme contract?"
3. Get the answer, with the exact clause it came from quoted underneath.
The citation is the point: you check the source passage instead of trusting the model. And because it's self-hosted, the data physically never travels to a third party — verify it yourself by pulling the plug on your internet and watching it keep working.
Manuals, receipts, warranties, tax documents, meeting notes, old contracts — the things you keep "somewhere" and can never find. Point Lochive at the folder and ask in plain language: "Which invoice was the standing desk on?", "What did we agree about the deposit?". It reads files in place and never modifies your folder.
Run it on a server your employees can reach and let them ask the handbook, the policies, the runbooks — instead of digging through a wiki or pinging whoever wrote it. Each person signs in; nothing is sent to an outside service unless you deliberately configure a hosted model.
Research papers, reports, a whole document set you need to reason across. Ask a question and get answers stitched from the relevant passages, each cited so you can jump to the original.
Keep a local model as the default so sensitive material is answered on-machine, and switch a specific chat to a stronger hosted model (GPT, Claude, Kimi…) when the content isn't sensitive. The local-only guardrail lets you mark individual documents that must never be sent to a non-local model — the server enforces it, refusing the question rather than quietly leaking the text.
The honest version: sending a document to a hosted provider means it leaves your machine. Lochive makes the fully-local path the default and the easy one, and gives you per-document controls when you mix in a hosted model — but the only way to guarantee data never leaves is to keep everything local, which is exactly what it does out of the box.
Lochive is the same program in both cases; one setting decides which.
For your own laptop or home server. There are no accounts and no login screen: you open it and it's your archive.
SINGLE_USER_MODE=trueEverything belongs to one implicit local owner. Your watched folder is
inbox/owner@lochive.local/.
Personal mode has no authentication at all. That's the point on a machine only you use — and completely wrong for anything other people can reach. Keep it bound to
127.0.0.1. The server logs a warning at startup to make this hard to forget.
For a company running Lochive internally so employees can search shared knowledge. Accounts are required; each employee signs in.
SINGLE_USER_MODE=falseToday every employee gets a private archive. Shared team workspaces are designed but not yet built — see docs/ROADMAP.md.
Requires Docker. For the default local model you also need Ollama with a model pulled:
ollama pull gemma4 # or any model you prefercp .env.example .env
sed -i '' "s|^JWT_SECRET_KEY=.*|JWT_SECRET_KEY=$(openssl rand -hex 32)|" .env
# personal archive, no login:
echo "SINGLE_USER_MODE=true" >> .env
docker compose -f docker-compose.dev.yml up -d --build
docker compose -f docker-compose.dev.yml exec backend alembic upgrade headOpen http://localhost:7300.
In personal mode (SINGLE_USER_MODE=true) there is no login — you go
straight in.
In team mode, .env.example ships a starter account so a fresh install has
something to sign into:
demo@lochive.dev |
|
| Password | lochive-demo-2026 |
Change this before anyone else can reach the instance. Either set a new
SEED_USER_PASSWORD, or setSEED_USER=falseand register your own account. Lochive refuses to create it whenENVIRONMENT=production, but that is a backstop, not a substitute for changing it.
Prefer a password nobody has seen? Leave SEED_USER_PASSWORD empty and one
is generated on first boot and printed once:
docker compose -f docker-compose.dev.yml logs backend | grep -A3 "Starter account"The first document is slower — the embedding model (~130 MB) downloads once and is cached in a volume.
API docs: http://localhost:7301/docs
SINGLE_USER_MODE=true. Open the app and you're in. No account, no password.
SINGLE_USER_MODE=false. Sign in with the
default login above, or click "No account? Create one"
to register. The first account is created the same way as every other one.
Registration is open to anyone who can reach the instance. Put it behind your VPN or a reverse proxy until invite-only signup exists.
SEED_USER=true
SEED_USER_EMAIL=demo@lochive.dev
SEED_USER_PASSWORD=lochive-demo-2026 # empty = generate one and log it- Refused when
ENVIRONMENT=production. It logs an error and creates nothing, so the shipped password can't follow you into a real deployment. - Ignored in personal mode, which has no accounts.
- Never resets an existing password. Change it and restarts leave it
alone — so changing the password in the app is enough; you don't also have
to edit
.env. - Set
SEED_USER=falseto stop creating it. That does not delete an account that already exists; remove it yourself if you want it gone. - The email must be a real-looking address. Reserved TLDs like
.localfail login validation, leaving an account you cannot use.
Four ways. All of them land in the same pipeline, and everything is searchable together.
Drop files onto the sidebar. Simplest for one-offs.
The main way to use Lochive as an archive. Drop files into a folder and they ingest themselves — no browser needed.
Personal mode — one folder, yours:
inbox/
owner@lochive.local/
manuals/printer.pdf
notes/2026-planning.md
receipts/invoice.pdf
Team mode — one subfolder per employee, named by their email:
inbox/
alice@company.com/
handbook.pdf
bob@company.com/
contracts/acme-2026.docx
What happens:
| You do | Lochive does |
|---|---|
| Add a file | Ingests it and makes it searchable |
| Edit a file | Re-ingests it; answers reflect the new version |
| Delete a file | Removes it and its vectors |
| Add a nested folder | Scans it recursively |
| Add an unsupported file | Ignores it |
| Add a huge file | Skips it and says why |
Scanned every INBOX_SCAN_SECONDS (default 30). Folders that don't match a
registered user are skipped, never guessed at.
Lochive never modifies your folder. Files are read in place — nothing is moved, rewritten, or deleted. That's also why folder documents can't be deleted from the UI: delete the file instead, and the next scan catches up.
./inbox is just the host path mounted into the container. Point it anywhere
by editing the mount in docker-compose.dev.yml:
backend:
volumes:
- /Users/you/Documents/Archive:/data/inbox # any folder you like
worker:
volumes:
- /Users/you/Documents/Archive:/data/inbox # must matchBoth services need the same mount — the API reads it, the worker ingests it.
Mount read-only with :ro if you want that guarantee enforced by Docker
rather than by convention:
- /Users/you/Documents/Archive:/data/inbox:roThen recreate — restart won't re-read the file:
docker compose -f docker-compose.dev.yml up -d backend workerMultiple source folders aren't supported yet; symlink or bind-mount them under one root for now.
Give it a URL to a web page or a direct file. HTML is stripped to text before ingesting.
URLs are validated against the resolved IP, not the hostname. Private, loopback, link-local (cloud metadata), and reserved ranges are refused, and every redirect hop is re-checked. Without that, "fetch this URL" would let anyone make your server read your internal network.
Point Lochive at Postgres or MySQL, pick tables or a query, and turn rows into searchable documents on a refresh schedule. Designed, not yet built — see docs/ROADMAP.md.
PDF, DOCX, TXT, MD, CSV, and HTML via URL. Scanned/image-only PDFs need OCR, which isn't implemented — they fail with a clear message rather than silently ingesting nothing.
Runs on a local Ollama model by default, so it works with no API key and nothing leaves your machine. Any LiteLLM-supported model also works — GPT, Claude, Kimi, Groq, DeepSeek, Mistral, or any OpenAI-compatible server (vLLM, LM Studio, OpenRouter).
List what you want available; users pick per chat from a dropdown:
LLM_MODELS=[
{"id":"ollama/gemma4:latest","label":"gemma4 (local)","api_base":"http://host.docker.internal:11434"},
{"id":"gpt-4o-mini","label":"GPT-4o mini"},
{"id":"claude-sonnet-5","label":"Claude Sonnet 5"},
{"id":"moonshot/kimi-k2-0905-preview","label":"Kimi K2"}
]
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
MOONSHOT_API_KEY=sk-...(One line in .env — expanded here for readability.) The first entry is the
default. Keys live in the environment and are never returned by the API.
Set
api_baseonly for local or self-hosted runtimes. It's per-model on purpose: one global base URL would route your OpenAI calls at your local Ollama.
Sending documents to a hosted provider means they leave your machine. If that matters, keep a local model as the default — and note that per-source model restrictions are on the roadmap, not built yet.
Everything below is in the app at /settings — no .env editing, no
restart.
Paste a provider key (OpenAI, Anthropic, Moonshot/Kimi, Groq, DeepSeek, Mistral, Together, Gemini) and add any LiteLLM model id. Added models appear in the chat picker straight away.
Keys are encrypted at rest and never returned by the API — the page shows only that a key is saved. They are passed to the provider per request and never written into the server's environment, so on a shared instance one person's key can't sign another person's call.
A self-hosted model endpoint on a private address (Ollama, vLLM) is refused unless you set
ALLOW_PRIVATE_MODEL_HOSTS=true. Accepting arbitrary base URLs would let any user aim the server at your internal network.
- Answer only from my documents — on by default. Off lets the model add general knowledge, which it must mark as unsourced.
- Exclude a document — never embedded, never retrieved. Excluding one that's already indexed purges its vectors, rather than just hiding it.
- Local-only documents — never sent to a model that isn't local. A question that needs them is refused rather than answered from a quietly reduced set. Enforced server-side.
- Redact before sending — strips emails, phone numbers and card/ID numbers from text sent to a hosted model. Citations keep the real text, which never left your machine. It catches formatted identifiers, not names or addresses.
Exclusion and local-only are set per document in the sidebar; the rest are on the settings page.
drag & drop ─┐
folder ─┼→ parse → chunk → embed → Qdrant (one collection per user)
link ─┘ ↓
question → embed → search → top matches → model → answer + citations
Ingestion runs in a background worker, so a large PDF doesn't block anything. Every query is scoped by user id on top of per-user collections.
| Layer | Choice |
|---|---|
| API | FastAPI (Python 3.11, async) |
| Worker | Celery + Redis |
| Database | PostgreSQL + SQLAlchemy 2.x + Alembic |
| Vectors | Qdrant, one collection per user |
| Embeddings | fastembed / BAAI bge-small-en-v1.5 — local, no key |
| Generation | LiteLLM — local or any provider, picked per chat |
| Frontend | Next.js 14 + TypeScript + Tailwind + ShadcN |
All in the 73xx block, bound to 127.0.0.1, so they don't collide with other
local projects or get exposed on your network.
| Service | Host |
|---|---|
| frontend | 7300 |
| backend | 7301 |
| postgres | 7302 |
| redis | 7303 |
| qdrant | 7304 / 7305 |
Change the frontend port and you must update CORS_ORIGINS in .env to
match, or the browser blocks every API call. It's a comma-separated list, so
adding an origin never needs a code change:
CORS_ORIGINS=http://localhost:7300,http://127.0.0.1:7300,https://archive.company.comlocalhost and 127.0.0.1 entries automatically allow each other — browsers
treat them as separate origins even though they're the same machine.
After editing, recreate the container; restart keeps the old environment:
docker compose -f docker-compose.dev.yml up -d backendbackend/app/
core/ config, security, dependencies
models/ User, Document, Chat, Message
services/ business logic — routers stay thin
parser_service text extraction per file type
chunker overlapping splits
embedding_service local embeddings
vector_service Qdrant, per-user collections
sync_service the watched folder
url_service link fetching + SSRF guard
ingestion_service the worker-side pipeline
chat_service retrieval + generation + citations
routers/ HTTP only
frontend/
app/ login and chat pages
lib/api.ts the only place that calls the API
docker compose -f docker-compose.dev.yml exec backend pytest78 tests, run against real Postgres — the same engine used in production,
since the models use Postgres-native UUID, JSONB, and enum types.
- No streaming. Answers arrive all at once; with a local model that's a
visible wait (~40s on
gemma4). Hosted models are much faster. - Citation numbering drifts on small local models. Retrieval picks the
right passage, but a weak model sometimes writes
[2]where it used[1]. - Team mode has no shared workspace yet — every employee's archive is private. Sharing means putting a copy in each person's folder.
- No admin controls. In team mode registration is open to anyone who can reach the instance; there's no invite flow or approval.
- No refresh-token revocation. Signing out clears browser tokens but can't invalidate an issued refresh token server-side.
- No OCR, so scanned PDFs are rejected.
- Signing out revokes on this device only. Other sessions stay signed in until their tokens expire; there is no "sign out everywhere".
- No settings UI — guardrails are designed, not built.
- The folder scan is a full walk, hashing every file each pass. Fine for hundreds of files; for tens of thousands, switch to filesystem events.
- Local archive: upload, watched folder, URL ingestion
- Retrieval with citations
- Any model, picked per chat
- Personal mode with no login
- Shared team workspaces
- Settings + guardrails
- SQL database as a source
- Streaming answers
- OCR for scanned documents
- Admin controls (invites, roles)
Details and design in docs/ROADMAP.md.
docker-compose.dev.yml is a development stack: it reloads on change and
mounts your source into the container. For anything other people use, there
is docker-compose.yml:
cp .env.example .env # then work through the checklist at the top of the file
docker compose up -d --build
docker compose exec backend alembic upgrade headIt builds the frontend rather than running a dev server, runs it as a
non-root user, publishes nothing but the app and API on 127.0.0.1, keeps
the database off the host entirely, and mounts the watched folder read-only.
Put a reverse proxy in front for TLS.
See CONTRIBUTING.md. Security reports go through SECURITY.md rather than a public issue.
MIT — see LICENSE.
