Back to work
Production2022–2026Founder & Full-Stack Engineer88% complete

DarkHorseStocks (Web Platform)

Subscription stock-research platform for Indian retail investors — REST + GraphQL backend, animated React 18 frontend, three payment gateways, FCM push, and cross-subdomain SSO.

Summary

DarkHorseStocks is a 4-year-old subscription stock research platform — a Node.js + TypeScript backend (Express, Apollo GraphQL, Prisma over SQL Server) paired with an animated React 18 frontend (MUI v5, raw SCSS, GSAP, MouseFollower) — shipping curated equity ideas, sector/tag screeners, and blog-style analysis to Indian retail investors. It runs three parallel payment gateways (Razorpay, Paytm, Stripe), four OAuth providers plus email/phone/WhatsApp auth, Firebase Cloud Messaging for push, cross-subdomain SSO between marketing and dashboard, a TwelveData → custom DHS API failover for real-time quotes, and a node-cron scheduled daily price + ranking job running in Asia/Kolkata trading hours. Deployed as Dockerized containers on AWS ECS via a GitHub Actions → SSM pipeline.

Target user

Indian retail investors looking for hand-curated long-term equity ideas (BSE/NSE), with a global tier (Stripe) for NRIs and a tiered subscription model that gates real-time prices, paid blog posts, sector deep-dives, and AI-assisted research behind Razorpay / Paytm / Stripe checkouts.

§ 01Stack
01Primary
Node.jsTypeScriptExpressApollo ServerGraphQLPrismaSQL ServerReactRedux Toolkit + RTK QueryMUISCSS
02Infrastructure
AWS ECSAWS SSM Session ManagerDockerHub registryDocker + docker-composeGitHub ActionsAWS S3Apache/Nginx frontingSelf-hosted SQL Server
03Integrations
TwelveData APIDHS custom price APIUpstox APIRazorpayPaytm Blink CheckoutStripe LiveFirebase Admin SDKGoogle OAuthFacebook LoginTwitter OAuthSign in with AppleZoho MailSocket.ioWhatsApp signup/login flow
04UI / Frontend
MUI v5 + EmotionRaw SCSSStyled ComponentsGSAP 3 + ScrollTriggerMouseFollowerFramer MotionRechartsChart.jsHighchartsreact-stockchartsSwiper 9 + React SlickReact Hook Form + YupSocket.io-client
§ 02Key features
  1. 01

    Shipped a tiered subscription engine spanning four product lines (DarkHorse Stocks, Fundalysis, DarkHorse USA, PMS) with three parallel payment gateways (Razorpay for INR, Paytm for India alternates, Stripe for international/NRI), HMAC signature verification on Razorpay webhooks, Stripe webhook secret validation, and a node-cron `syncSubscriptions` job that reconciles payment provider state with the local Subscription table.

  2. 02

    Designed a `BroadcastNotification` ↔ `UserNotificationRead` join-table system for in-app announcements with per-user read tracking, FCM push delivery, and `DeviceToken` lifecycle (iOS/Android platform tagging, app version capture, soft-deactivation on uninstall) — and exposed the whole thing through both REST routes and a new GraphQL resolver layer in the same release.

  3. 03

    Built a TwelveData → DHS custom API failover for real-time stock quotes: batched 97 symbols per upstream request to fit TwelveData's rate budget, then added a `DHS_API_BASE_URL` + `X-API-Key`-authenticated fallback so the dashboard keeps quoting when the upstream throttles — with the cron scheduled `0 8 * * 1-5` in `Asia/Kolkata` to match BSE/NSE trading days only.

  4. 04

    Stood up cross-subdomain SSO between the marketing site (`darkhorsestocks.in`) and the dashboard (`dashboard.darkhorsestocks.in`) with a 7-day JWT session, cookie-credentials CORS allowlist, and `/auth/verify-session` validation — so a single login carries across the consumer-facing brand register and the gated app.

  5. 05

    Migrated the production CI/CD from SSH-to-EC2 to GitHub Actions → AWS SSM Session Manager with a polling loop replacing a fixed 15s sleep, and dockerized the Prisma migration step into the image entrypoint (`prisma migrate deploy` runs before `node dist/src/index.js`) so schema changes ship atomically with the container.

  6. 06

    Built a brand-register frontend with GSAP + MouseFollower polish — magnetic buttons (parallax content following the cursor), a hero TextScramble animation that decodes character-by-character, ScrollTrigger-driven mountain parallax and sun/moon rotation on theme toggle, and a custom 4s ease-out dark/light transition with localStorage persistence and OS preference detection.

  7. 07

    Implemented Apollo GraphQL alongside the existing REST surface — Apollo Server 5 mounted on Express, an auth-aware GraphQL context, 8+ schema modules (User, Subscription, Plan, Company, BlogPost, Notification, Referral, BroadcastNotification, custom DateTime scalar), and an `/graphql-playground` admin route — without breaking the existing REST clients.

  8. 08

    Wired a referral + commission system: each user gets a unique referral code with configurable discount % and commission %, codes apply at checkout across all three payment providers, and a many-to-many back-relation tracks every referred subscription for payout reporting.

