Case studies

How the projects were built

The problem behind each product, what it needed to do, how I built it and where it ended up. Click any screenshot to see it full size.

01 / 08 · Co-founder & developer

GradeIt

An online TCG hobby store and South Africa's card-grading middleman. I built the website, a Flutter app for creating grading submissions, and the card database behind both.

gradeit.co.za →
GradeIt stats page with items graded and most graded cards
7,038historical items imported
5,735graded items on record
92.8%items linked to the card database
364customers on the leaderboard

The problem

Grading orders were managed by hand, and years of history lived in a spreadsheet of 44 batch sheets. Customers couldn't submit or track orders themselves, and nothing linked a graded slab back to the actual card, so stats and leaderboards weren't possible.

Goals

  • Let collectors build and pay for submissions themselves, on the web and on their phone.
  • Bring every historical order in without losing or mis-attributing anything.
  • A card database so every graded item resolves to a real card, set and price.
  • One place for pricing, stock and payment rules.

What I built

GradeIt submission wizard: choose your grading service

Submission wizard

A custom WooCommerce plugin powers a multi-step wizard: choose a grading company, card type, service level and add-ons, then search the card database to add each card. Insurance and totals are calculated as you go.

GradeIt grading costs table by company and turnaround

Card database & history import

A nightly catalogue sync runs in self-rescheduling batches and never deletes rows, so old orders always resolve. Classifier and language fixes were verified against ~195k catalogue rows.

An idempotent importer turned the spreadsheet into 116 real submissions. It matched 88% of items to customer accounts, linked 1,155 orders, and sent ambiguous rows to human review instead of guessing.

GradeIt app: choose a grading service GradeIt app: grading options with live total GradeIt app: grade reveal

Flutter app on a REST API

The plugin exposes a REST API with JWT access tokens and rotating refresh tokens. The app's cart is the real store cart, so coupons, wallet and shipping behave exactly like the website. Store rules are enforced on the server, never in the app.

Payment hands off to the website's checkout through a single-use sign-in link, so Apple Pay, Google Pay, Capitec Pay, PayJustNow, EFT and wallet all work without being rebuilt. A deep link brings the customer back to the order.

PSA's free API allows about 100 calls a day, so grade reveals are cache-first and customers never call PSA directly.

GradeIt app welcome screen GradeIt app grading services GradeIt app grading options GradeIt app grade reveal GradeIt app submission tracking GradeIt app shop

Outcome

Every historical batch is now a real submission with stats and leaderboards. Uncatalogued items on the stats page dropped from about 51% to 5.5%. Collectors create, pay for and track submissions on the site, and the app is ready for its store release.

  • WordPress
  • WooCommerce
  • Custom PHP plugin
  • REST API
  • JWT
  • Flutter
  • Riverpod
  • Docker

02 / 08 · Founder & sole engineer

ClubRuggas

Fixtures, results, standings, line-ups and live crowd-sourced scores for South African club rugby, on a website and a mobile app.

clubruggas.co.za →
ClubRuggas homepage
100%test coverage, enforced in all 3 repos
2clients: website and mobile app
50gamification levels, each with a reward

The problem

Club rugby results in South Africa are scattered and slow to appear, and most matches never get live coverage. Supporters at the ground know the score long before anyone publishes it.

Goals

  • One place for every club's fixtures, results, standings and team sheets.
  • Live scores from the crowd, without letting anyone corrupt official results.
  • Reward fans for contributing, and keep web and mobile in sync with one API.

What I built

ClubRuggas match page with score, scorers and line-ups

Score integrity

Official scores change through a single locked, audited, revisioned path. Match events are append-only. Fan claims go through weighted voting and consensus to produce a provisional score that never overwrites the official one.

Log tables update incrementally, and a property test checks them against a full nightly rebuild.

ClubRuggas fixtures and results with filters

Realtime and one API contract

Websockets (Django Channels) carry match, live and personal feeds, broadcast only after the database commit, with version-stamped caching on public reads.

The OpenAPI spec is the source of truth, and the React types and Dart models are generated from it. An undeclared filter that once silently returned unfiltered lists is now a compile error.

ClubRuggas tournaments list

Data ingest and gamification

A polite nightly scraper (ETags, rate limits) brings in competitions and skips finished ones. Fans earn XP, streaks and cosmetic unlocks at every level from 1 to 50, and every uploaded image goes through a strict pipeline of validated masters, generated variants and content-hash names.

The mobile app

ClubRuggas app home ClubRuggas app results ClubRuggas app match summary ClubRuggas app match timeline ClubRuggas app fixtures ClubRuggas app tournaments

Outcome

