Live
Contact List
About Contact List
Contact List
A lightweight, self-hosted contact manager. Store, search, and organise the people and companies in your life on your own computer — with optional two-way sync to your Google Contacts.
It runs as a small local web app at http://localhost:5002. Start it and it sits quietly
in your system tray — click the icon and choose Open Contact List when you want the
page. Everything lives in a single file on your machine, and nothing leaves your computer
except the Google sync you choose to run.
- 🔒 Private by design — localhost-only, your data stays on your machine.
- 💻 Runs anywhere — one-file downloads for Linux, Windows, and macOS; no Python needed.
- 🪶 Tiny — Flask + SQLite, no accounts, no cloud, no bloat.
Read the full guideShow less
Features
Your contacts
- People and companies — names, multiple emails and phones, and free-form notes.
- Photos & avatars — upload a photo, or let Google sync pull one in; a coloured initial is shown otherwise.
- Custom fields — add your own fields (birthday, address, anything) to any contact, no setup needed.
- Tags / labels — group contacts with free-text labels like "family", "work", "gym", and filter the list to any combination of them.
- Favourites — star the people you contact most to pin them to the top.
Finding and organising
- Fast search — by name, email, phone, notes, and custom-field values.
- Filter, sort, paginate — by contact type, first letter, or tag.
- Duplicate detection & merge — find likely duplicates and combine them field-by-field with nothing lost.
- Upcoming birthdays — a page listing whose birthday is coming up (and the age they'll turn).
Import / export
- CSV — import with a column-matching screen that remembers your choices, and export everything.
- vCard (.vcf) — import and export standard vCard files (custom fields round-trip).
Google Contacts sync (optional)
- Two-way — pull your Google contacts in, and push local edits and new contacts back; newest edit wins on conflicts. Deletions are never pushed. See the setup below.
Make it yours
- Settings page — timezone and date format, light/dark/colour themes, compact or roomy layout, list or card view, default phone region, contacts per page, and default sort — all remembered.
Under the hood
- Security-first — parameterized SQL, CSRF protection, autoescaped templates, and a
strict Content-Security-Policy. Binds to
localhostonly. - Light footprint — Flask + SQLite, vanilla HTML/CSS/JS, no build step, no ORM, no JS framework.
Get started
Option 1 — Download and run (easiest, no Python needed)
Grab the file for your operating system from the Releases page — each one is completely self-contained, so there's nothing else to install.
| Your OS | Download | How to run it |
|---|---|---|
| Linux | Contact-List-x86_64.AppImage |
Make it executable (chmod +x) and double-click, or run it from a terminal. |
| Windows | Contact-List.exe |
Double-click. If SmartScreen warns about an unknown app, choose More info → Run anyway. |
| macOS (Apple Silicon — M1/M2/M3 or newer) | Contact-List.dmg |
Open it, drag Contact List to Applications, then launch. The first time, right-click → Open to get past Gatekeeper. Intel Macs aren't supported by this build. |
Your contacts, photos, and settings are stored privately under
~/.config/contact-list/. To use Google sync you'll also add your own
credentials.json there — see Google Contacts sync.
Starting the app puts a small system-tray icon near your clock and otherwise stays out of your way — it does not throw a browser tab at you. Click the icon for Open Contact List, Restart, and Quit: a persistent control point that doesn't depend on having a browser tab open. Launching the app a second time while it's already running also just opens the page.
Where a desktop has no system tray at all (GNOME, unless you've added an AppIndicator extension), there'd be no icon to click — so in that case the app opens your browser on startup instead, rather than leaving you with no way to reach it.
Option 2 — Run from source (for developers)
Requirements: Python 3.12 or newer.
Linux only — for the tray icon, also install the system GTK/AppIndicator packages.
They can't come from pip (PyGObject has no binary wheel), so run.sh builds its
virtual environment with --system-site-packages to borrow the system's copies. Without
them everything still works; you just get no icon.
# openSUSE (python313 → match your system Python's version)
sudo zypper install python313-gobject typelib-1_0-Gtk-3_0 typelib-1_0-Gio-2_0 \
girepository-1_0 typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1
# Debian / Ubuntu
sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-glib-2.0 gir1.2-freedesktop \
gir1.2-ayatanaappindicator3-0.1 libayatana-appindicator3-1 libgtk-3-0
Downloaded releases need none of this — the AppImage carries its own copy.
git clone https://github.com/milnet01/contact-list.git
cd contact-list
./run.sh
run.sh creates a virtual environment, installs/updates dependencies on each launch, and
starts the app (via launcher.py, which shows the tray icon; it opens no browser unless
the tray is unavailable). Or do it by hand:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.py
Configuration
All settings are read from environment variables, with sensible defaults:
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
random per run | Flask session signing key. Set it to keep sessions stable across restarts. |
CONTACT_LIST_DB |
contacts.db next to the app (source) / under ~/.config/contact-list/ (downloaded app) |
Path to the SQLite database file. |
PORT |
unset | Port for an external process manager to bind. Must be an integer 1024–65535; anything else is a startup error (the app exits non-zero rather than silently using another port). Overrides CONTACT_LIST_PORT. Unset or empty means "not supplied" and changes nothing. |
CONTACT_LIST_PORT |
5002 |
Port the local server listens on. |
LWSM_MANAGED |
unset | Set to 1 to run without the system-tray icon (headless, logging to stdout). Anything else, including unset, shows the tray as normal. A presentation hint only — it grants nothing and changes nothing else. |
The database is created automatically on first run and is never committed to the repository.
Google Contacts sync (optional)
Syncing with Google needs your own Google Cloud OAuth credentials — the app never ships with any:
- In the Google Cloud Console, create a project and enable the People API.
- Create OAuth 2.0 client credentials of type Desktop app and download the JSON.
- Save it to
~/.config/contact-list/credentials.json. - Open the app's Sync page and connect — a browser window will ask you to authorise access.
Your access token is stored at ~/.config/contact-list/token.json with 0600 permissions,
never in the database or the repository.
Two-way sync. The app requests the read-write scope (contacts) so it can send
your changes back: local edits to synced contacts, and brand-new local contacts (which
become new Google contacts). When both sides changed the same contact since the last sync,
the newest edit wins. Deletions are not pushed — deleting a contact here leaves it
on Google. If you previously connected with the old read-only permission, the Sync page
will ask you to reconnect.
Running the tests
python -m pytest tests/ -v
./local-ci.sh runs the exact checks GitHub CI does (ruff, mypy, and the test suite across
Python 3.12 and 3.13) — handy before pushing.
Project layout
launcher.py Entry point for the packaged apps AND ./run.sh (starts server, runs the tray icon)
app.py Flask app factory; headless server when run directly (python app.py — no tray)
config.py Environment-based configuration
db.py SQLite connection management
models.py Data access layer (plain functions, no ORM)
resources.py Locates bundled files whether run from source or frozen
google_sync.py Google People API integration
google_auth.py Standalone OAuth helper
settings.py Per-user preferences
routes/ CRUD, sync, import/export, settings routes
templates/ Jinja2 server-rendered views
static/ CSS and minimal JavaScript
migrations/ Sequential SQL schema migrations
packaging/ PyInstaller spec + per-OS build scripts (AppImage / .exe / .dmg)
tests/ Unit and route tests
DESIGN.md Full design document and standards
See DESIGN.md for the architecture, data model, and security/efficiency standards in detail.
License
What's new · v1.1.0
See the release on GitHub.