StockPulse is a personal stock portfolio tracker and showcase app. It connects directly to your Robinhood account to display live portfolio data, provides AI-powered stock research and analysis via Claude, and offers a public showcase page so friends can follow your investing progress.
This is a personal tool, not a multi-user SaaS. Keep things simple.
- Framework: Next.js 14+ with App Router. Server Components by default; Client Components only where interactivity is required (charts, forms, real-time updates).
- Styling: Tailwind CSS + shadcn/ui components. Dark/light mode via CSS variables and next-themes.
- Database: PostgreSQL via Supabase with Prisma ORM. All monetary values use Decimal type (never float).
- Authentication: Supabase Auth with @supabase/ssr. Simple login - primarily for you as the owner.
- Robinhood Data: robin-stocks (Python) called from Next.js API routes via subprocess. Primary source of portfolio data.
- Stock Data: Finnhub API for supplemental data (historical charts, ticker search, company profiles). 60 req/min free tier.
- AI: Anthropic Claude API via @anthropic-ai/sdk. All AI calls routed through Next.js API routes (never client-side).
- Data Fetching: TanStack Query v5 for client-side caching. Default staleTime: 60000ms.
- Charts: Recharts for portfolio visualizations.
- Animations: Framer Motion for page transitions and micro-interactions. Keep under 300ms.
- Deployment: Vercel (frontend + API) + Supabase (database + auth). Vercel Cron for scheduled syncs.
- Strict mode enabled (
strict: truein tsconfig.json) - No
anytypes. Useunknownand narrow with type guards. - Prefer interfaces for object shapes, types for unions/intersections.
- All API inputs validated with Zod schemas at route boundaries.
- Server Components by default. Add
'use client'only when needed. - Component names: PascalCase. File names: kebab-case. Example:
PortfolioSummaryinportfolio-summary.tsx. - One component per file. Max 200 lines; extract sub-components if longer.
- Use named exports (except
page.tsx,layout.tsxwhich need default exports). - Props interfaces named
{ComponentName}Props. - No
useEffectfor data fetching; use React Query or server-side fetching.
- All routes in
src/app/api/following RESTful conventions. - Use route handlers (
route.ts), not page-based API routes. - Validate all inputs with Zod.
- Return consistent JSON:
{ data, error, meta }. - HTTP status codes: 200 (ok), 201 (created), 400 (bad input), 401 (unauthorized), 404 (not found), 500 (server error).
- Variables/functions:
camelCase - Components:
PascalCase - Constants:
UPPER_SNAKE_CASE - Database columns:
snake_case(Prisma maps to camelCase) - API routes: kebab-case (
/api/portfolio-summary) - Test files:
{filename}.test.tsor{filename}.test.tsx - Hook files:
use-{name}.ts
- API routes: try/catch with consistent error response format.
- Client: React Error Boundaries at route group level.
- Never expose stack traces or internal details to the client.
- Use path aliases (
@/maps tosrc/). - Order: 1) React/Next.js 2) External packages 3) Internal modules 4) Types 5) Styles. Separated by blank lines.
src/
├── app/
│ ├── (auth)/ # Auth route group
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx # Minimal auth layout
│ ├── (dashboard)/ # Protected route group
│ │ ├── dashboard/page.tsx # Portfolio overview / home
│ │ ├── portfolio/
│ │ │ ├── page.tsx # All positions
│ │ │ └── [symbol]/page.tsx # Stock detail
│ │ ├── transactions/page.tsx # Trade history
│ │ ├── analytics/page.tsx # Charts and metrics
│ │ ├── ai/page.tsx # AI chat and insights
│ │ ├── watchlist/page.tsx # Watchlisted stocks
│ │ ├── alerts/page.tsx # Price alerts
│ │ ├── settings/page.tsx # User + Robinhood settings
│ │ └── layout.tsx # Dashboard layout (sidebar + header)
│ ├── portfolio/
│ │ └── [username]/page.tsx # Public showcase page
│ ├── api/
│ │ ├── auth/callback/route.ts
│ │ ├── portfolio/route.ts
│ │ ├── positions/route.ts
│ │ ├── transactions/route.ts
│ │ ├── stocks/[symbol]/route.ts
│ │ ├── stocks/search/route.ts
│ │ ├── robinhood/
│ │ │ ├── sync/route.ts # Trigger Robinhood sync
│ │ │ └── status/route.ts # Connection status
│ │ ├── ai/
│ │ │ ├── analyze/route.ts # Portfolio analysis
│ │ │ ├── chat/route.ts # Stock Q&A
│ │ │ └── research/route.ts # Stock research
│ │ ├── alerts/route.ts
│ │ └── cron/
│ │ ├── sync-prices/route.ts # Robinhood + Finnhub refresh
│ │ └── daily-snapshot/route.ts # Portfolio snapshot
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Landing / redirect
│ └── globals.css
├── components/
│ ├── ui/ # shadcn/ui (auto-generated)
│ ├── layout/ # Sidebar, header, mobile nav
│ ├── portfolio/ # Position cards, tables, summary
│ ├── charts/ # Line chart, pie chart, sparklines
│ ├── ai/ # Chat panel, analysis cards
│ └── shared/ # Ticker badge, price indicator, skeletons
├── lib/
│ ├── supabase/
│ │ ├── server.ts # Server-side client
│ │ └── client.ts # Browser-side client
│ ├── prisma/
│ │ └── client.ts # Prisma singleton
│ ├── api/
│ │ ├── finnhub.ts # Finnhub typed client
│ │ └── anthropic.ts # Claude API wrapper
│ ├── robinhood/
│ │ ├── sync.ts # Orchestrates sync logic
│ │ └── parser.ts # Parses robin-stocks JSON output
│ ├── calculations/
│ │ └── portfolio.ts # P&L, cost basis, returns
│ ├── validators/ # Zod schemas
│ └── utils/
│ ├── format.ts # Currency, percentage, date formatting
│ └── cn.ts # Tailwind classname merge
├── hooks/ # Custom React hooks
├── providers/ # Query, theme, auth providers
├── types/ # TypeScript type definitions
├── config/ # Site metadata, nav config
└── middleware.ts # Auth middleware
scripts/
└── robinhood/
├── fetch_portfolio.py # robin-stocks: get positions + account
├── fetch_orders.py # robin-stocks: get order history
└── requirements.txt # Python dependencies (robin-stocks)
prisma/
├── schema.prisma
├── migrations/
└── seed.ts
tests/
├── unit/ # Unit tests (mirrors src/)
├── integration/ # API route tests
├── e2e/ # Playwright E2E tests
└── fixtures/ # Test data
- Python scripts in
scripts/robinhood/userobin-stocksto authenticate and fetch data. - Next.js API routes call these scripts via
child_process.execFile(). - Scripts output JSON to stdout; API routes parse and store in database.
- Frontend reads from database (fast) and can trigger a fresh sync on demand.
- Vercel Cron auto-syncs every 15 minutes during market hours (9:30 AM - 4:00 PM ET).
- Robinhood credentials in environment variables:
ROBINHOOD_USERNAME,ROBINHOOD_PASSWORD. - MFA via TOTP. Store TOTP secret in
ROBINHOOD_TOTP_SECRETenv var for auto-generation. - Session tokens cached in
/tmp/robinhood_session.pickle(server-side only). - Never expose Robinhood credentials to the frontend.
- Current positions (symbol, quantity, average buy price, equity, percent change)
- Account summary (total equity, cash, buying power)
- Order history (buy/sell with prices, dates, quantities)
- Dividends (amount, date, symbol)
- On demand: User clicks "Sync" button → API route → Python script → database update
- Automatic: Vercel Cron every 15 min during market hours
- Deduplication: Transactions matched by
robinhood_order_idto prevent duplicates - Fallback: If Robinhood sync fails, display last known data with "Last synced: X minutes ago"
src/lib/calculations/: 90% minimum (financial math must be precise)src/lib/validators/: 100% (all Zod schemas tested)src/components/: 70% minimum- Overall: 70% minimum
- Unit Tests (Jest + React Testing Library): calculation functions, validators, hooks, components
- API Route Tests (Jest): success cases, validation errors, auth failures. Mock external APIs.
- E2E Tests (Playwright): auth flow, add transaction, view portfolio, AI chat, public showcase page
npm run test— all unit + integration testsnpm run test:watch— watch modenpm run test:coverage— coverage reportnpm run test:e2e— Playwright E2Enpm run test:ci— full suite (unit + integration + e2e)
- Unit:
{source-file}.test.ts(x) - E2E:
{feature}.spec.tsintests/e2e/ - Fixtures:
{domain}.tsintests/fixtures/
main: production-ready, protecteddevelop: integration branchfeature/{description}: new featuresfix/{description}: bug fixes
Format: <type>(<scope>): <description>
Types: feat, fix, docs, style, refactor, perf, test, chore
Scopes: auth, portfolio, transactions, analytics, ai, robinhood, ui, db, api
Examples:
feat(robinhood): add automatic portfolio syncfix(calculations): handle zero-quantity positions in P&Ltest(portfolio): add unit tests for cost basis calculation
Rules:
- Max 72 characters in subject line
- Imperative mood ("add" not "added")
- One logical change per commit
// Success
{ "data": { ... }, "meta": { "synced_at": "..." } }
// Error
{ "error": { "code": "VALIDATION_ERROR", "message": "..." } }- Stock quotes: 15 seconds
- Portfolio data: 60 seconds (invalidate on sync/mutation)
- Historical data: 5 minutes
- AI responses: 5 minutes
- Cursor-based for transaction lists:
?cursor={id}&limit={number} - Default limit: 20, max: 100
All monetary values use Decimal(12,4). All tables have id (UUID), created_at, updated_at.
model User {
id String @id @default(uuid())
email String @unique
display_name String?
username String? @unique // for public URL: /portfolio/[username]
avatar_url String?
bio String?
is_portfolio_public Boolean @default(false)
robinhood_connected Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
portfolios Portfolio[]
watchlist_items Watchlist[]
alerts Alert[]
ai_conversations AiConversation[]
}model Portfolio {
id String @id @default(uuid())
user_id String
name String
description String?
is_public Boolean @default(false)
visibility_settings Json @default("{\"show_positions\":true,\"show_pnl\":true,\"show_trades\":false,\"show_value\":false}")
source PortfolioSource @default(MANUAL)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
positions Position[]
transactions Transaction[]
snapshots PortfolioSnapshot[]
@@unique([user_id, name])
@@index([user_id])
}
enum PortfolioSource {
ROBINHOOD
MANUAL
}model Position {
id String @id @default(uuid())
portfolio_id String
symbol String
quantity Decimal @db.Decimal(16, 8) // fractional shares
average_cost_basis Decimal @db.Decimal(12, 4)
total_cost_basis Decimal @db.Decimal(14, 4)
current_price Decimal @db.Decimal(12, 4)
current_value Decimal @db.Decimal(14, 4)
unrealized_pnl Decimal @db.Decimal(14, 4)
unrealized_pnl_percent Decimal @db.Decimal(8, 4)
sector String?
last_synced_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
portfolio Portfolio @relation(fields: [portfolio_id], references: [id], onDelete: Cascade)
@@unique([portfolio_id, symbol])
@@index([portfolio_id])
}model Transaction {
id String @id @default(uuid())
portfolio_id String
symbol String
type TransactionType
quantity Decimal @db.Decimal(16, 8)
price_per_unit Decimal @db.Decimal(12, 4)
total_amount Decimal @db.Decimal(14, 4)
fees Decimal @db.Decimal(10, 4) @default(0)
executed_at DateTime
notes String? // your trade rationale
source TransactionSource @default(MANUAL)
robinhood_order_id String? @unique // for deduplication
created_at DateTime @default(now())
updated_at DateTime @updatedAt
portfolio Portfolio @relation(fields: [portfolio_id], references: [id], onDelete: Cascade)
@@index([portfolio_id, executed_at])
@@index([portfolio_id, symbol])
}
enum TransactionType {
BUY
SELL
DIVIDEND
}
enum TransactionSource {
ROBINHOOD_SYNC
MANUAL
CSV_IMPORT
}model PortfolioSnapshot {
id String @id @default(uuid())
portfolio_id String
date DateTime @db.Date
total_value Decimal @db.Decimal(14, 4)
total_cost_basis Decimal @db.Decimal(14, 4)
total_pnl Decimal @db.Decimal(14, 4)
total_pnl_percent Decimal @db.Decimal(8, 4)
num_positions Int
created_at DateTime @default(now())
portfolio Portfolio @relation(fields: [portfolio_id], references: [id], onDelete: Cascade)
@@unique([portfolio_id, date])
@@index([portfolio_id])
}model Watchlist {
id String @id @default(uuid())
user_id String
symbol String
target_buy_price Decimal? @db.Decimal(12, 4)
notes String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@unique([user_id, symbol])
@@index([user_id])
}model Alert {
id String @id @default(uuid())
user_id String
symbol String
type AlertType
threshold_value Decimal @db.Decimal(14, 4)
is_active Boolean @default(true)
is_triggered Boolean @default(false)
triggered_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([user_id, is_active])
}
enum AlertType {
PRICE_ABOVE
PRICE_BELOW
}model AiConversation {
id String @id @default(uuid())
user_id String
title String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
messages AiMessage[]
@@index([user_id])
}
model AiMessage {
id String @id @default(uuid())
conversation_id String
role MessageRole
content String
created_at DateTime @default(now())
conversation AiConversation @relation(fields: [conversation_id], references: [id], onDelete: Cascade)
@@index([conversation_id])
}
enum MessageRole {
USER
ASSISTANT
}- Supabase RLS on all tables. Users can only access their own data.
- Robinhood credentials in env vars only, never in database or logs.
- Zod validation on every API route input.
- Parameterized queries via Prisma (SQL injection prevention).
- CORS configured for app domain only.
- Cron job endpoints protected by
CRON_SECRETheader. - All secrets in
.env.local(in.gitignore). Template in.env.example. - AI responses always include "not financial advice" disclaimer.
- LCP: < 2.5 seconds
- FID: < 100 milliseconds
- CLS: < 0.1
- Portfolio summary API: < 200ms
- Stock quote API: < 500ms
- AI analysis: < 10 seconds (streaming)
- Bundle size: < 200KB initial JS (gzipped)
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Database
DATABASE_URL=
# Robinhood (robin-stocks)
ROBINHOOD_USERNAME=
ROBINHOOD_PASSWORD=
ROBINHOOD_TOTP_SECRET=
# Stock Data
FINNHUB_API_KEY=
# AI
ANTHROPIC_API_KEY=
# Cron
CRON_SECRET=
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
- Next.js 14 project setup (TypeScript, Tailwind, shadcn/ui, Prisma, Supabase)
- Supabase Auth (login/register)
- Database schema: all models above (8 models, Prisma 7 with pg adapter)
- Dashboard layout (sidebar, header, dark/light mode)
- [] Robinhood integration: connect, pull positions + account value (robin-stocks Python scripts)
- Auto-sync transaction history from Robinhood (dedup by robinhood_order_id)
- [] Portfolio view: positions with live prices, total value, daily change
- [] P&L calculations: cost basis, unrealized gains/losses (25 unit tests, 100% coverage)
- Manual trade entry (fallback for non-Robinhood trades)
- Vercel deployment + Supabase production database
- User provisioning (bridge Supabase Auth → Prisma User table)
- Quality gate: ✅ 46 unit tests passing (25 calculations, 13 transaction validators, 7 portfolio validators + 1 edge case). P&L math 100% tested. Build succeeds. App runs locally. Deployed to Vercel with live Supabase DB. Auth end-to-end working.
- Portfolio performance chart (value over time)
- Position detail page with stock price chart
- Sector allocation pie chart
- Weekly/monthly gains and losses
- Top gainers/losers
- Transaction history (filterable, sortable, paginated)
- Watchlist
- Vercel Cron: daily snapshot + periodic re-sync
- Quality gate: Charts render at all breakpoints, historical data accurate
- Claude API with streaming responses
- Portfolio analysis (AI reviews your full portfolio)
- Stock research (ask about any stock)
- Position explainer (AI explains why you hold a stock)
- Trade ideas (suggestions based on your strategy)
- Earnings & news summaries for holdings
- Chat interface for free-form Q&A
- Quality gate: Streaming works, "not financial advice" disclaimers present
- Public portfolio page (
/portfolio/[username]) - Privacy controls (choose what visitors see)
- Trade notes (explain your reasoning on each trade)
- Price alerts
- Mobile responsiveness polish
- Loading/error/empty states everywhere
- Dark/light mode polish
- Quality gate: Public page works, privacy enforced, mobile looks great