My Financial Trading
02Full-Stack Web Application

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.

Full-StackVideo StreamingMulti-Gateway PaymentsReal-Time
01 // Overview

About the Project

What it is

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.

Who it's for

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.

Why it exists

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.

02 // My Role

What I Built

Backend Engineer

Designed and implemented the full Express/TypeScript API across 23 feature modules: auth, courses, payments, streaming, community, notifications, newsletter, and the partner affiliate system.

API & Database Architect

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.

Frontend Engineer

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.

Partner Portal

Developed the Vite + React affiliate portal with referral dashboards, tier progress tracking, payout request flows, and QR-code-based referral link sharing.

03 // Key Capabilities

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

04 // Under the Hood

Engineering Deep Dive

Click any card to read the full technical detail.

05 // Engineering Decisions

Challenges & Trade-offs

The real decisions behind the architecture.

⚠ Challenge

Token-based auth added a MongoDB lookup on every request, which became a bottleneck under concurrent load.

→ Decision

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.

⇄ Trade-off

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.

⚠ Challenge

Video files needed to be access-controlled without exposing the storage URL, while still supporting HTTP range requests for seeking.

→ Decision

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.

⇄ Trade-off

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.

⚠ Challenge

Video compression and thumbnail generation are CPU-heavy and would block the event loop if run inline in a request handler.

→ Decision

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.

⇄ Trade-off

Adds latency between file upload and content availability. Stream-video-processing concurrency is set to 1 to avoid exhausting CPU on a constrained deployment.

⚠ Challenge

Safari and iOS clients require different Cache-Control and Connection header semantics for HTTP range requests to function correctly during video playback.

→ Solution

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.

✓ Result

A single streaming endpoint handles all browser targets without client-side changes.

⚠ Challenge

Scheduled newsletters could be silently missed if the server restarted between the scheduling cron tick and the delivery time.

→ Solution

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.

✓ Result

Missed newsletters are detected and re-queued on the next cron tick with no manual intervention required.

⚠ Challenge

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.

→ Solution

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.

✓ Result

Subscription state is consistent across gateways; the rest of the application only reads PaymentStatus without knowing which gateway was used.

06 // Architecture

System Design

Feature-module pattern: each domain (Auth, Payment, StreamVideo, Community, Partner, etc.) has its own controller, service, route, validation, and interface files — 23 modules total
Split Prisma schema across 10 domain-scoped .prisma files compiled against a single MongoDB datasource
Three BullMQ queues sharing one Redis connection: video-processing, stream-video-processing, and newsletter — each with a dedicated Worker and concurrency settings
Socket.io server mounted on the same HTTP server instance, sharing CORS config with Express; an in-memory email-to-socketId map routes notifications to specific connected users
DigitalOcean Spaces as the S3-compatible object store; three S3 client instances with different timeouts (2 s / 4 s / 6 s) selected at request time based on byte-range size
07 // Performance

What Makes It Fast

Redis

Redis cache-aside in the auth middleware avoids a MongoDB query on every authenticated request after the first token validation is cached

Three

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

Newsletter worker processes up to 10 concurrent jobs with a rate limiter of 20 emails per second to avoid exceeding Brevo API limits

Winston

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

08 // Security

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

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

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

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

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

09 // Deployment

How It Ships

Backend deployed to Vercel (vercel.json present in dev-backend); DigitalOcean Spaces (nyc3) used as S3-compatible object storage
Next.js 15 frontend deployed to Vercel, running with Turbopack in development on port 8001
Affiliate partner portal built with Vite + React, deployed to Vercel (vercel.json present in partners/)
Redis shared across the BullMQ queue connection and the auth/cache layer via a single ioredis client instance
11 // Resources

Links & Assets

01 // HERO