Skip to content

Detailed Technical Reference

Architecture Overview

Finance Manager is a two-domain financial management application:

  1. Assets Domain — Portfolio tracking and market data analysis
  2. Transactions Domain — Bank import, AI categorization, and spending analysis

Built with FastAPI backend, React frontend, PostgreSQL database (Uberspace production), and local Ollama LLM.

Core Technologies

Backend Stack

  • Framework: FastAPI 0.100+
  • ORM: SQLAlchemy with SQLModel
  • Database: PostgreSQL (production) / SQLite (development)
  • LLM: Ollama (local default), OpenAI/Gemini (optional)
  • Market Data: Finnhub API (primary) → Alpha Vantage (fallback)
  • Security: Fernet encryption, HMAC blind indexing, OAuth2

Frontend Stack

  • Framework: React 18 with TypeScript
  • Build Tool: Vite
  • Styling: Tailwind CSS + inline CSS
  • HTTP Client: Axios (with custom interceptors)
  • State Management: React hooks + Context API

Infrastructure

  • Deployment: Docker Compose (local stack)
  • Hosting: Uberspace (PostgreSQL, oauth2-proxy)
  • CI/CD: GitHub Actions
  • Reverse Proxy: oauth2-proxy (production)

20+ Identified Improvements

Critical (Blocks Scalability)

1. Monolithic main.py

Status: Unresolved
Severity: HIGH
Impact: Maintainability, testing, code review friction

Problem:
All ~50 API endpoints in single main.py file. No APIRouter pattern. Makes code review slow and testing harder.

Solution:
Split into modules:

backend/
├── main.py (minimal)
├── routers/
│   ├── assets.py
│   ├── transactions.py
│   ├── settings.py
│   ├── ai.py
│   └── health.py

Effort: Medium (2-3 hours)
Benefit: Easier testing, maintenance, clearer responsibilities

2. Endpoint Ordering Dependency

Status: Unresolved
Severity: HIGH
Impact: Fragile, error-prone, hard to debug

Problem:
/api/assets/analyze must come BEFORE /api/assets/{symbol}/details in main.py or FastAPI pattern matching treats "analyze" as a symbol.

# WRONG ORDER - analyze endpoint never reached
@app.get("/api/assets/{symbol}/details")
def get_details(symbol: str):
    ...

@app.get("/api/assets/analyze")  # Never matches - "/api/assets/analyze" treated as symbol="analyze"
def analyze_all():
    ...

Solution:
Use explicit parameter validation or reverse order. Better: use APIRouter + clear route patterns.

Effort: Low (30 min)
Benefit: Eliminate fragile ordering requirement

3. No Encryption Key Versioning

Status: Unresolved
Severity: HIGH
Impact: Blocks key rotation, production deployment

Problem:
ALE_KEY and BLIND_INDEX_KEY must remain constant forever. No way to rotate. If compromised, all encrypted data at risk forever.

Solution:
1. Add key_version column to Transaction table 2. Store multiple versions of keys 3. Implement rotation with versioning:

# On key rotation
encrypted_new = Fernet(new_key).encrypt(decrypted)
transaction.key_version = 2
transaction.description_encrypted = encrypted_new
4. Document rotation procedure

Effort: Medium (2-3 hours)
Benefit: Enable key rotation; essential for security compliance

High Priority (Impacts Feature Development)

4. Frontend Theme Inconsistency

Status: Unresolved
Severity: MEDIUM
Impact: Maintenance burden, slower development

Problem:
- Asset screens: Dark theme + inline CSS styles - Transaction screens: Light theme + Tailwind utility classes - Makes development slower (always deciding which approach)

Solution:
Standardize on Tailwind CSS with single theme system.

Effort: Medium (4-6 hours)
Benefit: Consistent UX, faster development, easier theme changes

5. No API Versioning

Status: Unresolved
Severity: MEDIUM
Impact: Backward compatibility issues

Problem:
All endpoints at /api/.... If API changes, breaking changes for mobile clients, browser tabs, integrations.

Solution:
Add /api/v1/ prefix:

app = FastAPI()
v1_router = APIRouter(prefix="/api/v1")

@v1_router.get("/assets")
def get_assets():
    ...

app.include_router(v1_router)

Effort: Low (1 hour)
Benefit: Backward compatibility, safe API evolution

6. Hardcoded Transaction Categories

Status: Unresolved
Severity: MEDIUM
Impact: Blocks Budget feature, inflexible

Problem:
6 categories hardcoded as LLM prompt strings:

categories = ["Miete", "Lebensmittel", "Versicherungen", "Freizeit", "Gehalt", "Sonstiges"]

Budget feature needs flexible mapping. Can't extend or customize.

Solution:
Create TransactionCategory table:

CREATE TABLE transaction_category (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255),
    description TEXT,
    user_id INT REFERENCES "user"(id),
    created_at TIMESTAMP
);

