Back to Projects
NestJSTypeScriptNext.jsReactReact NativeExpoPostgreSQLRedisSocket.IOOpenAIMediaConvertCloudFrontAWSSentry

The ZFA ERP

The platform the largest forex trading institution in Zimbabwe runs on, serving over five thousand students. A public site, a client portal, a twenty seven section admin console, a learning platform with its own transcoding and transcription pipeline, a bill payments service on top of a bank's biller engine, and an iOS signals app.

The ZFA ERP

Project Overview

Zhou Forex Academy is the largest forex trading institution in Zimbabwe, with over five thousand students, and this is the system it runs on. It started in 2023 as a website with a chatbot. It is now a public site, a client portal, an admin console with twenty seven sections, a learning platform with its own video pipeline, a bill payment service wired into a bank, and an iOS app. Calling it a website stopped being accurate a long time ago.

The shape of it: a NestJS backend of 437 files and roughly 80,000 lines across 47 modules, a Next.js front end of 517 files and about 96,000 lines carrying both the public site and the admin console, a separate Vite application for the learning platform, an Expo app at 27,000 lines, and a Next.js bill payments front end. Postgres and Redis underneath, most of it on Elastic Beanstalk, Sentry watching the front end.

The learning platform is the piece I am proudest of, because paid video is genuinely hard to do properly. An instructor uploads a raw file to S3 and a MediaConvert job cuts it into an HLS ladder at 1080p, 720p and 360p, so a student on a phone in Bulawayo with two bars of signal gets a stream that actually plays. Delivery is CloudFront with signed URLs generated per request from a private key, which is the part that matters commercially: a paid lesson cannot be turned into a link and passed around a WhatsApp group. Jobs are polled on a schedule, retried up to three times, and the uploader gets an email when their video is ready, because nobody should have to sit refreshing a page to find out whether a transcode survived.

Then the transcripts, which is where it gets interesting. A second Lambda pushes each video through AWS Transcribe, polling every fifteen seconds up to fifty six times, which is about fourteen minutes and deliberately one minute short of the Lambda timeout so the function reports a clean failure instead of being killed mid-poll. The transcript lands in S3 and calls back into the API, and that callback is what triggers everything downstream. A summary is generated automatically. Quizzes are generated from the transcript on request, capped at 18,000 characters of context, produced in batches of twenty questions with three attempts per batch and a hard ceiling on total iterations, because a model asked for eighty questions in one call will cheerfully return sixty and a truncated JSON object. And students can ask questions about the video they are watching, with conversation history kept in Redis for twenty four hours and capped at forty messages, since the point is to answer a question about minute fourteen, not to remember last week.

The payments side is older than everything else and has grown the most scar tissue. Zimbabwe does not pay by card, so the public payment portal takes EcoCash, InnBucks, Mukuru and WorldRemit, and for the methods that cannot confirm themselves it takes an uploaded proof of payment that a human then approves. Around that sits deposit requests, withdrawals back to mobile wallets, configurable charges, and Paynow for the paths it covers. There is an important boundary written into the product: ZFA moves money between a student's mobile wallet and their broker, and never holds a balance, so the funds sit with the broker rather than with us. That sentence is in the app store listing on purpose.

The bill payments platform is the newest addition and the one that involved the most PDF archaeology. It is a public service for paying DSTV subscriptions, ZETDC electricity tokens and school fees, sitting on top of ZB Bank's biller engine over SOAP. School fees needed two different flows, which took a while to understand: universities and colleges are registered institutions you pick from a list and validate by student number, while primary and secondary schools all pay through one generic biller where the customer types the school and reference themselves. Every payment validates the account first, so the customer sees the name on the account before committing money, then initiates, then lands on a receipt page with a renderer written per service, because a DSTV receipt and an electricity token receipt carry genuinely different things and pretending otherwise produces a receipt with empty rows on it.

The mobile app, ZFA Signals, is built around one decision that took me an afternoon to accept. A signal requires exactly three fields: the pair, the direction and the status. Everything else is nullable. Zhou posts a price sometimes, a zone sometimes, and quite often just "buy gold now", and a schema that insisted on an entry price would force whoever publishes to invent one. An invented stop loss is worse than no stop loss. So the app carries the difference in the data instead: a market entry renders as "Market, now", a zone renders as a range, and a missing hold time renders as "No hold time given, watch for a close instruction" rather than a blank row. A blank row and "he did not say" mean opposite things to somebody sitting in a live trade.

Updates to a signal are an append only log. Enter now, add position, target hit, move stop, close now, note. Nothing is edited and nothing is deleted, so a "move your stop to breakeven" is a new row rather than a rewrite of the stop loss field, because somebody who entered an hour ago has to still be able to see the instruction they acted on. Notifications run in two lanes on purpose: new calls and close instructions arrive as time sensitive alerts that take over the screen, while target hits and notes arrive as ordinary ones. The alerts that cost money to miss are the only ones allowed to interrupt you.

