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

18 KiB
Raw Permalink Blame History

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 logicbudget_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 surfaceHTTPException 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 committedgit 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.

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.

# 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.