- Instead of entering a hard-to-calculate rate like 0.00773, users now enter a real sample (e.g. receipt=1000 KES, claimed=7.73 USD) - The system computes the rate automatically: 7.73 / 1000 = 0.00773 - Users can get the sample values from their payment app notification - Much more intuitive, especially for currencies with small exchange rates |
||
|---|---|---|
| internal | ||
| static | ||
| templates | ||
| .env.example | ||
| .gitignore | ||
| build-all.sh | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
ExpenseFlow — AI-Powered Expense Tracker
A production-ready, mobile-first Progressive Web App (PWA) that uses passwordless email OTP login, event-based expense tracking, AI receipt extraction (DeepSeek Vision), and event filing (CSV/PDF via email).
Tech Stack: Go 1.22+ · HTMX · SQLite · DeepSeek Vision API · PWA
📦 Quick Start — Binary Distribution
Pre-compiled binaries are available for download from the Releases page.
Download & Run
# Linux (amd64)
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-linux-amd64
chmod +x expenseflow
./expenseflow
# Linux (arm64) — Raspberry Pi, etc.
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-linux-arm64
chmod +x expenseflow
./expenseflow
# macOS (Intel)
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-darwin-amd64
chmod +x expenseflow
./expenseflow
# macOS (Apple Silicon M1/M2/M3)
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-darwin-arm64
chmod +x expenseflow
./expenseflow
The server starts on http://localhost:8080 by default. Set PORT=3000 to change the port.
Note: The binary embeds no configuration. You must create a
.envfile (see Configuration below) in the same directory you run the binary from.
🔧 Building from Source
Prerequisites
-
Go 1.22+ — Download
-
GCC (CGO is required for the SQLite driver)
# Debian/Ubuntu sudo apt install build-essential # macOS xcode-select --install # Alpine apk add build-base
Clone & Build
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
cd ExpenseFlow
# Build for your current platform
go build -o expenseflow .
# The binary is now ready: ./expenseflow
Cross-Compilation
The binary uses CGO (for mattn/go-sqlite3), so cross-compilation requires a cross-compiler.
# Linux amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-linux-gnu-gcc go build -o expenseflow-linux-amd64 .
# Linux arm64 (Raspberry Pi, etc.)
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -o expenseflow-linux-arm64 .
# macOS Intel
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CC=o64-clang go build -o expenseflow-darwin-amd64 .
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-apple-darwin-clang go build -o expenseflow-darwin-arm64 .
Tip: For macOS cross-compilation from Linux, use osxcross. For ARM Linux, install
gcc-aarch64-linux-gnu.
⚙️ Configuration
Copy .env.example to .env and fill in your credentials:
cp .env.example .env
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST |
Yes* | — | SMTP server hostname |
SMTP_PORT |
Yes* | — | SMTP server port (usually 587) |
SMTP_USER |
Yes* | — | SMTP username |
SMTP_PASS |
Yes* | — | SMTP password |
DEEPSEEK_API_KEY |
Yes* | — | DeepSeek Vision API key |
BASE_URL |
No | http://localhost:8080 |
Public URL for email links |
PORT |
No | 8080 |
HTTP server port |
* The app will start without SMTP/DeepSeek configured, but OTP emails and AI extraction will not work.
Getting Credentials
- DeepSeek Vision API: Sign up at platform.deepseek.com and create an API key.
- SMTP: Use any SMTP provider. The default config points to an OX hosting SMTP server.
🚀 Usage
1. Start the Server
./expenseflow
2. Open in Browser
Navigate to http://localhost:8080
3. Full Acceptance Flow
1. Enter your email → click "Send Verification Code"
2. Check your inbox for the 6-digit OTP code
3. Enter the OTP → click "Verify Code"
4. Create an event (e.g., "WebSummit 2026")
5. Click "Add Expenses" on the event card
6. Click "Capture Receipt" → take a photo or select an image
7. AI extracts: amount, merchant, category, date → pre-fills the form
8. Review and click "Save Expense"
9. Click "File Event" → enter recipient email → choose CSV or PDF
10. Report is emailed and event status changes to "closed"
11. Click "Reopen" to re-open a closed event
📡 API Reference
Public Endpoints (no authentication)
| Method | Path | Description |
|---|---|---|
GET |
/ |
Landing page with email login form |
POST |
/request-otp |
Request a 6-digit OTP code (sends email) |
POST |
/verify-otp |
Verify OTP code and create session |
POST |
/logout |
Clear session and redirect to login |
Protected Endpoints (require session cookie)
| Method | Path | Description |
|---|---|---|
GET |
/dashboard |
Event dashboard |
POST |
/events |
Create a new event |
GET |
/events/{id}/expenses |
View event with expense list |
PUT |
/events/{id}/reopen |
Reopen a closed event |
POST |
/expenses/upload |
Upload receipt image (multipart) |
POST |
/expenses |
Save expense from form data |
POST |
/events/{id}/file |
Generate report (CSV/PDF) and email it |
Static Files
| Path | Description |
|---|---|
/static/css/style.css |
Application stylesheet |
/static/icons/icon-192.png |
PWA icon (192×192) |
/static/icons/icon-512.png |
PWA icon (512×512) |
/manifest.json |
PWA manifest |
/sw.js |
Service worker |
/storage/{filename} |
Uploaded receipt images |
🏗️ Project Structure
ExpenseFlow/
├── main.go # Entry point, router, middleware, server
├── go.mod / go.sum # Go module definition
├── .env.example # Environment variable template
├── README.md # This file
├── internal/
│ ├── database/db.go # SQLite init, auto-migration, 11 query functions
│ ├── auth/
│ │ ├── otp.go # 6-digit OTP generation + 3-fail lockout
│ │ └── session.go # In-memory session store (crypto tokens, 24h TTL)
│ ├── handlers/
│ │ ├── auth.go # Auth endpoints + middleware
│ │ ├── events.go # Event CRUD + dashboard
│ │ ├── expenses.go # Receipt upload, AI extraction, save
│ │ └── file.go # CSV/PDF generation + email filing
│ ├── ai/deepseek.go # DeepSeek Vision API client
│ ├── email/smtp.go # SMTP sender (OTP + attachments)
│ └── utils/uuid.go # UUID generation
├── templates/
│ ├── index.html # Landing page
│ ├── dashboard.html # Event cards + create form
│ ├── event_expenses.html # Event detail + capture + filing
│ ├── receipt_form.html # AI-prefilled edit form
│ └── expense_list.html # HTMX expense list fragment
├── static/
│ ├── css/style.css # Mobile-first responsive CSS (1845 lines)
│ ├── manifest.json # PWA manifest
│ ├── sw.js # Service worker
│ └── icons/ # PWA placeholder icons
└── storage/ # Uploaded receipts (created at runtime)
🧩 Features in Detail
🔐 Passwordless OTP Authentication
- Email-based 6-digit code, 5-minute expiry
- Auto-creates user account on first login
- 3 failed attempts trigger a 1-minute cooldown
- HTTP-only session cookie (
SameSite=Lax, 24h TTL) - No passwords to store or forget
📋 Event-Based Expense Tracking
- Group expenses into events (trips, conferences, months)
- Open/closed lifecycle with reopen support
- Dashboard with event cards showing name, status, and creation date
🤖 AI Receipt Extraction
- Upload receipt images (JPEG/PNG, max 10 MB)
- DeepSeek Vision API extracts: amount, currency, merchant, category, date
- Editable pre-filled form on failure or success
- Images stored locally in
./storage/
📧 Email Reporting
- Generate CSV (via
encoding/csv) or PDF (viagofpdf) - Automatic email delivery via SMTP with file attachment
- Event auto-closes after successful filing
- Multipart MIME support with proper content headers
📱 Progressive Web App
- Installable on mobile and desktop (manifest.json)
- Offline shell caching (service worker)
- Camera capture for receipts (
capture="environment") - Theme color:
#10b981(emerald green) - Responsive design: 320px → 768px → 1024px+
🗄️ Database Schema (SQLite)
Auto-created on first run — 4 tables with foreign keys:
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE auth_otps (
email TEXT PRIMARY KEY,
otp_code TEXT NOT NULL,
expires_at DATETIME NOT NULL
);
CREATE TABLE events (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT CHECK(status IN ('open', 'closed')) DEFAULT 'open',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE expenses (
id TEXT PRIMARY KEY,
event_id TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT NOT NULL,
merchant TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
date TEXT NOT NULL,
image_path TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(event_id) REFERENCES events(id) ON DELETE CASCADE
);
🐳 Docker
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache build-base
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o expenseflow .
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/expenseflow .
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/static ./static
COPY --from=builder /app/.env.example ./.env.example
EXPOSE 8080
CMD ["./expenseflow"]
docker build -t expenseflow .
docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage expenseflow
🔒 Security
| Area | Implementation |
|---|---|
| Sessions | Cryptographically random tokens (32 bytes, hex-encoded), in-memory store, 24h TTL |
| OTP | 6-digit codes from crypto/rand, 5-minute expiry, 3-fail lockout (1 minute) |
| Cookies | HTTP-only, SameSite=Lax, path restricted |
| SQL Injection | Parameterized queries on all database operations |
| File Upload | Magic byte validation (JPEG/PNG), 10 MB limit, sanitized filenames (UUID) |
| Credentials | All secrets via environment variables only — never hardcoded |
| HTMX | Server-rendered HTML, no client-side data exposure |
🧪 Development
Run Tests
go vet ./...
go test ./...
Manual Smoke Test
# Start server
go run main.go &
# Test landing page
curl -s http://localhost:8080/ | head -5
# Test OTP request
curl -s -X POST -d "email=test@example.com" http://localhost:8080/request-otp
# Check database
sqlite3 expenses.db "SELECT * FROM auth_otps;"
📄 License
MIT — Free to use, modify, and distribute.
🙌 Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit changes (
git commit -am 'feat: add my feature') - Push (
git push origin feature/my-feature) - Open a Pull Request
Built with Go, HTMX, SQLite, and ❤️