Live in production on AWS (Cape Town region). A git tag builds the images and deploys with health checks, and the Flutter app ships to the Play Store's internal track. All three codebases hold a 100% coverage gate in CI.

  • Django 5.2
  • DRF
  • Channels
  • Celery
  • PostgreSQL
  • Redis
  • React 19
  • Tailwind
  • Flutter
  • AWS EC2
  • GitHub Actions

03 / 08 · Owner & developer

PetTrek

Trusted pet care, booked in two taps. A South African marketplace where owners book walking, sitting, grooming or training, and vetted local handlers take the jobs.

pettrek.co.za →
PetTrek homepage

The problem

Finding a trustworthy dog walker usually means word of mouth and WhatsApp. Owners can't check who's coming, see the walk happen or pay safely, and good handlers have no easy way to find work.

Goals

  • A two-sided marketplace: owners book in a couple of taps, handlers accept jobs and get paid.
  • Trust first: handlers are identity-verified before they can work.
  • Live walk tracking and chat.
  • Money handled on the server and never trusted from the browser.

What I built

PetTrek handler sign-up

Handler onboarding

A separate handler portal. Applicants submit a FICA document pack (SA ID, certified copy, proof of address), ID numbers are checksum-validated, and approval and verification are separate steps.

PetTrek features: vetted, live walk tracking, booked in two taps

Bookings, realtime and hardening

Prices are calculated on the server, job acceptance is row-locked so two handlers can't take the same booking, and status and payment fields are protected from the client.

A dedicated ASGI service runs chat and live location, with a tracking toggle for handlers and a live map for owners.

Before launch I ran a full security audit, fixed every finding with regression tests, and set a backend coverage floor of 94%. TLS auto-renews with a fallback certificate so a renewal problem never takes the site down.

Outcome

The web platform is live at pettrek.co.za, with a Flutter app alongside. Next up: switching on payments and a design-system refresh.

  • Django
  • DRF
  • Channels
  • Celery
  • PostgreSQL
  • React 19
  • MUI
  • Leaflet
  • Flutter
  • AWS EC2

04 / 08 · Owner & developer

ClaimsaleBot

A WhatsApp bot for collector communities. It runs claim sales, keeps a vouch-based reputation for every member and tracks moderation per group, with a dashboard for hosts and group admins.

whatsappclaimbot.co.za →
ClaimsaleBot sign-in

The problem

Collectible sales in WhatsApp groups run by hand. The host scrolls back through hundreds of messages to work out who claimed first, chases winners and keeps a spreadsheet of orders. Trust is word of mouth too: "is this seller legit?" gets asked in every group, and the answer is buried in old chats.

Goals

  • Award every slot instantly and fairly, and track claims, auctions and orders.
  • Give every member a reputation that can't be easily gamed.
  • Let group admins track warnings and manage their own group.
  • Contact winners without getting the host's number banned.

Claim sales

🃏
Cape Collectorsbot, Host, 214 members
Host📷 Charizard ex SIRR650 · NM · 1 available
LeratoClaim👍
Pieterclaim👎
Host📷 Booster boxR1,800 · Sealed
Auction · R50 increments · ends 20:00
Pieter1850
Sam1900

Illustration with fictional names

First claim wins, automatically

A small Node bridge (Baileys) holds the WhatsApp session and forwards every message to a Django + Celery engine. Listing captions are auto-parsed for price, condition and quantity. The first claim gets 👍, later ones 👎, and ❌ marks a listing sold out. Auctions support reserves, increments and end times.

When a winner deletes their claim, the next-oldest claim is promoted automatically. At the end, an orders dashboard tracks payment, collection or PUDO delivery and tracking numbers, with bulk updates for hosts.

The bot never cold-DMs anyone, because that gets numbers soft-banned. An end-of-sale shout-out invites winners to message first, which unlocks a DM menu with their tally, payment details and a PUDO locker search.

Vouches & reputation

🤝
Cape Collectors · Reviewsbot, 214 members
Lerato@Sipho +rep great seller, fast shipping👍
Lerato@Sipho +rep
ClaimsaleBotYou were the last person to rep Sipho, so that one doesn't count. Someone else has to rep them before you can again.
Dan@Sipho +rep👍
ClaimsaleBot🎉 Congrats @Sipho has reached 🔥 Trusted rank!
Pieter@Sipho rep
ClaimsaleBotSipho 🔥 Trusted
Pos: 12 (10 unique), Neg: 0 (0 unique)
15 more people to 💎 Elite

Repped by (their own rep in brackets):
👍 Lerato ⭐ ×2 (Pos: 6, Neg: 0)
   great seller, fast shipping
👍 Dan (Pos: 2, Neg: 0)
…and 8 more

Illustration with fictional names. The reply formats are the bot's own.

Reputation that's hard to game

Any member can vouch for another with @user +rep or -rep, with an optional comment, for up to five people in one message. Vouches are append-only, self-reps are blocked by a database constraint, and nobody can rep the same person twice in a row. Someone else has to vouch in between.

