Available for Projects

Hi, I'm
CuongHoang

Full-Stack Developer + AI Builder

I build modern web apps with Next.js, Node/Express and TypeScript on PostgreSQL, with AI features that actually ship — interview grading, CV critique, language tutoring. This whole platform is something I built and run myself.

MVP2-4 wks

Fast Delivery

L
R
N

AI-Powered

DB tables277
Migrations117

High Quality

Web pages:206|
API routers:71|
DB tables:277|
Commits:2,847|
Web pages:206|
API routers:71|
DB tables:277|
Commits:2,847|
206

Pages on the site

2,847

Commits

About Me

CuongHoang is a full-stack developer who would rather build the thing than go looking for it. This entire platform — the data schema, the API routers, the zero-downtime deploy pipeline — is written and operated by him on his own VPS.

Outside of coding, he writes up the production incidents he has hit and how each was fixed — the part nobody can copy, and the part he learns most from.

JavaScriptTypeScriptReactNext.jsJavaSpring BootPostgreSQLRedisDocker
CuongHoang
Counted from the repository

Size of the codebase

Not one of these is typed by hand — a script recounts the whole repository before every deploy.

2,847
Commits
80
Days building
206
Web pages
71
API routers
277
Database tables
117
Schema migrations
1,514
Source files
455,960
Lines of code

Refreshed on every deploy · 27/08/2026

AI Knowledge Stream

Latest Articles

Sharing programming knowledge and experience

Blog

Latest articles

The most recent articles published on the site.

Skills & About

Technologies and skills I use to build products

Java95%
Spring Boot90%
Node.JS72%
JavaScript85%
TypeScript80%
React85%
Next.js80%
Golang60%
Mobile App68%
VPS90%
Linux88%
Docker80%
PostgreSQL85%
Redis75%
AWS65%
Full stack

Everything running on this site

No percentages. Every entry below can be checked against package.json or the repository tree.

Backend

10
Node.jsExpressTypeScriptPrismaZodJWT + bcryptHelmetexpress-rate-limitMulternode-cron

Frontend

11
Next.js (App Router)ReactTypeScriptTailwind CSSFramer MotionZustandTanStack QueryTipTapMonaco EditorRechartsMermaid

Data

5
PostgreSQLPrisma ORMRedis (ioredis)pgvectorVersioned migrations

Infrastructure

6
DockerNginxSelf-managed VPSGitHub ActionsCloudflare R2 (S3 API)Sentry

AI

6
LLM adapter (Anthropic / OpenAI-compatible)RAG + pgvectorTransformers.js embeddingsPrompt-injection screeningPII gateProvider circuit breaker

Real-time

3
Socket.IORaw WebSocket (/device-ws)Redis adapter

Media & documents

7
SharpFFmpegpdfkitdocxmammothunpdfmusic-metadata

Other

6
Three.js / WebGLESP32-S3 firmwareVNPayResendJavaSpring Boot
Incident log

8 times I broke production, and how each got fixed

Anyone can list technologies. The harder thing to write down is where you got it wrong.

Symptom

The GIF picker died and chats “disappeared” — re-logging in did nothing.

Cause

A deploy that synced code but did NOT rebuild: the container kept serving an old build that never mounted the GIF route. The chats were never lost — a per-viewer delete flag filtered them out.

Fix

Diagnose routes with an unauthenticated `curl`: 401 means mounted, 404 means stale build. Deploys now always rebuild and smoke-test the core routes.

Symptom

Turning on the site-wide dark theme broke the Notes module’s own three-theme switcher.

Cause

The `dark` class landed on `<html>`, force-activating every Tailwind `dark:` variant nested inside — including areas with their own palette.

Fix

The global dark class got its own name, `dark:` is reserved for the Notes wrapper, and everything else uses theme CSS variables.

Symptom

Sessions died silently after exactly 24 hours. The cookie was still there, but every authenticated call was rejected.

Cause

The token expired in 24h while the cookie lived 7 days — and the refresh path called an endpoint that did not exist.

Fix

A real refresh endpoint plus an interceptor that refreshes once and retries. Sessions self-heal; no config change needed.

