Detailed Technical Reference¶
Architecture Overview¶
Finance Manager is a two-domain financial management application:
- Assets Domain — Portfolio tracking and market data analysis
- 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
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 transactionsGET /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¶
GET /api/assetsis NOT read-only- Writes current prices to price_cache table
-
Used to build price history
-
Frontend polling creates the price history
- No separate scheduler
-
Historical data depends on active dashboard usage
-
Response field named
description_encryptedeven after decryption - Frontend and tests depend on this field name
-
Actually contains decrypted plaintext
-
Endpoint ordering matters in main.py
/api/assets/analyzemust come before/api/assets/{symbol}/details- FastAPI pattern matching is greedy
For implementation roadmap, see Improvement Roadmap.