Ranks are scored on unique people, not the raw total, so two friends trading reps can't farm a crown. The ladder runs 5 ⭐ Rising, 10 🔥 Trusted, 25 💎 Elite, 50 🏆 Legendary, 100 👑 King Repper, and only upward milestones are announced.

A lookup shows each voucher's own standing in brackets, so a vouch from someone credible reads differently to one from a brand-new account. History is kept when a member leaves the group, and a typed number can still find it.

Commands only count when the message starts with the tag. A real early bug: "Are your negative reps gonna be there forever @jared" posted Jared's review list to the group.

Group tracking & moderation

🛡️
Cape Collectors · Reviewsbot, Admins, 214 members
Admin@Kyle warn no-show on collection
ClaimsaleBot⚠️ Warning recorded for Kyle, they now have 2 warnings.
Admin@bot reppers month
ClaimsaleBot🏅 Top reppers - this month

1. 🔥 @Sipho (Pos: 8, Neg: 0)
2. ⭐ @Lerato (Pos: 5, Neg: 0)
3. @Dan (Pos: 2, Neg: 0)

Illustration with fictional names

Every group runs its own way

Groups opt into features: claim sales, reputation or both. A reviews-only group ignores claims entirely, and reputation is off until an admin switches it on, so the bot never surprises a group.

Group admins are assigned per group with per-capability permissions, and get a scoped dashboard for their own groups only. Warnings are issued in chat (@user warn reason) or from the dashboard, and are revoked rather than deleted, so the record stays intact.

Leaderboards show top reppers all-time, this week, month or year, and hosts get stats, revenue trends and leaderboards for their sales.

Outcome

Running in production on AWS Lightsail with daily database backups to S3, checked by a CI canary. Buyer identity is keyed on WhatsApp's newer private IDs, so it's ready for WhatsApp usernames. When the bridge once stayed "connected" while silently receiving nothing, I added a nightly restart that keeps the session, and it's been stable since.

  • Django
  • DRF
  • Celery
  • Redis
  • PostgreSQL
  • Node.js
  • Baileys
  • Docker
  • AWS Lightsail
  • S3

05 / 08 · Owner & developer · Built for Starke Ayres sales staff

Stark Notes

A field app built for Starke Ayres sales staff to keep notes on their areas and the seed conditions each one needs, track their test plants, and reach seed documents and information quickly.

Stark Notes home screen Stark Notes crop catalogue with search Stark Notes variety detail with fact sheet Stark Notes variations with comparison tables and growing guides Stark Notes trial tracking Stark Notes area notes
125seed varieties in the catalogue
25crop categories
5trial stages, delivery to archive

The problem

Seed sales reps cover several areas, and each has its own conditions that decide which varieties will do well. Their knowledge of those areas lived in their heads, notebooks and chat messages. Variety details (maturity, sunlight, water, soil) sat in separate PDF fact sheets and growing guides on the website. Test plants sent to nurseries and farms were tracked by hand, so results were easy to lose.

Goals

  • Notes per area and per variety, with photos, so field knowledge isn't lost.
  • Track every test plant from delivery to results in one place.
  • Every fact sheet, comparison table and growing guide one tap away.
  • A private account per rep, so each person sees only their own trials and notes.

What I built

Catalogue import

A scraper turns the public Starke Ayres catalogue into structured JSON, and an idempotent Django management command imports it: 25 categories and 125 varieties with fact sheets, comparative tables and production guidelines. The API offers search and filters by maturity days, sunlight, water and soil.

One data model for varieties

I refactored separate variation, sub-variation and three media models into a single hierarchical CropItem (sub-items via a parent link) and one MediaAsset model. I wrote a migration guide so the app could move to the new endpoints and field names.

Test plant tracking

Each trial moves through Awaiting delivery → Delivered (to a nursery or farm) → Planted → Complete → Archived, with a date stamped at every stage, plus the farm contact. Trial varieties are reusable across trials, and dated results record each observation with its value and unit.

Notes, favourites and the app

Notes are private to each rep, can be linked to a variety, and carry photos, files or links, with archiving, so observations about an area and what grows there stay together. Favourite varieties are one tap away. The Flutter app stores JWT tokens in secure storage, signs back in automatically, and switches between local, tunnel and production APIs per build.

In the app

Bean variations with notes, tables and favourites Tomato sub-varieties Variety detail with growing information Trials list with status filters Trial varieties Favourite varieties

Screens captured from the app running against a local copy of the backend with the full catalogue and demo data. Farm names and notes are fictional.

Outcome

Deployed with Docker Compose (Django, Postgres 15 and nginx) on AWS EC2 with TLS. It has been developed over more than 60 commits since August 2025, and is moving to its own server and domain next.

  • Django
  • DRF
  • JWT
  • PostgreSQL
  • Scraping
  • Flutter
  • Provider
  • Docker
  • nginx
  • AWS EC2