Effort: Medium (3-4 hours)
Benefit: Support Budget feature, user customization

7. Frontend Polling Without Backoff

Status: Unresolved
Severity: MEDIUM
Impact: Resilience, thundering herd risk

Problem:
30-second hard-coded polling cadence:

setInterval(() => fetchAssets(), 30000);  // No jitter, no backoff

If server errors, all clients retry immediately (thundering herd).

Solution:

const exponentialBackoff = (attempt) => 
  Math.min(30 + Math.random() * 5, 30 * Math.pow(1.5, attempt));

// Implements jitter + exponential backoff

Effort: Low (1-2 hours)
Benefit: Better resilience, reduced server load spikes

Medium Priority (Quality of Life)

8. No Request/Response Validation Models

Status: Unresolved
Severity: MEDIUM
Impact: Type safety, documentation

Problem:
Endpoints lack Pydantic models. No auto-generated OpenAPI docs.

Solution:
Add models:

class AssetCreate(BaseModel):
    symbol: str
    quantity: float
    avg_cost: float

@app.post("/api/v1/assets")
def create_asset(asset: AssetCreate):
    ...

Effort: Medium (2-3 hours)
Benefit: Type safety, better IDE support, auto-generated API docs

9. No Error Handling Middleware

Status: Unresolved
Severity: MEDIUM
Impact: Inconsistent error responses

Problem:
Errors return different formats. No standardized error codes.

Solution:

@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={"error": str(exc), "code": "INTERNAL_ERROR"}
    )

Effort: Low (1-2 hours)
Benefit: Consistent error handling, better debugging

10. Insufficient Test Coverage

Status: Unresolved
Severity: MEDIUM
Impact: Confidence in critical paths

Problem:
Limited tests. Transaction encryption pipeline not fully covered.

Solution:
Add tests: - Transaction encryption roundtrips - Blind index searchability - Fingerprint deduplication - Endpoint error cases

Effort: High (6-8 hours)
Benefit: Confidence, catch regressions early

Low Priority (Nice to Have)

11-14. Additional Improvements

  • Structured request logging (LOW effort, LOW impact)
  • Feature flags system (MEDIUM effort, LOW impact)
  • Parallel market data requests (LOW effort, LOW impact)
  • API rate limiting (MEDIUM effort, LOW impact)

Architecture Decision Records (ADRs)

ADR-1: Encryption Strategy for Transactions

Decided: Use Fernet (AES-256) + HMAC blind indexing
Rationale: Balance security with searchability
Trade-off: Cannot search encrypted descriptions without blind index

ADR-2: LLM Categorization via Ollama

Decided: Local Ollama by default, OpenAI/Gemini as upgrades
Rationale: Privacy-first, works offline, upgradable
Trade-off: Depends on local resources

ADR-3: Price History via Polling

Decided: Frontend polling creates price history
Rationale: Simpler than separate scheduler
Trade-off: Depends on active dashboard usage

ADR-4: Market Data Fallback

Decided: Finnhub primary, Alpha Vantage fallback
Rationale: Finnhub more reliable; fallback ensures availability
Trade-off: Different data formats require reconciliation


Performance Considerations

Frontend Polling Cadence

  • Current: 30 seconds (hard-coded)
  • Recommended: Configurable (15-60s user preference)
  • Future: WebSocket for real-time updates

Database Query Optimization

  • Index on: asset(symbol), transaction(user_id, date)
  • Denormalization: Consider price_cache materialization

API Response Times

  • GET /api/assets → ~200ms (includes market data fetch)
  • POST /api/transactions/upload → ~2s per 100 transactions
  • GET /api/transactions → ~50ms (cached decryption)

Security Considerations

Encryption

  • ✅ Fernet (AES-128 in CBC mode + HMAC)
  • ✅ Blind indexing (HMAC deterministic)
  • ✅ Key stability (never rotate without versioning)

Authentication

  • ✅ OAuth2 (production via oauth2-proxy)
  • ✅ Token-based (development)

Data Isolation

  • ✅ User-scoped queries (all transactions filtered by user_id)
  • ✅ No cross-user data leaks

External API Security

  • ⚠️ API keys stored in environment variables
  • ⚠️ Consider secrets management (HashiCorp Vault, AWS Secrets Manager)

Non-Obvious Behaviors

  1. GET /api/assets is NOT read-only
  2. Writes current prices to price_cache table
  3. Used to build price history

  4. Frontend polling creates the price history

  5. No separate scheduler
  6. Historical data depends on active dashboard usage

  7. Response field named description_encrypted even after decryption

  8. Frontend and tests depend on this field name
  9. Actually contains decrypted plaintext

  10. Endpoint ordering matters in main.py

  11. /api/assets/analyze must come before /api/assets/{symbol}/details
  12. FastAPI pattern matching is greedy

For implementation roadmap, see Improvement Roadmap.