LiveFlow
Brazilian equity analytics — PE10, PFCF10, PEG, leverage ratios for ~800 B3-listed stocks
sponda.capital
System Architecture · March 2026

Browser


Single-page application served from /staticfiles/frontend

Session cookie for auth state

Authorization Tiers


  • Anonymous — unlimited lookups, no favorites or saved lists
  • Unverified — 14 ops/day, 5-day grace period
  • Verified — unlimited
  • Admin — dashboard access

Session Auth


CSRF-exempt for same-origin API

CORS middleware for cross-origin

Secure cookies in production (SSL-only)

renders SPA

React 19 · TypeScript 5.7 · Vite 6

Routing

TanStack Router 1.92

  • / — home, favorites, lists
  • /:ticker — metrics card
  • /:ticker/graficos — charts
  • /:ticker/comparar — compare
  • /account — profile
  • /shared/:token — public list

Server State

TanStack Query 5.62

  • usePE10 · useMultiplesHistory
  • useFavorites · useSavedLists
  • useCompareData · useTickers
  • useQuota · usePageTracking
  • useAuth (context + session)
  • deriveForYears (utility)

Search

Fuse.js 7.1

Client-side fuzzy matching

~800 stocks indexed in memory

Autocomplete with sector grouping

Charts

Recharts 3.8

Historical price line chart

Year-end multiples (P/L, P/FCL)

Dual-panel layout

Styling

Tailwind CSS v4

Component-scoped CSS files

Responsive, mobile-first

@apply only — no utility in JSX

JSON / REST

Django 5.1 · DRF 3.15 · Gunicorn 23 · Python 3.12

quotes

Core financial data & indicator engines

Indicators
  • PE10 — Shiller P/E, 10yr inflation-adjusted
  • PFCF10 — 10yr avg free cash flow multiple
  • PEG — earnings CAGR-adjusted PE
  • PFCF PEG — FCF CAGR-adjusted PFCF
  • Leverage — debt/PL, passivo/PL, debt/earnings
Modules
  • multiples_history — year-end price/multiple series
  • og_image — OG images via Pillow 10.3
  • brapi.py — BRAPI API integration
  • refresh_ipca — mgmt command (daily)
  • refresh_tickers — mgmt command (daily)

accounts

User lifecycle, rate limiting, analytics

Models
  • User — custom AbstractUser, email verification
  • FavoriteCompany — starred tickers per user
  • SavedList — named comparison lists, shareable
  • PageView — SHA-256 hashed IPs, privacy-first
Operations
  • LookupLog — audit trail for rate limiting
  • UserOperation — unverified user ops tracker
  • PasswordResetToken — 24hr expiry
  • EmailVerificationToken — 72hr expiry

Rate limit: 200 distinct tickers/day

config

Settings & URL routing

  • Settings — base / dev / prod
  • Middleware — CORS · Session · CSRF

API endpoints
  • /api/quote/<ticker>/
  • /api/auth/*
  • /api/og/*.png
  • /api/tickers/
  • /api/health/
ORM · HTTPS

PostgreSQL 15

psycopg2 2.9 · SQLite in dev

Financial data
  • Ticker (~800 B3 stocks served, ~2,300 total stored)
  • QuarterlyEarnings
  • QuarterlyCashFlow
  • BalanceSheet
  • IPCAIndex (IPCA = Brazil's official inflation index)
User & operations
  • User · EmailVerificationToken
  • PasswordResetToken
  • FavoriteCompany · SavedList
  • LookupLog · UserOperation
  • PageView

BRAPI

brapi.dev · requests 2.32

  • Real-time stock quotes
  • Financial statements (quarterly)
  • IPCA inflation index
  • B3 ticker catalog

Lazy refresh: stale after 24h

Resend

SMTP · smtp.resend.com

  • Welcome email
  • Email verification
  • Password reset
  • Feedback forwarding

Google OAuth 2.0

Optional provider

ID token validation

Create or login user

Hidden until configured


Production Server

159.203.108.19 · /opt/sponda

  • Nginx → reverse proxy
  • Gunicorn → WSGI server
  • systemd → sponda.service
  • Timer → daily 06:00 UTC refresh

refresh_ipca + refresh_tickers

Docker

Multi-stage build

  • Stage 1 — Node 20 → frontend build
  • Stage 2 — Python 3.12 → Django app

  • docker-compose — web + db
  • Port 8710
  • PostgreSQL 15-alpine volume

CI / CD

GitHub Actions

  • Trigger — push to main
  • Test — pytest (unit, split from E2E)
  • Test — vitest (frontend)
  • Test — Playwright (E2E)
  • Deploy — SSH → pull, build, migrate, restart

Domains


  • sponda.capital
  • sponda.poe.ma

Env vars
  • BRAPI_API_KEY
  • DATABASE_URL
  • RESEND_API_KEY
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • DJANGO_SECRET_KEY

TDD — tests first, always

Backend Unit / Integration

pytest 8.3 · pytest-django 4.9 · factory-boy 3.3

Models, indicator calculations, API views, auth flows

E2E

Playwright 1.49 · pytest-playwright 0.6

Full browser flows — auth, favorites, page navigation

Split from unit tests in CI to avoid session leakage

Frontend Unit

Vitest 4.1

Hooks, utilities, data derivations

HTTP Mocks

responses 0.25

BRAPI API stubs for deterministic tests

Ticker Lookup
Browser
User searches ticker
TanStack Query
Cache check (usePE10)
PE10View
GET /api/quote/<ticker>/
Freshness check
Stale after 24h?
BRAPI
Fetch earnings, CF, balance sheet
PostgreSQL
Read cached data
Indicator engines
PE10 · PFCF10 · PEG · Leverage
Recharts
Render metrics & charts
Daily Refresh (06:00 UTC)
systemd timer
sponda-refresh.timer
refresh_ipca
Management command
BRAPI
Fetch IPCA index
PostgreSQL
Update IPCAIndex table
systemd timer
sponda-refresh.timer
refresh_tickers
Management command
BRAPI
Fetch B3 ticker list
PostgreSQL
Sync all tickers from B3
User Signup
Signup form
Email + password
POST /api/auth/signup/
Create User (unverified)
Resend SMTP
Welcome + verification email
User clicks link
/verify-email?token=…
POST /api/auth/verify-email/
Mark email_verified = true
Full access
Unlimited operations

Technology choices and trade-offs
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.
DRF (Django REST Framework) — the standard library for building REST APIs in Django. Provides serializers, viewsets, authentication, and content negotiation.
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.
WSGI (Web Server Gateway Interface) — the standard protocol between Python web apps and web servers. Gunicorn spawns worker processes that each run a copy of the Django app, handling concurrent requests.
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.
Pillow — Python's image processing library (fork of PIL). Used here to draw text and shapes onto a blank canvas, producing PNG images server-side without a browser.
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.