My Financial Trading
Financial education platform with video streaming and multi-gateway subscriptions
A financial education platform with structured trading courses, subscription billing across four payment gateways, and a community for learners.
About the Project
My Financial Trading is a subscription-based platform that delivers structured trading courses, live-streamed sessions, and a gated community — all from a single backend API serving a Next.js frontend, an admin dashboard, and a separate affiliate partner portal.
The platform serves three distinct user types defined in the codebase: students who subscribe to consume course content, admins and super-admins who manage content and users, and partner affiliates who earn commission by referring new students.
The platform was built to centralise trading education content under a subscription model, replacing reliance on third-party platforms with owned infrastructure for payments, video delivery, and community interaction.
What I Built
Designed and implemented the full Express/TypeScript API across 23 feature modules: auth, courses, payments, streaming, community, notifications, newsletter, and the partner affiliate system.
Modelled the MongoDB schema in Prisma across 10 schema files covering users, students, payments, courses, community, partner tiers, audit logs, email logs, and webhook logs.
Built the Next.js 15 student-facing application and admin dashboard, covering course browsing, subscription checkout with Stripe and PayPal React SDKs, HLS video playback via hls.js, and community feeds.
Developed the Vite + React affiliate portal with referral dashboards, tier progress tracking, payout request flows, and QR-code-based referral link sharing.
What It Can Do
Token-protected video streaming with byte-range requests served directly from DigitalOcean Spaces (S3-compatible)
Subscription lifecycle management across Stripe (webhooks), Apple StoreKit (server-side receipt verification), and CoinPayments (crypto invoices)
BullMQ-backed background workers for video compression via FFmpeg, thumbnail generation via Sharp, and batched newsletter delivery
Redis cache-aside pattern in the auth middleware to avoid per-request database token lookups
Engineering Deep Dive
Click any card to read the full technical detail.
Challenges & Trade-offs
The real decisions behind the architecture.
Token-based auth added a MongoDB lookup on every request, which became a bottleneck under concurrent load.
Cache token validity in Redis with a 3-hour TTL keyed by a hash of the raw token. Invalidation happens implicitly when the token changes on logout.
A revoked token remains valid in cache for up to 3 hours unless the key expires or the Redis entry is explicitly deleted. Accepted because refresh-token rotation is the primary logout mechanism.
Video files needed to be access-controlled without exposing the storage URL, while still supporting HTTP range requests for seeking.
Generate a short-lived JWT per video access. The streaming endpoint verifies the token server-side, performs the range request against S3, and proxies the binary stream back to the client.
All video traffic passes through the Node.js process, increasing memory and CPU load. Chosen over signed S3 URLs because it allows per-browser response header control and centralised access revocation.
Video compression and thumbnail generation are CPU-heavy and would block the event loop if run inline in a request handler.
Upload triggers a BullMQ job. The worker calls FFmpeg via fluent-ffmpeg for compression and Sharp for thumbnail generation, uploads the result to S3, then saves the URL to MongoDB.
Adds latency between file upload and content availability. Stream-video-processing concurrency is set to 1 to avoid exhausting CPU on a constrained deployment.
Safari and iOS clients require different Cache-Control and Connection header semantics for HTTP range requests to function correctly during video playback.
The streaming service detects the browser from the User-Agent header and applies browser-specific response headers: no-cache + keep-alive for Chrome, public max-age=3600 for Safari/iOS, broader stale-while-revalidate for others.
A single streaming endpoint handles all browser targets without client-side changes.
Scheduled newsletters could be silently missed if the server restarted between the scheduling cron tick and the delivery time.
A recovery loop inside the 5-minute cron looks back 24 hours for newsletters that are unsent and not in the queue. It re-enqueues them with a 5-second delay and marks them as scheduled in the DB before adding to the queue to prevent duplicates.
Missed newsletters are detected and re-queued on the next cron tick with no manual intervention required.
Three payment gateways (Stripe, Apple StoreKit, CoinPayments) each have different webhook formats, verification requirements, and subscription event lifecycles that needed to converge on the same data model.
Each gateway has its own verification helper (Stripe HMAC on raw body, Apple receipt verification via Apple's endpoint, CoinPayments HMAC signature). All three update the same Payment and Student Prisma models and normalise to a shared PaymentStatus enum.
Subscription state is consistent across gateways; the rest of the application only reads PaymentStatus without knowing which gateway was used.
System Design
What Makes It Fast
Redis cache-aside in the auth middleware avoids a MongoDB query on every authenticated request after the first token validation is cached
Three S3 client instances with different socket timeouts (2 s / 4 s / 6 s) routed by byte-range size to minimise time-to-first-byte on small initial video chunks
Newsletter worker processes up to 10 concurrent jobs with a rate limiter of 20 emails per second to avoid exceeding Brevo API limits
Winston logger skips log entries for /stream/ routes to avoid I/O overhead on high-throughput video connections; responses above 1 s trigger a separate WARN log
Security Model
JWT access tokens are validated against the Token record stored in MongoDB (one record per user per client type); logout deletes the record, invalidating the cache on next check
Stripe webhook endpoint uses express.raw() to preserve the raw body required for HMAC signature verification before any JSON parsing middleware runs
Video access requires a separately-signed short-lived JWT (VIDEO_JWT_SECRET, HS256, custom issuer and audience); the S3 object URL is never sent to the client
Login rate limiting tracked in Redis: 25 attempts per hour per email address, 20-minute lockout on breach; OTP attempts have a separate cap of 23 per 10-minute window