Symptom

The 3D playground hung on its loading screen across two sessions, with no error anywhere.

Cause

Next.js fixes its static-file list at server start. Rebuilding renamed the JS bundles by content hash, so the running server 404’d files that existed on disk — no JS ran, so nothing surfaced an error.

Fix

Changing static files means restarting the server, and killing it BY PORT (Node renames its process, so killing by name misses). Production was never affected — each deploy is a fresh container.

Symptom

“The database is down, only the frontend works.” Yet Postgres and the backend both reported healthy, and other users were fine.

Cause

Not an outage — a per-IP rate limit. One exam page fired 18,297 requests in 10 minutes (~32/s), re-uploading the whole audio recording each time. The root cause was a self-feeding loop: on timeout an effect auto-submits; when the submit fails the catch block MUST clear the “submitted” flag so the manual button still works; once cleared, the effect sees the same condition again and re-submits — as fast as the network allows. The latch released itself at exactly the condition that had tripped it.

Fix

A separate “already attempted” flag that is never cleared, split from the manual path’s flag. The rule: any effect that acts on a condition which does NOT clear itself (a timeout, a network error, a status flag) needs its own attempt latch. Trace it by scanning the Redis rate-limit keys to see whether one IP or the whole bucket is drained, then counting endpoints in that IP’s nginx log lines.

Symptom

Rate limiting looked like it was working, yet a deliberate client could call the API without limit.

Cause

The key generator read the FIRST entry of the X-Forwarded-For header. That entry is client-supplied, so changing it on each call yields a fresh counter bucket every time — proven with curl. The site also does not sit behind Cloudflare, so trusting CF-Connecting-IP would be worse still: anyone can set that header.

Fix

Switch to the LAST X-Forwarded-For entry — the one nginx appends itself, which the client cannot touch — for both the general and the auth limiter. Regression check: curl with a forged header must still land in the SAME bucket after the fix; a fresh bucket means the hole is back.

Symptom

A deploy failed with “no space left on device”, and every database-backed endpoint returned 500 — while the home page still returned 200, so at a glance the site looked fine.

Cause

The disk hit 100%. Postgres could not write its write-ahead log and got stuck in recovery: “database system is not yet accepting connections”. The space was not where you would look either — this host uses the containerd snapshotter, so the real images live under /var/lib/containerd while /var/lib/docker looks deceptively small.

Fix

Reclaim through docker itself (never hand-delete inside containerd, and never pass --volumes — that is the Postgres data), then restart Postgres and it finishes recovery in under a minute. Long term: prune the build cache after every deploy, plus a weekly job that reclaims disk. The diagnostic lesson: a static page returning 200 proves nothing — call an endpoint that actually touches the database.

Symptom

The deploy stopped at P3009: “migration failed to apply”. No migration could run any more, not even a genuinely new and harmless one.

Cause

The database had drifted from the migration history: six older migrations had their DDL applied to production but were NOT recorded in the history table — some rows left half-finished, some never inserted at all. Prisma saw a broken history and blocked everything, exactly as it should.

Fix

For EACH migration, confirm in psql that the tables and columns it creates really exist, and only then mark it applied; after that the genuinely new migration runs. Wrongly marking a truly-missing migration skips the DDL you needed and corrupts data silently — which is why the rule is now: a failed migration stops and reports, never auto-resolves, and never gets patched with tricks that force it through.

What I Do

Services services.provide

Comprehensive technology solutions to turn your ideas into real products

Web Development — SPEC_SHEET.md
LIVE

Web Development

End-to-end web applications with modern frameworks

TTFB

< 80ms

LCP

< 1.8s

CLS

< 0.05

Uptime

99.97%

API_SPEED
80ms avg
LATENCY
< 50ms p99
DATABASE
PostgreSQL 16
GUARDRAIL
ENABLED

Tech Stack

Next.js 15TypeScriptTailwind CSSPostgreSQLRedisDocker

Guardrail Integrations

SSR + ISREdge CachingImage OptimizationFont Subsetting

Get in touch

Open to work, collaboration, or just a question about something on this site.