§ 03Hardest problems
  1. Razorpay, Paytm, and Stripe each have their own order/transaction/payment-intent shapes, their own verification crypto (Razorpay HMAC, Paytm checksum library, Stripe webhook secrets), and their own retry semantics. The naive design — one Subscription row written eagerly on payment intent creation — leaks orphaned subscriptions when verification fails downstream, and the inverse (write only after verification) leaks paid charges with no subscription. Solved with three separate provider-specific tables (`PaytmResponse`, `Razorpay`, `Stripe`), each foreign-keyed to a single `Subscription` that is only created post-verification, plus a daily `syncSubscriptions` cron that polls each provider's API for the prior day's settled charges and rebuilds any subscriptions lost to a verification crash. Still missing a Postgres-style transaction across the provider write + Subscription write — an honest gap, documented as next on the infra list.

  2. TwelveData's WebSocket tier was the obvious answer but the cost/reliability tradeoff didn't pencil out at our subscriber count, so the WebSocket integration sits commented out in `src/index.ts` (lines 9–10, 73–137) as a deliberate not-yet, with HTTP polling against a custom in-house DHS quote API as the current production path. The fallback design — TwelveData batched at 97 symbols/request as primary, DHS API with `X-API-Key` header as backup — was added in commit `33f5811` and gives the dashboard a second source if the upstream throttles. The honest read: it's polling, not streaming, and a real WebSocket server is in the planned infra.

  3. The marketing site has to do real work — convert visitors into subscribers — but Stripe-checkout-style aesthetics don't sell a finance brand to Indian retail investors who associate stock advisors with personality and presence. The solve was a layered animation system: TextScramble for the hero headline (character-by-character decode with `nbsp` space preservation, recently tuned), GSAP timelines with ScrollTrigger for word-stagger reveals on scroll, MouseFollower-driven magnetic buttons (cursor + content parallax via `gsap.to` on mousemove, scale reset on mouseleave), and a separate FooterAnimation component (running horse, mountains, rotating sun/moon) that ties the brand mark to the dark/light theme toggle. The animations had to feel intentional rather than decorative, and the gated content design means unauth visitors land on a hero + ticker tape only — no content leakage, no `?utm_demo` exfiltration.

§ 04What I learned
  • L01
    Dual ORM is a smell that's also a reasonable transition strategy.

    Sequelize sits alongside Prisma in this repo because the original 2022 codebase was Sequelize-first and Prisma was layered in for new modules without rewriting the old ones. The right answer would have been a hard cutover; the answer that actually shipped was 'new code goes through Prisma, old Sequelize code stays until it's the bottleneck.' Three years later it's still not the bottleneck — and writing two ORMs has been cheaper than the rewrite would have been.

  • L02
    Cross-subdomain SSO is mostly a CORS and cookie problem, not an auth problem.

    The hard part wasn't issuing a JWT — it was getting `credentials: 'include'` to actually send the cookie from `darkhorsestocks.in` to `dashboard.darkhorsestocks.in` across all the major browsers, building the allowlist origin check on the server, and convincing Safari that a sibling subdomain isn't a third-party cookie. Once that worked, the JWT verification was a one-liner.

  • L03
    Animation polish has a real conversion impact in finance UX.

    The TextScramble hero and magnetic buttons started as 'just make it feel premium' and ended up being the highest-leverage UX work on the marketing site — the brand impression carries the trust signal that the screener and blog content depend on. Cutting animations to ship faster would have been a false economy.

§ 05By the numbers
backend lines of code
~17759
frontend lines of code
~46173
backend commits
258
frontend commits
732
prisma models
~20
payment gateways
3
oauth providers
4
api routes
~28
graphql schema modules
~8
development span years
4