06 / 08 · Developer · Squarespace → WooCommerce

Move360

A gym, Pilates, physio and recovery studio in Durbanville. They asked me to move their Squarespace site to WooCommerce while keeping the original styling. I converted it and added a shop, member sign-up and a booking system.

move360.co.za →
Move360 homepage
1:1visual match, desktop and mobile
100%of URLs and SEO titles kept
8pages rebuilt as editable blocks

The problem

The site was locked into Squarespace, bookings went through a paid third-party embed, and there was no way to sell packages or memberships online.

Goals

  • Look exactly like the existing site, so customers notice nothing.
  • Don't lose any search rankings in the move.
  • Add a shop, sign-up and an in-house booking system with online payment.
  • Let the owner edit everything without code.

What I built

Move360 Pilates page

Same design, new platform

A custom theme, screenshot-compared against the original page by page. Every page is native Gutenberg blocks with ready-made Move360 section patterns, so the owner can add and edit sections without a page builder.

The move was SEO-safe: identical URLs verified against the old sitemap, 301s for legacy links, titles and descriptions carried over, plus local-business structured data and alt text the old site never had.

Move360 booking calendar

Booking system, shop and sign-up

A Calendly-style booking plugin replaced the paid embed: recurring and one-off slots, per-slot capacity, blackout dates, member and non-member rates priced on the server, and an admin week calendar.

PayFast checkout is shared by shop and bookings. Unpaid spots are held for 20 minutes, and a validated payment webhook confirms them. Session-credit packages and memberships only ask customers to sign in when they buy one, and credits are refunded on cancellation.

Outcome

Live at move360.co.za with the same look and URLs as before, a working shop, and bookings taken directly on the site instead of through a paid embed.

  • WordPress
  • WooCommerce
  • Custom theme
  • Custom PHP plugin
  • PayFast
  • Gutenberg
  • Docker

07 / 08 · Lead developer

Hobby Huyss

A TCG and hobby store selling Pokémon, One Piece, MTG and more, plus board games and accessories, with in-store events and leagues.

hobbyhuyss.co.za →
Hobby Huyss homepage
~135products at launch
~180supplier categories mapped
4stock states incl. pre-order

The problem

The existing site was half-configured: placeholder menus, no proper catalogue structure and no link to the distributor's stock. Most of the range is sourced on demand, so the store couldn't show what it was actually able to sell.

Goals

  • A store organised the way TCG players shop: game, then era, then set.
  • Sell distributor stock on back-order safely, without overselling.
  • Local shipping and payments, legal pages and a hardened admin.

What I built

Hobby Huyss shop with filters and back-order products

Catalogue and distributor sync

A Game › Era › Set brand tree drives the mega-menus and filters. In-stock, out-of-stock, back-order and pre-order states stay in sync automatically.

A custom sync plugin reads the distributor's live stock through their store's JSON endpoints, with no scraping. Customers can back-order up to a safe share of the distributor's stock, and prices are calculated from cost, VAT and markup, rounded and capped at RRP.

Checkout covers Courier Guy, PUDO and in-store collection, with PayFast, Stitch and PayJustNow. Security-hardening and migration plugins mean large backups restore without paid tools.

Outcome

Live at hobbyhuyss.co.za with a structured catalogue, distributor-backed back-orders and local payment and shipping options.

  • WordPress
  • WooCommerce
  • Custom PHP plugins
  • Porto
  • WP-CLI
  • Docker

08 / 08 · Developer

Mi Adventure Tours

A themed WooCommerce site built as a portfolio and booking page for Mizo's Egypt tours, showing off the team, past trips and tour options, with a booking form on every tour.

Mi Adventure Tours homepage

The problem

Mizo needed a professional home for the tours: somewhere to show the team and past trips, explain each itinerary, and take bookings directly instead of over messages.

Goals

  • A polished travel site, focused on Egypt only.
  • Tour pages with itineraries, route maps, galleries and pricing.
  • A booking form on every tour, with date and group-size selection.

What I built

Tour page with booking form

Tours and booking

A travel theme with a tour-booking plugin. The 8-day and 11-day Egypt journeys were rebuilt to a consistent layout, with custom Google My Maps route embeds, activity highlights, and a booking panel with date, group size and live pricing.

About page

Real content, update-safe

All theme demo content was replaced with real About, Privacy and Terms copy. Text, style and mobile layout fixes live in a must-use plugin, so theme updates never undo them. A verification script checks every page for the required content and flags any leftover demo text.

Outcome

A complete portfolio and booking site with tour listings, team and gallery pages, and a booking flow from date selection to checkout.

  • WordPress
  • WooCommerce
  • Tour booking
  • PHP
  • WP-CLI
  • Docker