Files
WealthySmart/codebase-review/01-architect.md
Carlos Escalante 5c265129e8 Add five-hat codebase review and phased improvement plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:19 -06:00

212 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architect Review — WealthySmart
> Hat: Staff-level Software Architect. Focus: structure, layering, data model, migration story, API consistency, deploy topology.
>
> **Editorial note:** ARCH-01 was corrected after verification — see the finding itself and `README.md`.
## Executive Summary
WealthySmart is a single-user personal finance management application with a modern full-stack architecture: FastAPI + SQLModel backend (PostgreSQL) paired with a React 19 + Vite frontend served through a Hono proxy, plus an LLM-powered AI agent interface. The system handles multi-currency transactions (CRC/USD/EUR/BTC/XMR), budget forecasting with complex billing-cycle logic, pension/municipal bill tracking, and push notifications. The architecture is sound overall — no fundamental redesign is needed — but there are weak defaults in configuration, zero test coverage, an ad-hoc migration story, and consistency gaps in validation and response schemas that should be addressed incrementally.
## How the System Fits Together
```
┌─────────────────────────────────────────────────────────────────┐
│ DEPLOYMENT (Docker Compose) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌─────────────────────┐ │
│ │ Frontend (Node) │ │ Backend (FastAPI) │ │
│ │ ┌─────────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ Vite (port 3000)│ │ │ │ Uvicorn │ │ │
│ │ │ React 19 + TS │ │ │ │ (port 8000) │ │ │
│ │ └─────────────────┘ │ │ └───────────────┘ │ │
│ │ ┌─────────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ Hono Server │ │ │ │ 15 endpoints │ │ │
│ │ │ (port 3001) │ │ │ │ + LLM agent │ │ │
│ │ │ • SPA serving │ │ │ └───────────────┘ │ │
│ │ │ • CopilotKit │ │ │ │ │
│ │ │ runtime │ │ │ │ │
│ │ │ • /api proxy │ │ │ │ │
│ │ └─────────────────┘ │ │ │ │
│ └──────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ └────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────┐ │
│ │ PostgreSQL 16 (port 5432) │ │
│ │ • 15 tables + enums │ │
│ │ • No Alembic migrations │ │
│ └──────────────────────────────────┘ │
│ │
│ Production: nginx-proxy + Let's Encrypt on single VPS │
│ Ingestion: n8n flows POST to /api/v1 with API tokens │
└─────────────────────────────────────────────────────────────────┘
```
**Key data flows:**
- Frontend requests `/api/v1/...` (proxied by Hono to FastAPI)
- Agent requests go to `/api/v1/agent/agui` (Microsoft Agent Framework + OpenAI)
- Exchange rates refreshed every 6 hours from multiple fallback APIs
- Auth via httpOnly cookies (SPA) or Bearer tokens (API clients / n8n)
## Strengths
1. **Modular endpoint structure** — 15 focused routers (accounts, transactions, budget, analytics, etc.) with clear single responsibilities.
2. **Sophisticated budget projection logic**`budget_projection.py` correctly handles billing cycles (18th18th for credit cards vs. 1st1st calendar month), deferred transactions, recurring items with override amounts, and cumulative balance tracking across years, with a clean API (`compute_monthly_projection`, `compute_yearly_projection_with_cumulative`).
3. **Resilient exchange-rate handling** — fallback chain (BCCR → ExchangeRate-API → currency-api → FloatRates → CoinGecko) with in-memory and database caching; uses a `_last_known_*` pattern to retain previous values during API outages.
4. **Clean auth model** — dual-mode authentication (httpOnly cookie for SPA, Bearer token for API clients) with JWT + API-token support. Token revocation marks rows inactive rather than deleting them.
5. **Seed data for development** — comprehensive default categories, accounts, and recurring items; single-user design appropriate for the app.
6. **Type-safe models** — SQLModel schemas enforce some constraints (unique categories, year/month combinations) at the DB level.
7. **REST conventions mostly respected** — POSTs return 201, pagination via `offset`/`limit` is consistently applied.
8. **Consistent error surface**`HTTPException` with clear status codes and detail messages throughout.
## Findings
### [ARCH-01] Weak secret hygiene around `.env` files and config defaults
**Severity:** High (downgraded from Critical after verification)
**Location:** `.env`, `.env.prod` (working tree), `backend/app/config.py:69`, `docker-compose.yml`
**Evidence (corrected):** `.env` and `.env.prod` exist in the repo root and contain real secrets (OpenAI API key, DB password, admin credentials, VAPID keys), but **they are gitignored and were never committed**`git ls-files` and `git log --all --diff-filter=A` confirm no history. The genuine issues are: (a) `config.py` ships working fallback credentials (`SECRET_KEY="change-me-in-production"`, `ADMIN_USERNAME/PASSWORD = "admin"`), (b) `docker-compose.yml` hardcodes dev DB credentials, and (c) plaintext secrets on the dev machine with no rotation discipline.
**Why it matters:** If a deploy ever runs without its env file (typo in compose, CI regression — note `reference_gitea_deploy`: `.env.prod` is regenerated each deploy), the app silently boots with `admin/admin` and a known JWT signing key.
**Recommendation:** Remove the defaults and fail fast at startup (see SEC-01). Keep `.env.example` as the documented template. Rotate any key that has been pasted into chats/logs.
### [ARCH-02] No Formal Database Migration Strategy
**Severity:** High
**Location:** `backend/app/db.py:1376`; no `alembic/` directory
**Evidence:** Migrations are ad-hoc SQL strings in `run_migrations()` with `IF NOT EXISTS` clauses. Adding a column requires hand-written `ALTER TABLE` code. Schema evolution is not version-controlled or reversible.
**Why it matters:** Manual migrations are error-prone and have no rollback. Dev and prod can diverge silently. The project rule "never reset prod DB; use migrations" makes this the single most important infrastructure gap.
**Recommendation:** Introduce Alembic: `alembic init`, autogenerate a baseline from current models, convert the existing manual statements, run `alembic upgrade head` at container startup. Effort: **M** (12 days).
### [ARCH-03] Validation & Error-Handling Inconsistencies
**Severity:** Medium
**Location:** `endpoints/transactions.py:7190`, `endpoints/budget.py:108109`, `endpoints/analytics.py:56`
**Evidence:** `list_transactions()` mixes `cycle_year/cycle_month` and `start_date/end_date` filters with `or_` in a nested query — unclear precedence. `budget.py` uses positional `HTTPException(400, ...)` while others use keyword args. `analytics.py:56` filters with the string literal `"COMPRA"` instead of `TransactionType.COMPRA`.
**Why it matters:** Mixing filter modes risks undefined behavior; hardcoded enum strings break refactoring safety; inconsistent error style hampers debugging.
**Recommendation:** Extract a `build_transaction_query()` helper with explicit precedence; standardize on `status_code=` kwargs; replace string literals with enum members. Effort: **S**.
### [ARCH-04] Agent Tools Duplicate Service Logic
**Severity:** Medium
**Location:** `backend/app/agent/tools.py`, `backend/app/services/budget_projection.py`
**Evidence:** `tools.py` re-implements queries (`get_accounts()`, `get_net_worth()`, `get_cycle_summary()`) that overlap with the service layer instead of delegating to it.
**Why it matters:** Budget logic changes must be applied in two places or the assistant and the UI disagree about the user's money.
**Recommendation:** Make agent tools thin wrappers over service functions. Effort: **M** (1 day).
### [ARCH-05] No Input Limits on Bulk Operations
**Severity:** Medium
**Location:** `endpoints/import_transactions.py:96149`
**Evidence:** `paste_import()` accepts arbitrary text with no size cap; a 10 MB paste is parsed entirely in memory before any validation.
**Why it matters:** Memory-exhaustion DoS vector; bulk operations should validate request size upfront.
**Recommendation:** `Field(..., max_length=1_000_000)` on the request model; pre-validate before opening the write loop. Effort: **S**.
### [ARCH-06] Missing Test Coverage
**Severity:** High
**Location:** No `tests/` directory anywhere; `requirements.txt` lacks pytest
**Evidence:** Zero test files in the repository.
**Why it matters:** Budget projection is the most intricate logic in the app (billing-cycle edges, deferred carryover, cumulative balances with overrides) and is entirely unguarded against regression.
**Recommendation:** `backend/tests/` with `test_budget_projection.py` (Feb 28/29, year boundaries, overrides), `test_auth.py`, `test_exchange_rate.py`; Vitest for `useBudget`. Effort: **L** (35 days for meaningful coverage).
### [ARCH-07] Single Hono Instance Is a Single Point of Failure
**Severity:** Low (revised for a single-user app)
**Location:** `frontend/server.ts:120`, `frontend/Dockerfile`, `docker-compose.prod.yml`
**Evidence:** One Hono container serves SPA + CopilotKit runtime + proxy. The Dockerfile healthcheck probes the backend's `/api/health`, not Hono's own readiness.
**Why it matters:** If Hono crashes the whole app is down even with a healthy backend. For a single-user app, replicas are over-engineering; a correct healthcheck + restart policy is the right-sized fix.
**Recommendation:** Healthcheck Hono itself; `restart: unless-stopped`; add 5xx retry in the frontend API client. Skip load balancing.
### [ARCH-08] Exchange-Rate Refresh Loop Swallows Cancellation
**Severity:** Low
**Location:** `backend/app/services/exchange_rate.py:348366`, `backend/app/main.py:66`
**Evidence:** `refresh_rates_periodically()` runs `while True` with a broad except that logs and continues; cancellation during shutdown can be silently swallowed.
**Recommendation:** Catch `asyncio.CancelledError` explicitly and `break`; keep the broad handler for everything else. Effort: **S**.
### [ARCH-09] No Foreign-Key Delete Rules
**Severity:** Medium
**Location:** `backend/app/models/models.py:144, 258, 427`
**Evidence:** `Transaction.category_id` and similar FKs declare `foreign_key=` with no `ondelete` behavior; deleting a category leaves dangling references.
**Why it matters:** Silent referential inconsistency — budget rows pointing at categories that no longer exist.
**Recommendation:** Decide per-FK between `SET NULL` (transactions should outlive categories) and `CASCADE` (water readings die with their receipt); apply via Alembic migration. Effort: **S**.
### [ARCH-10] Fragile Cookie Parsing in Agent Middleware
**Severity:** Medium
**Location:** `backend/app/main.py:88142`
**Evidence:** Cookie extraction uses regex `r"(?:^|;\s*)ws_token=([^;]+)"`, which mishandles edge cases (multiple cookies of the same name, unusual encodings).
**Recommendation:** Use stdlib `http.cookies.SimpleCookie`. Effort: **S**.
### [ARCH-11] `deferred_to_next_cycle` Implemented in Backend but Not Exposed in UI
**Severity:** Medium
**Location:** `backend/app/models/models.py:145`, `endpoints/transactions.py:219235`
**Evidence:** The field is settable via PATCH and respected by budget projection; the deferral toggle exists in row actions but `TransactionModal.tsx` has no field for it, and the affordance is undiscoverable (see UX-05).
**Recommendation:** Add the checkbox to `TransactionModal.tsx` and make the row-action state visible. Effort: **S**.
### [ARCH-12] Pagination Lacks a Deterministic Tiebreaker
**Severity:** Low
**Location:** `endpoints/transactions.py:96`, `endpoints/salarios.py:3536`
**Evidence:** Paginated queries order by `date DESC` only; equal dates yield non-deterministic page boundaries.
**Recommendation:** `.order_by(col(...).date.desc(), col(...).id.desc())` everywhere paginated. Effort: **S**.
### [ARCH-13] Mutation → Refetch-Everything Pattern
**Severity:** Low
**Location:** `frontend/src/hooks/useBudget.ts:6174`
**Evidence:** Every mutation triggers `Promise.all([fetchProjection(), fetchMonthDetail(), fetchRecurringItems()])` regardless of what changed.
**Why it matters:** Wasteful, and the pattern will multiply as modules grow. Acceptable as a simplicity tradeoff today; the durable fix is a query library (see FE-10 / Plan Phase 3).
### [ARCH-14] Hardcoded Dev DB Credentials in Compose
**Severity:** Medium
**Location:** `docker-compose.yml` (db service `environment:` block)
**Evidence:** `POSTGRES_PASSWORD: wealthy_pass` hardcoded in source control (prod compose correctly uses `${POSTGRES_PASSWORD}`).
**Recommendation:** Move dev credentials to the gitignored `.env`; reference via `${...}` in both compose files. Effort: **S**.
### [ARCH-15] N+1 Query in Category Aggregation
**Severity:** Medium
**Location:** `backend/app/services/budget_projection.py:370383`
**Evidence:** `compute_cc_by_category()` calls `session.get(Category, cat_id)` inside a loop — one query per category with spending.
**Recommendation:** Prefetch `{c.id: c for c in session.exec(select(Category)).all()}` and look up in the dict. Effort: **S** (minutes).
### [ARCH-16] API Response Schema Inconsistency
**Severity:** Low
**Location:** `endpoints/transactions.py:2833` (Pydantic models) vs `endpoints/budget.py:100157` (raw dicts) vs `endpoints/analytics.py:1823`
**Evidence:** Some endpoints return typed models, others plain dicts; OpenAPI schema generation is incomplete and frontend types are maintained by hand.
**Recommendation:** Shared models in `app/api/schemas.py` + `response_model=` on every route; this also unlocks frontend type codegen (see FE-14). Effort: **M**.
### [ARCH-17] Agent Session via ContextVar Couples Tools to HTTP Middleware
**Severity:** Low
**Location:** `backend/app/agent/tools.py:4052`, `backend/app/main.py:131141`
**Evidence:** Tools fetch the DB session from a `contextvars.ContextVar` set by middleware; calling a tool outside a request raises `LookupError`.
**Recommendation:** Document the constraint and add a clear error message (see BE-12); full refactor only if tools ever need to run outside HTTP. Effort: **S** for the guard.
### [ARCH-18] No Data Export / Archival Path
**Severity:** Low
**Location:** All tables are append-only with no export endpoint
**Why it matters:** Acceptable growth for a personal app, but there is no way to get historical data out (taxes, accountant, backup verification).
**Recommendation:** CSV export endpoint per major table (also UX wishlist #5). Effort: **M**.
### [ARCH-19] Hono Proxy Trusts Upstream Blindly
**Severity:** Low
**Location:** `frontend/server.ts` proxy handlers
**Evidence:** Responses are piped through without shape validation; combined with FE-14 (no runtime validation client-side), schema drift fails confusingly.
**Recommendation:** Don't validate in the proxy (wrong layer); fix via shared response schemas (ARCH-16) and optional Zod at the API-client boundary.
### [ARCH-20] Lazy Imports Papering Over a Circular Dependency
**Severity:** Low
**Location:** `backend/app/auth.py:3943`
**Evidence:** `_validate_token()` imports `get_session` and `APIToken` inside the function body to dodge a cycle with `db.py`.
**Recommendation:** Split token utilities into their own module to break the cycle properly. Effort: **S**.
## Recommended Refactors (prioritized)
| # | Item | Effort | Phase |
|---|---|---|---|
| 1 | ARCH-01/SEC-01: fail-fast on default secrets, fix dev compose creds | S | 0 |
| 2 | ARCH-02: Alembic migrations | M | 1 |
| 3 | ARCH-06: test suite (budget projection first) | L | 1 |
| 4 | ARCH-15: N+1 fix in `compute_cc_by_category` | S | 2 |
| 5 | ARCH-03: standardize validation/error handling/enums | S | 2 |
| 6 | ARCH-09: FK delete rules via migration | S | 2 |
| 7 | ARCH-04: agent tools delegate to services | M | 2 |
| 8 | ARCH-16: shared response schemas (`response_model=` everywhere) | M | 3 |
| 9 | ARCH-13/FE-10: query library on the frontend | M | 3 |
| 10 | ARCH-11: expose deferred-transaction UI | S | 4 |
| 11 | ARCH-18: CSV export | M | 4 |
| 12 | ARCH-05/08/10/12/14/17/20: small hardening fixes | S each | 02 |
**Overall assessment:** Production-ready for a single-user personal app, with the caveat that configuration defaults must fail fast before anything else. The architecture needs no redesign — invest in migrations, tests, and consistency.