Every call also opens its own chat room, and the rooms have a lifecycle rather than a settings page. A signal room is born with its call and dies with it, while the master trading floor outlives every call and is where the community sits. I deliberately did not reuse the existing chat module for this, even though it was right there, because that one is customer support: staff guarded conversations with a participant list and delivery receipts, built around one agent talking to one client. A trading floor is open to everyone on a channel, has no participant list, and its lifetime belongs to a signal rather than to a ticket. Bending either into the other would have left both worse.

Live prices nearly broke the budget before I understood the vendor's billing. Twelve Data charges one credit per symbol rather than per request, so a twelve symbol batch is twelve credits, and on the plan we are on it is simply refused. Twelve instruments refreshed every five seconds would be 207,360 credits a day against an allowance of 800, which is 259 times over, and would have been cut off about forty five seconds after boot. So prices are paced rather than live, with two limits enforced separately, per minute and per day, and a reserve held back out of each so that a signal going out can always be priced immediately even in the middle of a poll. The app is told how old every price is and never pretends otherwise, which is the only version of this that is honest.

The rest is the unglamorous half that makes it a real system: KYC with document review, an affiliate programme with its own guards and commission tracking, memberships with expiry reminders, role based access control, audit logs, presence, a WhatsApp chatbot with scripted flows, inbound mail handling through SES receipt rules, AI drafted email replies, a knowledge base, a library of ebooks with a scheduler, events with seat reservations, a blog, a shop, and a buy bitcoin request flow. The admin console has a section for each of these and a dashboard on top of all of it.

The iOS app went live on the twentieth of September 2026. Android is next, and the website currently says "Android coming soon" underneath a greyed out Google Play badge, which is the most honest button I have ever shipped.

Key Features

  • Forty seven backend modules behind a public site, a client portal and a twenty seven section admin console
  • Learning platform with courses, topics, videos, enrollments, quizzes and per video progress
  • MediaConvert HLS ladders at 1080p, 720p and 360p, so a phone on a weak signal still plays the lesson
  • CloudFront signed URLs minted per request, so a paid lesson cannot be passed around as a link
  • Transcode jobs polled on a schedule, retried three times, with an email to the uploader when one lands
  • AWS Transcribe in a Lambda that stops one minute short of its own timeout and reports a clean failure
  • Automatic video summaries triggered by the transcription callback
  • Quiz generation from transcripts, batched twenty at a time with capped retries and a context ceiling
  • Ask a question about the video you are watching, with history in Redis for twenty four hours
  • Payment portal for EcoCash, InnBucks, Mukuru and WorldRemit, with proof of payment review
  • Deposit requests, withdrawals to mobile wallets, configurable charges and Paynow
  • Bill payments for DSTV, ZETDC electricity and school fees over ZB Bank's SOAP biller engine
  • Two school fees flows: registered institutions validated by student number, and a generic biller for primary and secondary
  • Account validation before payment, so the customer sees the name on the account before committing money
  • Signals with only pair, direction and status required, because an invented stop loss is worse than none
  • Append only signal updates, so an instruction someone acted on an hour ago is never rewritten
  • Two notification lanes, keeping close instructions apart from target hits and notes
  • A chat room per signal that dies with the call, plus a master trading floor that outlives every one
  • Paced live prices with per minute and per day credit budgets, each holding a reserve for publishing a signal
  • KYC, affiliates, memberships, RBAC, audit logs, a WhatsApp chatbot, inbound mail, an ebook library and a shop

Get the apps

Zhou Forex Academy app icon

Zhou Forex Academy

Live trade calls, the room under each one, and the wallet

App StoreAndroid soon

Challenges & Solutions

Serving paid video without handing students a URL they can forward to everyone they know

A transcription Lambda that must finish, or fail honestly, inside a fifteen minute ceiling

A model asked for eighty quiz questions returns sixty and a truncated object, so generation had to be batched

Zimbabwe does not pay by card, so half the payment methods confirm themselves and half need a human

Learning ZB Bank's biller engine from a folder of PDFs and a SOAP envelope

School fees needing two entirely different flows that had to look like one product

A publisher who sometimes gives an entry price and sometimes says buy gold now, which no strict schema survives

Twelve Data bills per symbol, so twelve instruments every five seconds would have burned the daily allowance in forty five seconds

Resisting the reuse of a support chat module for a trading floor, which would have left both worse

Key Learnings

Nullable fields are sometimes the correct model, because forcing a value invents one
An append only event log is the only honest way to record instructions people have already acted on
Two notification lanes are worth the effort, since an alert that always interrupts stops being read
Showing how old a price is beats pretending it is live, especially when the vendor bills per symbol
A lifecycle owned by a signal rather than a settings screen removes an entire category of admin work
Leaving a minute of headroom under a timeout turns a killed function into a readable error

Project Info

Timeline

Since 2023, still going

Team

Solo build for Zhou Forex Academy

Status

Live

Tech Stack

Backend

NestJSTypeScriptTypeORM

Frontend

Next.jsReactViteTailwind

Mobile

React NativeExpoTanStack QueryZustand

Data

PostgreSQLRedis

Media

MediaConvertCloudFrontTranscribeS3Lambda

AI

OpenAI

Realtime

Socket.IO

Payments

PaynowEcocashInnbucksZB Bank

Infrastructure

AWSElastic BeanstalkSESSQSSentry