Live P&L streamed tick by tick, FIFO cost basis, holdings heatmap, P&L attribution, risk metrics, price alerts and market news, in one terminal.
Live demo → · one click, no sign-up
- Real-time by default. Trades stream from Finnhub's WebSocket through a server-side hub to the browser over Server-Sent Events. Prices tick as they print, with flash-on-change, live sparklines and a "today's value" chart that draws itself.
- Real portfolio accounting. FIFO lot matching, fees capitalised into cost basis, realized vs. unrealized P&L, dividends, and a ledger that refuses inconsistent history (you can't sell shares you never bought).
- Risk at a glance. Value-weighted beta, Herfindahl concentration (HHI → "effective positions"), largest position and sector exposure.
- Alerts that actually fire. Price and daily-move alerts are evaluated every 5 minutes during market hours by a background job, de-duplicated per trading day, and emailed.
- Built like a trading terminal. A command bar that takes tickers and mnemonics (
AAPL⏎,PORT,WL,ALRT), ticker tape, world clocks, market-session countdown, keyboard navigation and dense tabular numerics. - Analytics, not just numbers. Holdings heatmap (size = weight, colour = today's move), day-P&L attribution by position, and day performance against the S&P 500.
- Instant demo. Every visitor gets a private sandbox account with a seeded portfolio. It's purged after 24 hours.
| What | Result |
|---|---|
| Stream latency: trade reaches the server → browser receives it | p50 25 ms · p95 48 ms · p99 50 ms at 1,000 concurrent streams (load test) |
| Streams sharing one upstream connection | 8,000 held with 0 dropped; first strain at ~8,000 per process (99.2 % delivered, p99 63 ms) |
| Memory at 1,000 streams | Next.js app 335 MB · standalone relay 85 MB |
| Serverless time limit | Streams rotate make-before-break 5 s before the cut-off, so the limit causes no price gap (by design; details) |
| Tests | 65 unit tests; CI enforces coverage ≥ 85 % lines (93 % statements) |
| Availability target | 99.5 % of external health checks pass (SLO and runbook) |
Measured with k6 + xk6-sse on one machine against a single Node process and a synthetic 20 trades/s feed. The method, raw results and how to reproduce them are in loadtest/.
| Portfolio | Security page |
|---|---|
![]() |
![]() |
| Watchlist | Alerts |
![]() |
![]() |
| Area | What you get |
|---|---|
| Dashboard | Net liquidation value, day P&L vs S&P 500, unrealized and total return · live intraday value chart · holdings heatmap · positions with sparklines · day-P&L attribution · sector exposure · watchlist movers · alerts near trigger · personalized news |
| Portfolio | Sortable live holdings with weights and sparklines · buy / sell / dividend entry · CSV import (validated atomically) and export · filterable transaction history · monthly realized P&L chart · risk panel |
| Watchlist | Live price, change, market cap, P/E, 52-week range position, inline alert creation, news for your symbols |
| Alerts | Price above/below and day-move up/down conditions, once or daily, pause/resume, edit, trigger history, "check now" |
| Stock page | Live quote header (O/H/L/prev close), your position, key statistics (beta, EPS, yield, margins, ROE), candlestick chart, technicals, financials |
| Markets | Index overview, sector heatmap, quotes and top stories |
| Accounts | Email/password or Continue with Google / Apple / Microsoft (each enabled when configured), one-click demo, export my data (JSON), delete my account |
| Legal | Terms of Service, Privacy Policy and Risk & Data Disclaimer pages; strictly necessary cookies only |
| Platform | Market-hours engine (NYSE holidays and early closes), keyboard shortcuts, email notifications, health endpoint, robots.txt and sitemap |
| Keys | Action |
|---|---|
| ⌘ K or / | Command bar: type a ticker (AAPL ⏎), a mnemonic (PORT, WL, ALRT, MKT, DASH) or TRADE / HELP |
| T | New trade |
| G then D / P / W / A / M | Go to Dashboard / Portfolio / Watchlist / Alerts / Markets |
| ? | Show all shortcuts |
flowchart LR
subgraph Browser
UI[React Server + Client Components]
Store[Market store<br/>one SSE + REST baseline]
end
subgraph Next.js on Vercel
Actions[Server Actions<br/>trades · watchlist · alerts]
Quotes[/api/quotes/]
Stream[/api/stream · SSE/]
Hub[Stream hub<br/>ref-counted subscriptions]
Jobs[/api/inngest/]
end
Finnhub[(Finnhub<br/>REST + WebSocket)]
Mongo[(MongoDB)]
Inngest[[Inngest<br/>cron + events]]
Mail[[SMTP]]
UI --> Actions --> Mongo
Store --> Quotes --> Finnhub
Store <-- ticks --> Stream --> Hub <-- trades --> Finnhub
Inngest --> Jobs --> Mongo
Jobs --> Finnhub
Jobs --> Mail
- Components call
useLiveQuotes(symbols). A page-wide store merges every requested symbol into oneEventSourceand one REST poller. /api/streamsubscribes those symbols on a single shared upstream WebSocket per server instance. Subscriptions are reference counted, trade bursts are coalesced per symbol and flushed every 250 ms, and the upstream reconnects with exponential backoff.- Streamed prices are re-based on the previous close from the REST quote, so day change and P&L stay correct. If streaming is unavailable, the store falls back to polling (15 s when open, 2 min when closed) and the indicator switches from Live to Delayed.
- Client-side re-marking (
lib/finance/live.ts) is tested for parity with the server engine, so live numbers always match a page refresh. - Serverless limits. On Vercel a stream can't outlive the function, and the shared hub is per instance. Streams announce their cut-off and the browser switches to a replacement before the old one closes, so there's no gap. For scale, a standalone relay (
relay/, Docker + Fly.io) holds one upstream socket for every user, authenticated with short-lived signed tokens. See docs/streaming.md.
lib/finance/portfolio.ts is pure and I/O-free. It replays transactions chronologically into FIFO lots (buys before sells on the same day), produces realized events, marks open positions to market, and derives allocation and risk. Every number on screen is reproducible and covered by unit tests.
| Function | Trigger | What it does |
|---|---|---|
check-price-alerts |
every 5 min, weekdays 9–16 ET | Evaluates active alerts only while the market is open; each trigger is a conditional update, so overlapping runs never double-fire |
daily-news-summary |
daily 12:00 UTC | Per-user digest from watchlist news, summarized and emailed |
sign-up-email |
app/user.created |
Personalized welcome email |
purge-demo-accounts |
hourly | Deletes demo users and their data after 24 h |
Frontend: Next.js 15 (App Router, RSC, Server Actions) · React 19 · TypeScript (strict) · Tailwind CSS 4 · Radix UI / shadcn · IBM Plex · lucide icons · TradingView widgets
Backend: Next.js route handlers and server actions · MongoDB + Mongoose · Better Auth (email/password, sessions) · Zod validation · Inngest · Nodemailer · Gemini
Market data: Finnhub REST (quotes, profiles, fundamentals, news) and WebSocket (trades)
Observability: OpenTelemetry (@vercel/otel, OTLP → Grafana) · token-gated metrics endpoint · external uptime probe and SLO
Quality and delivery: Vitest with coverage gates · k6 load tests · ESLint (zero warnings) · GitHub Actions (lint, typecheck, test, build, Docker) · Dependabot · Docker multi-stage image · Vercel
git clone https://github.com/sAchin-680/Real-Time-Stock-Market.git
cd Real-Time-Stock-Market
npm install
cp .env.example .env.local # then fill in the values below
npm run dev # http://localhost:3000Without MONGODB_URI, development falls back to mongodb://127.0.0.1:27017/tickline. Without a Finnhub key, the app still runs: positions are valued at cost and a notice explains how to enable live data.
To run the background jobs locally, start the Inngest dev server in a second terminal:
npx inngest-cli@latest dev -u http://localhost:3000/api/inngestcp .env.example .env # set FINNHUB_API_KEY and BETTER_AUTH_SECRET
docker compose up --build # app :3000 · MongoDB · Inngest dev server :8288| Variable | Required | Description |
|---|---|---|
MONGODB_URI |
✅ | MongoDB connection string |
BETTER_AUTH_SECRET |
✅ | 32+ random characters (openssl rand -base64 32) |
BETTER_AUTH_URL |
✅ | Public URL of the app, e.g. https://your-app.vercel.app |
FINNHUB_API_KEY |
✅ for live data | Quotes, fundamentals, news and the trade stream (server-side only) |
INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY |
production jobs | Added automatically by the Inngest Vercel integration |
NODEMAILER_EMAIL / NODEMAILER_PASSWORD |
for emails | Gmail address and app password |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
optional | "Continue with Google" — redirect URI <BETTER_AUTH_URL>/api/auth/callback/google |
APPLE_CLIENT_ID / APPLE_CLIENT_SECRET |
optional | "Continue with Apple" — Services ID + signed client-secret JWT; return URL /api/auth/callback/apple |
MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET |
optional | "Continue with Microsoft" — Entra app; redirect URI /api/auth/callback/microsoft (MICROSOFT_TENANT_ID defaults to common) |
NEXT_PUBLIC_CONTACT_EMAIL |
optional | Contact shown in the Privacy Policy and Terms |
GEMINI_API_KEY |
optional | Personalized welcome emails and daily digests |
EMAIL_FROM_NAME |
optional | Sender name (default Tickline) |
LOG_LEVEL |
optional | debug · info · warn · error |
| Command | Description |
|---|---|
npm run dev |
Dev server (Turbopack) |
npm run build / npm start |
Production build / server |
npm run lint |
ESLint with zero warnings allowed |
npm run typecheck |
tsc --noEmit |
npm test / npm run test:coverage |
Unit tests / with coverage thresholds |
npm run check |
Lint + typecheck + tests |
| Endpoint | Auth | Description |
|---|---|---|
GET /api/health |
public | DB ping, market-data config, market session, version; 503 when degraded |
GET /api/quotes?symbols=AAPL,MSFT |
session | Batched quotes (max 50), rate limited, plus market status |
GET /api/stream?symbols=AAPL,BINANCE:BTCUSDT |
session | Server-Sent Events: ticks events with {s, p, t, v} |
GET /api/portfolio/export |
session | Transactions as CSV |
GET /api/account/export |
session | Everything stored about the user, as JSON |
/api/auth/* |
public | Better Auth endpoints (OAuth redirects and callbacks) |
/api/inngest |
signed | Inngest function endpoint |
CSV import format (header row required, any column order):
date,symbol,side,quantity,price,fees,notes
2026-01-15,AAPL,BUY,10,185.50,1,Core position
2026-06-20,AAPL,SELL,4,214.30,1,Trim
2026-08-14,AAPL,DIVIDEND,6,0.26,0,Q3 dividend- The market-data key is server-only. Browsers receive quotes and ticks through authenticated endpoints, never the key.
- Every server action re-validates input with Zod and scopes queries to the session user. Helpers that must not be callable from the browser live in
server-onlymodules. - Rate limiting covers auth, search, quotes, the stream and on-demand alert checks.
- CSP (scoped to TradingView), HSTS,
X-Frame-Options: DENY, strict referrer and permissions policies; noX-Powered-By. - Email templates escape user-controlled values; CSV export neutralizes spreadsheet formula injection.
- The Docker image runs as a non-root user with a healthcheck.
app/
(auth)/ sign-in, sign-up (with one-click demo)
(root)/ dashboard, portfolio, watchlist, alerts, markets, stocks/[symbol]
api/ health, quotes, stream (SSE), portfolio/export, inngest
components/
brand/ Tickline logo and wordmark
finance/ workspace, heatmap, attribution, holdings, charts, dialogs, alerts
layout/ command bar, icon rail, ticker tape, clocks, status bar, shortcuts
ui/ Radix/shadcn primitives
lib/
finance/ pure engines: portfolio (FIFO, P&L, risk), alerts, live re-marking, treemap
server/ Finnhub client, stream hub, session helpers (server-only)
client/ shared real-time market store
services/ portfolio, alerts, news, users, demo (server-only)
actions/ server actions
inngest/ background functions and prompts
database/models/ transactions, alerts, watchlist
tests/ unit tests
The project deploys to Vercel through its GitHub integration: pull requests get preview deployments and merges to main go to production. Set the environment variables above in Project → Settings → Environment Variables (mark secrets as Sensitive), install the Inngest integration for scheduled jobs, and allow Vercel to reach MongoDB (Atlas network access 0.0.0.0/0).
Any container platform works too: docker build -t tickline . produces a standalone, non-root image that serves on port 3000.
Market data © Finnhub and TradingView. For informational purposes only; not investment advice.




