Upload a CSV of contacts and enrich it with employment info, location, links, and photos — using your own AI provider (Anthropic, OpenAI, or Gemini). Self-hosted, no third-party SaaS, your API key stays on the server. Ships as a prebuilt Docker image.
Repo: https://github.com/mstras/AIContacts · Image: ghcr.io/mstras/aicontacts
Enrichment happens in two layers:
- Email-derived (always on, no key, no AI). From each contact's email domain the server works out the company name, website, and logo (personal domains like gmail are skipped), and pulls a Gravatar photo if the person has one.
- AI-grounded (optional). If you configure a provider, the server asks it — with web-search grounding — for the job title, location, and a short profile, then merges what it finds on top of layer 1. The prompt is tuned to report only what's actually found and to mark anything unconfirmed rather than guess.
You can edit or override any field by hand (your values always win), reject the machine's data for a contact, and export a CSV that imports cleanly into Google Contacts.
browser ── static React app ──► /api/enrich ──► your AI provider (key held here)
└────────► email-derived data + Gravatar
A tiny Express server serves the built frontend and exposes /api/enrich. Your API key lives in
the server's environment and is never sent to the browser — which also sidesteps the CORS and
key-exposure problems you hit calling providers directly from client-side code.
The published image is at ghcr.io/mstras/aicontacts.
git clone https://github.com/mstras/AIContacts.git
cd AIContacts
cp .env.example .env # then edit .env: set AI_PROVIDER and AI_API_KEY
docker compose up -d # pulls ghcr.io/mstras/aicontacts:latest and starts itOr without cloning, using docker run:
docker run -d --name aicontacts -p 8787:8787 --env-file .env \
ghcr.io/mstras/aicontacts:latestOpen http://localhost:8787 (or your server's host/port). To put it behind your existing
reverse proxy, point the proxy at the container's port 8787. Update later with
docker compose pull && docker compose up -d.
If the package is private, authenticate first:
echo $GHCR_TOKEN | docker login ghcr.io -u <your-username> --password-stdin
git clone https://github.com/mstras/AIContacts.git
cd AIContacts
cp .env.example .env
# In docker-compose.yml: comment out `pull_policy: always` and uncomment `build: .`
docker compose up -d --build.github/workflows/docker-publish.yml builds and pushes to GHCR automatically:
- on every push to
main→ghcr.io/mstras/aicontacts:latest - on a
v*git tag → a matching version tag
It uses the repo's built-in GITHUB_TOKEN (no secrets to add). After the first successful run,
open the package on GitHub and set its visibility to public if you want anyone to pull without
logging in.
All config is via environment variables (see .env.example):
| Variable | Default | Notes |
|---|---|---|
AI_PROVIDER |
(blank) | anthropic, openai, or gemini. Blank = email-derived only. |
AI_API_KEY |
(blank) | Your key for that provider. |
AI_MODEL |
per-provider | Override the model. Defaults: claude-sonnet-4-6 / gpt-4o / gemini-2.0-flash. |
AI_MODEL_DEEP |
= AI_MODEL |
Optional stronger model used only for Deep check passes (more searches + bigger budget). |
AI_WEB_SEARCH |
true |
Use the provider's web-search grounding. Auto-retries without it if the model rejects the tool. |
AI_TIMEOUT_MS |
60000 |
Per-request timeout for the AI call. |
AUTH_USER |
admin |
Username for Basic Auth (only used if a password is set). |
AUTH_PASSWORD |
(blank) | Set this to require a login for the whole app (frontend and API). Blank = no auth. Pair with HTTPS. |
PORT |
8787 |
Listen port. |
After changing .env, restart: docker compose up -d.
- Anthropic — Messages API with the
web_searchtool. Any model your key can access works. - OpenAI — Responses API with the
web_searchtool. If your model doesn't support it, the server falls back to a plain call automatically. - Gemini —
generateContentwithgoogle_searchgrounding. Prefer a 2.x model (defaultgemini-2.0-flash); older 1.5 models use a different grounding API.
npm install
cp .env.example .env # set your provider + key
npm run dev # server on :8787, Vite dev server on :5173 (proxies /api)Open http://localhost:5173. Build a production bundle with npm run build, then npm start.
Any CSV with a header row. The app auto-detects common column names and lets you remap them:
- Name —
name,full name, orfirst name+last name - Email —
email(unlocks company/website/logo/photo) - Company —
company,organization,employer(optional hint)
- Google CSV — maps everything into Google Contacts' own columns (
Organization Name/Organization Title,Website,Notes,Photo, and anEnrichedlabel) and passes through any Google-format columns your file already had, so a re-import updates contacts without losing phones or addresses. Location goes intoNotes(not a structured address) on purpose. Google's CSV import generally won't fetch photo URLs, so thePhotocolumn is for reference. - Full CSV — your original columns plus enrichment as extra columns.
Both honor your manual edits and per-contact rejections.
- Select for export — tick the checkbox on any rows (or the header box to select everything in the current view). With a selection active, both exports include only the ticked contacts; with nothing ticked, they export everything.
- Filter by status — the pills (All / Enriched / Derived / Minimal / Not enriched / Rejected) split contacts by how well they enriched, so you can e.g. work only the ones that came back empty.
- Require search — a toggle in the engine panel (gear icon). When on, the app does not fill company/website/logo/photo from the email domain; those fields are only kept if a live web search actually finds them (needs a configured provider). Off is the fast default: domain-derived data plus AI.
By default the app has no authentication — fine on a trusted LAN, not for the open internet. Set
AUTH_PASSWORD (and optionally AUTH_USER) to require HTTP Basic Auth on every route, including
the API, so a browser login is needed before anything loads. This is deliberately simple; for anything
exposed publicly, also terminate HTTPS at your reverse proxy (Basic Auth sends credentials base64-encoded,
so it must run over TLS) and consider your proxy's own access controls.
AI results — even with web search — can be wrong, mismatched, or out of date, which is why every row links out for manual verification. Enriching contact data is your responsibility: make sure your use is consistent with the contacts' reasonable expectations and with applicable law (e.g. GDPR / CCPA). This tool does not return home or street addresses by design.
MIT — see LICENSE.