01
Bilingual RAG + NL-to-SQL assistant for immigration officers
In production
Client system · government agency · ~9,500 LOC · AWS EC2
One chat interface answering two unrelated classes of question, in Thai or
English: statistical queries against ~10.85 million passenger records, and
procedural questions about an official operations manual.
FastAPIReact 19ClickHouse
PostgreSQLRedisChromaDB
vLLM · Llama 4bge-m3Caddy
Engineering detail
What I built
- A rule-based router, deliberately not an LLM one. A keyword/regex scorer weighs a policy score against a statistics score over bilingual keyword lists, with hard-coded fast paths, across three answer patterns: SQL statistics, policy retrieval, and ephemeral per-session document Q&A. Cheaper, faster, and auditable — and a SQL route can re-route itself to retrieval when it finds no statistical signal.
- NL-to-SQL through hand-written templates. An intent classifier produces ~40 named intents, each mapped to a reviewed ClickHouse query. Free-form LLM SQL was off the table: a government system cannot risk a hallucinated query against live records.
- Retrieval that decomposes. Complex scenario questions split into up to five sub-topics, retrieved separately, then de-duplicated, over a local embedding model — no document text leaves the host.
- Two separate follow-up mechanisms. Parameter inheritance for statistics ("what about China?" reuses the previous intent) and scenario enrichment for policy questions, both over a Redis context with a 30-minute TTL.
- A memory-poisoning guard. Prior SQL statistics are stripped from the model's message window on retrieval routes, so passenger numbers cannot bleed into a procedural answer.
- Honest refusal. Citation verification and relevance-gated retrieval, so the assistant declines rather than fabricates.
- Production hardening. JWT auth, OTP email verification, PDPA-compliant audit logging, Docker Compose, Caddy TLS.
- Measured, not asserted. A 92-question bilingual benchmark: 98.9% routing accuracy, 91.3–95.7% judge-verified answer correctness.
"The interesting decision was refusing to let the LLM write SQL. For a
government system, an auditable rule-based router into reviewed query
templates beats a smarter but unverifiable agent — and I have a
92-question benchmark showing the router hits 98.9%."
02
Passenger risk-scoring system
Decision support
Client system · immigration checkpoints · ~13,400 LOC · 10.85M training records
A hybrid 0–100 risk score for inbound passenger screening that an officer can
read the reasons for. A rule engine, a gradient-boosted model and an anomaly
signal, fused and banded into a recommended action — with a human always in
the loop.
PythonXGBoostpandas
FastAPIDocker
Engineering detail
The engineering that matters
- Rules weighted above the model on purpose — 0.50 rules / 0.30 ML / 0.20 anomaly. The model predicts a narrow document-violation proxy; the rules carry actual screening policy. Defending that weighting is the core of the design.
- Hard overrides and soft floors. Expired visa, watchlist hit or prior deportation force the top band regardless of score; certain profiles set a floor. A statistical model should not be able to talk a system out of a categorical rule.
- Leakage-safe training across five source systems. GroupKFold on passport number, 5-fold — a random split lets the model memorise passengers who appear on many flight legs.
- Rule-generated labels excluded from training. Training a model on labels the rules produced is circular, and the resulting score would just be the rules wearing a hat.
- ~1% positive class handled honestly: keep every positive, sub-sample negatives, scale the positive weight. No SMOTE — synthetic passengers inflate metrics without improving real detection.
- Explainable output — score, band, which rules fired, the hard-override list, and the recommended officer action.
"The most valuable thing I did was retract my own result. An F1 of 0.98
looked great until I realised a random split was leaking the same
passenger across train and test — one person has many flight legs.
GroupKFold on passport number gave an honest number, and I can explain
exactly why the honest number is the useful one."
03
Enterprise IoT platform — broker to browser
50 commits · public
AIT
Real-time telemetry ingestion, device management and dashboard
visualisation — including the C++ firmware that publishes the telemetry and
the Kubernetes manifests that run what receives it.
EMQX · MQTTNext.jsTypeScript
PostgreSQL · PrismaMongoDBKubernetes
ESP32 · C++GitLab CI
Engineering detail
What I built
- Multi-tenancy enforced at the transport layer. Mountpoint isolation per device and group in the MQTT broker, so one tenant's devices physically cannot publish into another's topics — rather than filtering it out in application code afterwards.
- Device credentials verified through the broker's HTTP auth hook against an internal endpoint, using a three-part credential model.
- Polyglot persistence chosen per access pattern: PostgreSQL + Prisma for users, devices, groups and projects; MongoDB for time-series telemetry and dashboard configs. Two stores because the read patterns genuinely differ, not by default.
- Broker rule-engine forwarding into the application backend, with Nginx terminating TLS for both HTTPS app traffic and MQTT-over-WebSocket.
- Deployment both ways — Docker Compose for development, Kubernetes with Kustomize for the platform, behind a GitLab CI/CD pipeline.
"It's the full vertical — I wrote the C++ that publishes the telemetry and
the Kubernetes manifests that run the thing that receives it. The part I'd
defend hardest is mountpoint isolation at the broker: multi-tenancy
enforced at the transport layer, not by application filtering."
04
Aesthetic clinic ERP
In production
Client system · clinic group · ~14,400 LOC · 172 files · 17 collections
A complete bilingual clinic ERP: booking, OPD, sub-unit stock deduction,
course packages, finance and HR. The business logic is the hard part — this
is a system where the inventory and the money have to agree at the end of
every day.
Next.js 16React 19MongoDB · Mongoose
Redux Toolkiti18n TH/ENDocker
Engineering detail
What I built
- "Close case" as a single transactional trigger. One action fires five steps — FIFO stock deduction, bottle usage update, course visit count, commission generation, queue completion. Concentrating it in one service is what keeps the money and the inventory consistent.
- FIFO with real lot costing. Already-opened bottles are consumed before new ones, earlier-expiring lots first, and the cost recorded is the actual cost of the deducted lot — not a moving average that quietly launders margin.
- Double-booking prevented at the API layer, blocking both room and practitioner overlap even across different rooms, so a second client cannot bypass it.
- Book-before-registration. Bookings can exist before a patient has a hospital number; the system back-links it when the case is first opened. Reception should never have to invent data to make the software proceed.
- Multi-branch data model with per-branch catalogues, owner-sees-all / staff-see-their-own visibility, and patient records shared across the chain.
- Role-based access for six roles, enforced at the API rather than by hiding buttons.
- A 27-step smoke test walking the entire business cycle: instalment course sale → booking with overlap protection → registration → OPD → mandatory measurement → separately-billed add-on → case close → finance and permissions.
- Usability-driven iteration. A formal HCI evaluation scored it 58/100; I shipped a remediation round — async button states, toasts, route guards, Buddhist-era dates, global search, status legends.
"The decision I'm proudest of is making 'close case' the single trigger for
everything financial. Five things have to happen together — stock, bottles,
course counts, commissions, queue state. Spreading that across the UI is how
clinics end up with inventory that doesn't match the money."
05
HRIS with org-chart-routed approvals
Delivered
Client system · international school · ~7,000 LOC · 70 files
Client code — not published
Employee self-service for leave, entitlements, overtime and attendance, with a
supervisor approval workflow routed by the organisation chart rather than by a
hard-coded list of approver names — all behind single sign-on.
Next.js 16TypeScriptPostgreSQL 16
Prisma 6Auth.js v5 · OIDCTailwind v4
shadcn/uiDocker Compose
Engineering detail
What I built
- Built to the brief's three foundations in dependency order — organisation chart, then HR master data, then SSO. Approvals derive from the chart, so a reporting-line change reroutes every pending and future approval automatically.
- Pluggable OIDC SSO. Google Workspace is wired; Microsoft Entra or any generic OIDC provider drops into the same seam. First sign-in links the SSO identity to the employee record by email.
- A dev-login escape hatch that cannot ship. Gated behind an environment flag and forced off in production, so the app is demoable without an identity provider but cannot be deployed insecurely.
- Seeded role-based demo data so the client could evaluate the actual workflow without a setup session.
- One-command deployment. Compose brings up Postgres, applies migrations, seeds and serves, with host ports remapped to avoid collisions on the client's machine.
"Approvals route off the organisation chart, not a list of approver names.
When someone changes manager, HR changes one edge in the chart and every
pending and future approval follows — no config, no migration."
06
Service desk — department routing and rotation assignment
In production
Client system · international school · ~12,800 LOC · 97 files · 24 API routes
Staff report a problem, it reaches the right department, and a named person is
accountable for fixing it. Raise → triage → assign → accept → hold, transfer or
outsource → close → rate, with the workflow around that lifecycle editable by
the school's own administrator.
Next.js 14TypeScriptMongoDB
React QueryZodJWT + Google OIDC
PlaywrightCaddy
Engineering detail
Routing, and where configuration should stop
- Split out of the room-booking codebase as its own system, on the customer's instruction that an event and a job request are different things. Separate repo, container, database and release cycle; they share exactly one value — a signing secret — so someone signed in to one is recognised by the other. No workflow crosses between them.
- Tickets go to a department's central inbox, never fanned out to everyone in it. A dispatcher assigns from that one queue. Broadcasting to a whole team means nobody owns the job.
- Assignment by rotation, and it explains itself. Fewest open jobs → longest waiting → name, so an even queue is a plain A → B → C but it self-corrects for someone on leave or already holding four jobs. The rotation writes its reasoning into the ticket history, and a dispatcher can always override — which is what you want when a fault comes back and should return to whoever fixed it last time.
- "Accept" is a timestamp, not a new status. A status records where a ticket is, never when it got there, and a second name for one state adds a case to every site that switches on status. By that same test, "outsourced" genuinely is a status — and deliberately not a transfer, because a vendor has no queue and nobody who signs in, so the job stays with the technician who has to chase it.
- Two KPI clocks, reported as medians. Assigned→accepted and accepted→closed, per department, team and person. Medians because one laptop left at a repairer over the holidays makes a mean useless; every figure carries the count it was drawn from; per-person figures group by who accepted the job so a later reassignment can't credit the wrong name. It also surfaces the two things a ticket-derived report hides — jobs never accepted, and technicians with no jobs at all.
- Technicians on leave stay visible — greyed, labelled, still selectable, skipped by the rotation. A dispatcher who sees four names where there were five can't tell whether someone is away or the roster is wrong.
- The school owns its configuration; I own the state machine. Statuses and their transition table stay in code because the code reasons about them by name. Everything around it is theirs: SLA hours, which endings must be explained, what each status and priority is called, which events send mail, what a requester must fill in. Written up as a 32-item master-data audit where every "keep in code" line has to state a real reason, with additive, version-stamped config migrations so a school that has already tuned a setting keeps it.
- The bug class this project taught me to grep for: a constant that becomes configurable has to be read from the configuration everywhere it is enforced or displayed. A setting that only changes the settings page is worse than no setting, because the administrator believes it worked.
- Mail with an outbox. A dependency-free SMTP client records every notification before delivery is attempted, so while a department still has no mailbox its messages accumulate as "skipped" rather than vanishing.
- Tests grown from 3 to 85 over five weeks — and the suite went from 30 seconds to under one once I stopped the pure access-rule module from importing the config layer, which was opening a database connection just by being imported.
- A 34-case UAT driven through the real application with Playwright across five personas: 29 pass, 3 fail, 2 not exercisable without real elapsed time. Every screenshot a real screen, every "disabled" judgement read off the actual DOM rather than the picture.
"The line I had to defend was which parts of the workflow the customer owns.
They asked to edit the flow themselves, and the right answer was not 'here is
a drag-and-drop state machine' — the code reasons about statuses by name, so
an arbitrary graph would have given them a system that quietly stopped
notifying and reporting. The skeleton stays mine; what it's called, how fast
it must move, what must be explained and who hears about it are theirs."
Built twice
An earlier FastAPI + React version implemented the same planning notes as
one system combining tickets and room booking. The delivered system went
the other way — two systems, separate databases, sharing only a signing secret.
Two honest answers to "when do you split a system out, and what does sharing
a codebase actually cost?"
07
Team planner — Gantt + Kanban workspace
Public repo
~19,200 LOC
A full-stack rebuild of a single-file localStorage HTML planner into
a real multi-user product backed by PostgreSQL — the same Gantt and Kanban
workspace, but a whole team shares one board and nothing is lost when a browser
is cleared.
Next.js 16React 19Prisma 7
PostgreSQL 17Tailwind v4Docker
Engineering detail
Deployment decisions worth mentioning
- A three-service Compose with a dedicated migrate service that applies migrations, bootstraps admin accounts, then exits before the app starts — ordered startup, not a race.
- Bootstrap passwords from environment variables, never committed.
- Postgres bound to localhost on the production host, so the database is not internet-reachable.
- An rsync + ssh redeploy that deliberately excludes the server's own environment file — the most common way a deploy script destroys production config.
"Migrating a localStorage prototype to Postgres is the moment a demo becomes
software — you suddenly own identity, concurrency, migrations and backups."
08
Clinic income & expense dashboard
Live
Client system · ~8,300 LOC · 82 files · deployed on Vercel
Client code — not published
Replaces a clinic's summary spreadsheet with a real application: daily visit
entry with patient autocomplete, line items, split payments, expense entry with
daily cash closeout, monthly per-day × payment-method reports with CSV export,
and a KPI dashboard.
Next.js 16Drizzle ORMPostgreSQL
Auth.jsnext-intlRechartsVercel
Engineering detail
The migration path is the project
- A CSV importer for the old spreadsheet's daily tabs, with preview-then-confirm and idempotent re-import — re-importing a date replaces it rather than doubling it.
- Verified by an end-to-end script that runs the real sheet export through the production code path and asserts every aggregate matches the sheet's own totals: day totals, per-method sums, report rows and dashboard rows.
- That is how you earn a client's trust when you replace the system they have been reconciling by hand for years. The migration is not "done" when the data loads; it is done when their numbers and your numbers agree.
09
School website with a custom CMS
Delivered
Client system · ~17,600 LOC · 135 files · 30+ content sections
A full public school website — admissions, curriculum, athletics, news,
calendar, staff, tuition — with a bespoke database-backed CMS so staff edit
every page without a developer. Authenticated admin area, file uploads, Excel
export.
Next.js 16PrismaNextAuth
ZodexceljsDocker
Engineering detail
Worth mentioning as client-facing delivery at scale: 30+ routes and a bespoke
CMS data model, not a template with the colours changed. The interesting
constraint was designing a content model general enough for thirty
structurally different sections while still giving each editor a form that
looks like the page they are editing.
10
Customer retention decision support system
10 commits · public
Predicts subscriber churn, scores risk, quantifies revenue at risk and
simulates retention strategies — a full ETL → prepare → model → dashboard
pipeline, reproducible with one command.
Pythonscikit-learnXGBoost
PostgreSQLStreamlitDockerMake
Engineering detail
The credibility detail: it ships a leakage audit and
calibration alongside the model comparison — baselines plus
logistic regression, decision tree, random forest and XGBoost with
cross-validated metrics — and the README states plainly that the dataset is
synthetic and roughly balanced, so this is a methodology demonstration and not
a claim about any real company.
Stating your own limitations is what separates a data-science project from a
screenshot of a good score.
11
Tournament management — built twice, two architectures
Two repos
Live scoring · bracket generation · leaderboards · role-based admin
The same product implemented two ways: once with a separate FastAPI backend and
split containers, then collapsed into a unified full-stack Next.js application.
FastAPINext.jsFirebase Auth
FirestoreMermaid docs
Engineering detail
Google OAuth with Admin-SDK token verification, Google Sheets import, and
Firestore persistence. Both repositories document their architecture with
Mermaid diagrams — a component graph and a request sequence.
Why it's worth mentioning: it is a direct, concrete answer to "when would
you split a backend out versus keep it in the framework?" — not an
opinion, but two working implementations and the cost of each.
12
Event photo-booth queue system + staff dashboard
Public · two repos
Admins pre-register customers, generate one-time QR passes and drive a queue
(waiting → session → review → complete) from a dashboard; customers scan and
watch their live status.
Next.js 16React 19TypeScript
Tailwind 4googleapisVercel
Engineering detail
The detail worth stating: the Google service-account key is base64-encoded
into a server-side-only environment variable and never reaches
the browser. A one-time QR pass is only "one-time" if the thing that mints it
cannot be read by the person holding it.
13
Appointment booking — an authentication exercise
Public
Bank technical assignment
A compact full-stack application built deliberately around authentication and
authorisation: admins see all appointments, users see only their own — enforced
server-side.
FastAPISQLAlchemyPostgreSQL
JWTReactpytestDocker
Engineering detail
- Demo users seeded in code, idempotently, at startup — gated behind a flag that is forced off in production, where a single admin is bootstrapped from environment variables instead.
- Passwords bcrypt-hashed, never stored or logged in the clear.
- The pytest suite runs against a real PostgreSQL database, not SQLite — so the tests exercise the database production actually uses, including the constraint behaviour that differs between them.
14
VulnScan — port scanner & vulnerability assessment tool
Public · zero dependencies
A zero-dependency Python scanner that goes past open/closed: a 50+ port
vulnerability database, five risk levels, banner grabbing to identify service
versions, three to nine concrete remediation steps per finding, and JSON + text
reports.
Python 3stdlib only100 threads
Engineering detail
Multi-threaded at 100 concurrent connections, shipping a written assessment
report alongside real scan output. A finding without a remediation step is
just an alarm; the value of the tool is in the three-to-nine things you can
actually do about each one.
Paired with the ISC2 Certified in Cybersecurity credential. Intended and
documented for authorised testing on networks you own — the repository carries
a legal disclaimer.
15
FortuneClub — team full-stack project
67 commits · public
AIT coursework
A Django REST backend with a React + TypeScript frontend — chat, posts with
image upload, and separate Docker images for development and production.
Django RESTReact · ViteTypeScriptDocker
Engineering detail
The number to lead with is 67 commits on a team project — the clearest
evidence here of sustained, collaborative, iterative development, which is
usually what a reviewer is actually looking for when they open a repository.
16
Earlier work — embedded, research and coursework
Foundation
The work the rest is built on: a senior project that went end to end from
microcontroller to web dashboard, real embedded tooling, and a master's thesis.
What's in here
- Full-stack IoT dashboard (senior project, KMUTNB) — end to end from C on an ESP32, through a LAMP stack on a Raspberry Pi, to a SQL-backed dashboard. The first system I owned from sensor to screen.
- IoT design lab — ESP32 firmware in C++ built with PlatformIO, roughly 83 KB of C++: real embedded tooling rather than IDE sketches.
- Master's thesis research (in progress) — electronic-nose development for fragrance and body-odour evaluation in the cosmetics industry, written in LaTeX with a reproducible toolchain.
- Business-intelligence study hubs — around 50 hand-built, self-contained HTML pages published on GitHub Pages. Not software engineering, but evidence of technical writing, which matters more in solution and IT roles than people admit.