Backend
Django + DRF
Batteries-included: ORM, migrations, auth, admin — all built in. Python is natural for financial math (inflation adjustment, CAGR, weighted averages). DRF gives serialization and viewsets with minimal boilerplate.
Trade-off: Synchronous. No async views. Acceptable because BRAPI calls are infrequent (lazy 24h refresh) and indicator computation is CPU-bound, not I/O-bound.
PostgreSQL
Financial data is inherently relational — earnings belong to tickers, tickers have balance sheets, users own favorites. ACID guarantees matter when writing quarterly financials. Unique constraints prevent duplicate data on re-sync.
Trade-off: Heavier than SQLite for a single-server app. Worth it for concurrent writes, proper indexing on LookupLog for rate limiting queries, and production reliability. SQLite used in dev for zero-config convenience.
Gunicorn
Standard WSGI server. Simple. Battle-tested. No configuration complexity — just workers and a bind address behind Nginx.
Trade-off: No WebSocket support. Not needed — all data flows are request/response.
Session auth (not JWT)
SPA is same-origin — Django sessions just work. No token refresh logic, no client-side token storage, no JWT library. CSRF exempted for same-origin API calls; CORS handles cross-origin.
Trade-off: Sessions are server-side state. Fine for a single-server deployment. Would need sticky sessions or a session store (Redis) to horizontally scale — not a current concern.
Pillow for OG images
Server-rendered PNG with financial metrics baked in. No headless browser needed. Fast, lightweight, deterministic output.
Trade-off: Pixel-level layout in Python is tedious. Limited typographic control. Acceptable because OG images are simple data cards, not rich designs.
Frontend
React 19 + TypeScript
Component model fits the UI well — metrics cards, charts, comparison tables are natural components. TypeScript catches shape mismatches in API responses (financial data has many optional fields).
Trade-off: SPA means no SSR, so initial load is slower and SEO depends on OG images. Acceptable for a tool used by logged-in analysts, not a content site.
TanStack Router
Type-safe routing — route params like /:ticker are typed, so typos in links are caught at build time. File-based route definition keeps routing colocated with pages.
Trade-off: Smaller ecosystem than React Router. Less community support. Worth it for the type safety on a data-heavy app where URL params drive everything.
TanStack Query
Financial data is server state, not client state. Query handles caching, deduplication, background refetch, and stale-while-revalidate — exactly what's needed when 50 tickers might be loaded in a comparison view.
Trade-off: Another abstraction layer. But eliminates entire categories of bugs (stale data, race conditions, cache invalidation) that would otherwise need manual management.
Recharts
React-native charting — components compose naturally. Good defaults for line charts (historical prices, year-end multiples). Responsive out of the box.
Trade-off: Less customizable than D3. Can't do every exotic visualization. Sufficient for the two chart types Sponda needs (price history + multiples bars).
Fuse.js
~800 stocks loaded once, searched entirely client-side. No server round-trip on every keystroke. Fuzzy matching handles typos ("PETR" finds "PETR4").
Trade-off: ~200KB of ticker data loaded upfront. Negligible on modern connections. Would need server-side search if the dataset grew 100x.
Tailwind CSS v4
Rapid styling with design constraints built in (spacing scale, color palette). Component-scoped CSS files with @apply keep JSX clean — no utility class soup in templates.
Trade-off: @apply-only usage loses some of Tailwind's composability benefits. But gains readability — styles live in CSS files, not inline.
Vite
Sub-second hot reload. Native ES modules in dev. Dev server proxies /api to Django on port 8710 — seamless full-stack development.
Trade-off: None meaningful vs. webpack for this use case. Vite is strictly better for a React SPA.
External Services
BRAPI
Only viable API for B3 (Brazilian stock exchange) data — quotes, financial statements, IPCA inflation. No Bloomberg/Refinitiv needed at this scale. Sponda uses their top-tier plan.
Trade-off: Single point of failure for all financial data. Mitigated by 24h caching — if BRAPI goes down, users see yesterday's data, not an error page.
Resend
Modern email API with good deliverability. SMTP integration means Django's built-in email backend works unchanged. Simple pricing, no vendor lock-in on the code side.
Trade-off: Another SaaS dependency. But email delivery is not a problem worth solving in-house.
Google OAuth (optional)
Reduces signup friction for users who don't want yet another password. Hidden by default — only appears when environment variables are configured.
Trade-off: Google dependency for auth. Mitigated by being optional — email/password always works. Users aren't locked in.
Infrastructure
systemd timers (not Celery/Redis)
Two daily jobs — refresh IPCA and refresh tickers. Celery + Redis + a worker process + a broker is massive overkill. systemd is already running. Zero additional dependencies.
Trade-off: No retry logic, no task queue, no distributed scheduling. Would need Celery if jobs become frequent or need to run on separate machines. Currently unnecessary complexity.
Single server (not Kubernetes)
One Django process serves the entire app. Financial data for ~800 stocks doesn't need horizontal scaling. Deployment is git pull && restart — simple, fast, debuggable.
Trade-off: Single point of failure. No auto-scaling. Acceptable for the current user base. Vertical scaling (bigger server) is the first move if needed.
Docker multi-stage build
Node builds frontend, Python runs backend — both in one image. No Node.js needed on the production server at runtime. Reproducible builds across environments.
Trade-off: Image size is larger than a slim Python image. Acceptable — build happens once, runs forever.
GitHub Actions
CI/CD lives next to the code. Free for public repos. Runs tests, deploys via SSH. No Jenkins server to maintain.
Trade-off: Vendor lock-in to GitHub. Workflows are YAML, not code. Acceptable — migration to another CI is straightforward if needed.
Testing
pytest + factory-boy
pytest's fixtures and parametrize are ideal for testing indicator calculations across many input scenarios. factory-boy generates realistic model instances — no fragile JSON fixtures to maintain.
Trade-off: factory-boy has a learning curve. Worth it — test data stays in sync with model changes automatically.
Playwright (not Selenium/Cypress)
Reliable auto-waiting, multi-browser support, and first-class Python integration via pytest-playwright. Tests auth flows, favorites, and page navigation end-to-end.
Trade-off: E2E tests are slow. Split from unit tests in CI to avoid session state leakage. Isolated test jobs run in parallel.
Vitest (not Jest)
Native Vite integration — shares the same config, same transforms, same module resolution. No separate build pipeline for tests. Fast startup, watch mode works instantly.
Trade-off: Smaller ecosystem than Jest. Compatible API means migration is trivial if ever needed.
responses (HTTP mocking)
Mocks the requests library at the transport level. BRAPI API tests run without network access — deterministic, fast, no API key needed in CI.
Trade-off: Mocks can drift from the real API. Mitigated by lazy refresh in production — if BRAPI changes its response shape, the real app surfaces the error quickly.