Partiu is a self-hosted alternative to TripIt — a personal flight tracker PWA that automatically reads your airline confirmation emails, parses the flight details, and organises everything into trips. No third-party account needed, no data leaving your server.
Want to try it before self-hosting? A public demo is available at:
| Field | Value |
|---|---|
| Username | demo |
| Password | demo1234 |
Note: The demo is reset every 6 hours with fresh sample data. Any changes you make will be wiped on the next reset.
OLLAMA_URL is set, unknown-airline emails are sent to a local LLM as a last resort; output is validated (IATA codes, flight number format, required fields) against the airports DB before import — invalid data is rejected silently| Airline | IATA Code |
|---|---|
| LATAM Airlines | LA |
| SAS Scandinavian Airlines | SK |
| Norwegian Air Shuttle | DY |
| Azul Brazilian Airlines | AD |
| Lufthansa | LH |
| British Airways (+ Iberia legs on BA itineraries) | BA |
| ITA Airways | AZ |
| Kiwi.com (multi-airline bookings) | — |
| Ryanair | FR |
| Austrian Airlines | OS |
| TAP Air Portugal (check-in, boarding pass, booking confirmation, e-ticket receipt) | TP |
| Finnair | AY |
| Wizz Air | W6 |
More airlines can be added by contributing a new rule (see Contributing).
Partiu is designed to run on your own server — a VPS, a Raspberry Pi, or anything that can run Docker. Your flight data stays on your machine.
SECURE_COOKIES=false to disable if needed)git clone https://github.com/your-username/partiu
cd partiu
cp .env.example .env
# Edit .env — at minimum set SECRET_KEY (see below)
docker compose up -d --build
Open https://your-domain and complete the first-run setup to create your admin account.
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
✓ | Secret key for signing session cookies. Generate with openssl rand -hex 32 |
DB_PATH |
Path to the SQLite database (default: ./data/partiu.db) |
|
DISABLE_SCHEDULER |
Set to true to disable background email sync (useful for dev) |
|
AVIATIONSTACK_API_KEY |
Free API key for aircraft type lookup |
All other settings (Gmail credentials, sync interval, SMTP server) are configured per-user or by the admin through the Settings page in the UI.
On first visit, Partiu shows a setup page to create the admin account. After logging in, go to Settings to configure your Gmail address and App Password.
Each user can enable TOTP-based 2FA from Settings → Two-Factor Authentication. Use any authenticator app (Google Authenticator, Authy, 1Password, etc.).
Provides aircraft type (e.g. Boeing 737-800) for airborne flights. The free plan includes 100 requests/month, enough for personal use.
.env as AVIATIONSTACK_API_KEYFalls back to OpenSky Network (free, no account needed) if not set.
Immich is a self-hosted photo management platform. Partiu can automatically create an Immich album for each completed trip using photos taken during the trip's date range.
Setup:
In your Immich instance, go to Account Settings → API Keys and create a new API key
Grant the following permissions to the key:
| Permission | Why |
|---|---|
asset.read |
Find photos within the trip date range |
asset.view |
Access asset metadata |
asset.download |
Required alongside read for full access |
asset.copy |
Needed for album operations |
album.create |
Create the trip album |
album.read |
Read album details |
album.update |
Update album contents |
albumAsset.create |
Add photos to the album |
In Partiu, go to Settings → Immich and enter:
https://photos.yourdomain.com)Click Test Connection to verify — then Save
Once configured, a Create Immich Album button will appear on any completed trip (in both the trip detail view and the history page). If an album was already created for that trip, the button changes to Open Immich Album and takes you directly to it.
To recreate an album, delete it in Immich first — the button will revert to "Create".
Known issue: when tapping "Open Immich Album" on iOS/Android, the deep link navigates correctly to the album only if the Immich app is already open. If the app is fully closed, it will launch but land on the home page instead of the album. This is a bug in the Immich mobile app's cold-start deep link handling — tracked at immich-app/immich#27069.
Instead of — or in addition to — Gmail IMAP sync, you can forward emails directly to Partiu:
2525)your-server:2525This is useful if you use a non-Gmail provider or want instant processing without waiting for the next IMAP poll.
| DNS record | Value |
|---|---|
A mail.yourdomain.com |
Your server's public IP |
MX yourdomain.com |
mail.yourdomain.com (priority 10) |
Forward port 25 (or 2525) on your router/firewall to the server running Partiu.
curl -LsSf https://astral.sh/uv/install.sh | sh)bash run.sh
run.sh starts the backend and frontend in parallel and stops both when you press Ctrl+C.
| Service | URL |
|---|---|
| Backend API | http://localhost:8000 |
| Frontend dev server | http://localhost:5173 |
Note: For local development, set
SECURE_COOKIES=falsein your.env— no HTTPS or certificate needed. Browsers also treatlocalhostas a secure context, sosecurecookies work over plain HTTP on localhost even without this flag.
uv sync
uv run uvicorn backend.main:app --reload
cd frontend
npm install
npm run dev # Vite dev server at localhost:5173
# Backend unit tests
uv run pytest backend/tests/ -v --cov=backend --cov-fail-under=70
# E2E tests (requires a running server at localhost:8000)
uv run playwright install chromium
uv run pytest frontend/tests/ -v
Several developer tools live in backend/tools/ for working with the parser pipeline and the LLM fallback.
Runs one or two Ollama models against a list of .eml files and prints a side-by-side comparison. Useful for evaluating LLM prompt changes against known flight emails.
# Single file
uv run python -m backend.tools.eval_eml_files ~/Downloads/flight.eml
# Glob of files
uv run python -m backend.tools.eval_eml_files ~/Downloads/*.eml
# Compare two models
uv run python -m backend.tools.eval_eml_files ~/Downloads/*.eml \
--models qwen2.5:0.5b,qwen2.5:1.5b
# Save full JSON report
uv run python -m backend.tools.eval_eml_files ~/Downloads/*.eml --output data/eml_compare.json
Compares two JSON result files produced by eval_eml_files and reports which emails one model extracted but the other didn't.
uv run python -m backend.tools.compare_eval \
--a data/eval_1.5b.json \
--b data/eval_0.5b.json \
--output data/eval_diff.json
The LLM fallback was evaluated against 114 real flight emails using two small local models via Ollama:
| Model | Extracted | Invalid data | Rejected |
|---|---|---|---|
qwen2.5:1.5b |
0 (0%) | 2 (2%) | 112 (98%) |
qwen2.5:0.5b |
21 (18%) | 57 (50%) | 36 (32%) |
Key findings:
qwen2.5:1.5b is too conservative — it rejects nearly everything, including obvious booking confirmation emails.qwen2.5:0.5b finds more flights but has a high invalid-data rate: it frequently confuses booking references with flight numbers, uses city names instead of IATA codes, and invents today's date instead of reading the date from the email. Most of these are caught by validation and never reach the DB.Conclusion: the LLM fallback is not reliable enough to replace parser rules for known airlines. It may work better with a larger or more capable model (e.g. llama3, mistral, or a cloud API), but this has not been tested.
To improve results, edit the system prompt in backend/llm_parser.py (_PROMPT_SYSTEM) and re-run eval_eml_files against known flight emails to measure the impact.
| Layer | Technology |
|---|---|
| Backend | FastAPI + APScheduler + SQLite (WAL mode) |
| Frontend | Svelte 5 + Vite (PWA) |
| Auth | Session cookies (itsdangerous) + bcrypt + TOTP 2FA |
| Email fetch | Gmail IMAP with App Password (per user) |
| Email receive | aiosmtpd (inbound SMTP server) |
| HTML parsing | BeautifulSoup4 + lxml |
| Aircraft data | AviationStack (primary) + OpenSky Network (fallback) |
| Photo albums | Immich (optional, self-hosted) |
Contributions are welcome! Here are the most impactful ways to help:
See CONTRIBUTING_PARSERS.md for the full step-by-step guide.
The short version:
# 1. Anonymize your .eml fixture
uv run python tools/anonymize_eml.py ~/Downloads/confirmation.eml \
--out backend/tests/fixtures/myairline_anonymized.eml
# 2. See what gets extracted (or what's missing)
uv run python tools/parse_eml.py backend/tests/fixtures/myairline_anonymized.eml
# 3. Add rule + extractor, then scaffold a test automatically
uv run python tools/parse_eml.py backend/tests/fixtures/myairline_anonymized.eml \
--generate-test --out backend/tests/test_myairline_parser.py
Open an issue and attach (or paste) the relevant parts of the email — subject line, sender address, and the text body (redact personal info if needed). HTML structure matters most.
sqlite3, no ORM, plain dicts)$state / $derived runes, no extra UI frameworksMIT