Work

Everything, client work first. Each one carries the part that was actually difficult, running here rather than described.

Live

Gunung

gunungclimbing.my (opens in a new tab)

Direct-to-consumer climbing gear for Southeast Asia. Storefront, inventory, checkout, and a four-market currency system, end to end.

Commerce that handles real money, real inventory, and four currencies in production.

Four markets, four currencies
MY · ID · SG · PH
The right country and currency picked automatically, and remembered. Rupiah prices drop the decimals, because rupiah does not have any.
Sells while you sleep
Stock stays honest
Inventory decrements on payment, not on add-to-cart, so two people cannot buy the last one.
Database changes since launch
14
Each one applied to a live store without losing an order.
Payments
Books actually balance
Card fees come back in ringgit whatever the customer paid in, and get converted before they hit the order.
  • Next.js 16
  • React 19
  • Supabase
  • Stripe
  • Vercel
RegionMYR
MYR189.00

MYR carries two decimals.

state

Real rates from the shipped storefront, and the button runs its real loading state when clicked.

Live

Consultant neurosurgeon

drsyedneuro.com (opens in a new tab)

A practice site for a consultant neurosurgeon in Malaysia: conditions, patient guides, publications, and a career timeline, all editable through a Supabase-backed admin area.

Patients read these pages before brain surgery. Accessibility and clarity were requirements, not preferences.

Written for patients, not doctors
18 pages
Conditions and guides rewritten from clinical sources into language someone can read the night before surgery.
Findable when it matters
Structured for search
Someone searching a condition at 2am finds the page, not the homepage.
Readability
Passes AA
Every text colour measured against its background; the faint greys were replaced.
Edits it himself
No developer needed
Conditions, guides, publications, photos and videos, all editable from a login.
  • Next.js 16
  • TypeScript
  • Supabase
  • Tailwind v4
  • JSON-LD
Condition searchtype a condition

Type a condition. At 2am, this is what Google shows — the page, not the homepage.

Live

SAPOT

book.sapot.app (opens in a new tab)

A peer support platform matching people with trained supporters who have lived experience. Rebuilt from WordPress: booking, payments, calendar sync and an admin area.

A live service with real bookings on it was migrated and rebuilt without losing an appointment.

Moved off WordPress
Nothing lost
Accounts and bookings carried into a new schema while the service stayed open.
Cannot be double-booked
Pattern plus exceptions
A weekly pattern, minus the days blocked off, minus what is already taken.
Consent and age
Recorded, not assumed
Stored per user, and under-age signups are logged rather than quietly refused.
Payments
Signed, not trusted
Callbacks are checked against a signature, so a forged one cannot buy a session.
  • Next.js 15
  • React 19
  • TypeScript
  • Supabase
  • Billplz
  • Google Calendar
Bookable hours5 open
  • 09:00Open
  • 10:00Open
  • 11:00Open
  • 12:00
  • 13:00
  • 14:00Open
  • 15:00Open
  • 16:00

The recurring pattern on its own. Turn on the other two to see what removes hours from it.

Live

NarrativeScapes

Client work · private tool

A node-based canvas for building image campaigns. Brand feeds keywords, keywords feed context, context feeds topics, each stage generating from the one before it.

Built to a designer's Figma file rather than to whatever was convenient to build.

A pipeline, not a form
Nine node types
Edit one and everything built from it knows it is out of date.
Built to the Figma
Not approximately
Where the design was awkward to build, the design still won.
Every generation kept
Rewindable
Trying something new never costs the version that already worked.
Found in production
105 images → 12
Regenerating kept adding images without retiring the old ones. Diagnosed and fixed in place.
  • React
  • React Flow
  • Python
  • Supabase
  • PostgreSQL
Generation graphall current

Every node is current. Click any one to edit it and watch what that costs.

Live

AALC

learnkoreanwithaisyasya.com (opens in a new tab)

A Korean language school's course catalogue and shop. Students buy a course, get their materials, and the teacher sees the sale without logging into anything unfamiliar.

The admin panel is a spreadsheet, because that is what the client already knew how to use.

Runs on a spreadsheet
No admin to learn
Every sale writes a row to the teacher's own Google Sheet.
Materials behind payment
Checked server-side
Downloads are released once Stripe confirms, not when the page says so.
Heavy media, fast pages
Loaded on demand
Routes and images arrive as they are needed rather than all at once.
  • React
  • Express
  • Stripe
  • Google Sheets API
  • Vercel
Course materialswithheld
lesson-01.pdfLocked

Nothing has been paid yet. Turn on either claim to see which one the server actually believes.

Live

Midpoint

midpoint.city (opens in a new tab)

Finds a fair meeting point for a group: three optimisation strategies, live voting, in-meeting chat, and venue discovery around the result.

When the map provider goes down, it quietly switches to a backup and the user never sees an error.

When the map provider dies
Nobody notices
A second service takes over mid-request. An outage on their end is not an outage on yours.
“Fair” is three different answers
You pick which
Shortest distance, everyone travels the same time, or least total travel. They rarely agree.
Everyone sees it live
Voting and chat
Votes and messages appear for the whole group as they happen.
Findable on Google
Every page
Pages arrive complete rather than being assembled in the browser, so search engines can read them.
  • Next.js
  • Supabase
  • Leaflet
  • Foursquare
  • OpenRouteService
Routingidle
  1. OpenRouteService
  2. Failover
  3. GraphHopper

Waiting to work out travel times

Two map services are set up. The main one goes first.

Research

Bangkit Bionics

Research project

A prosthetic hand that reads what the wearer is trying to do. Sensors on the forearm pick up muscle signals, and a model works out which of four grips was intended, twenty times a second.

The model with the best headline score was the worse model. Picking the right one meant knowing which mistakes hurt.

Grips read correctly
84.9%
Across 22 people, on recordings the model had never seen.
Hardest grip, before and after
42% → 69%
The key grip was the one it kept missing. Fixing that cost some overall accuracy and was worth it.
Reads muscle signals
20 times a second
Filtered to strip out electrical noise and heartbeat before anything is decided.
Works for amputees
6% → 64%
Trained on people with intact limbs first, then adapted. That transfer is what made it usable.
  • PyTorch
  • NumPy
  • SciPy
  • WebSocket
  • NinaPro
Reading grips from muscle signalschosen
  • Open85.9%
  • Power88.9%
  • Pinch79.0%
  • Key68.6%

Overall score 84.9%. Lower on paper, better in the hand: pinch and key are the grips you reach for, and both now clear the line.

Have something that needs building?

Tell us what it is, what it has to do, and when it needs to be live.

Start a project