Portfolio Manifest — Prepared 2026-08-20
Full-Stack Engineer
An independently modernized fork of the RealWorld Conduit example app — a Medium-style publishing platform (CRUD, JWT auth, pagination) rebuilt one file at a time rather than copied over wholesale, with every dependency brought up to its current latest and every real bug found via a real failing test, not read twice and assumed fine. Backend complete and independently runnable; frontend in progress.
A dated example app, modernized for real
RealWorld's Conduit is a well-known "same app, N stacks" demo — a useful common ground, but the reference implementations age: pinned dependencies, an unmaintained CommonJS backend, no TypeScript, no CI, no tests. Rather than fork it wholesale, this repo rebuilds it file by file, upgrading every dependency and pattern to current latest as each file goes in, with the reasoning for every real decision written down as it happened — not reconstructed afterward.
Five route groups, four models, one middleware that never rejects a request for missing auth
verifyToken runs in front of nearly every route, but it's not
a gate: no Authorization header just means the request
proceeds anonymously — each controller decides for itself whether that's
acceptable. Real migrations are the only schema source of truth; nothing
patches the live schema at boot anymore.
bcrypt cost factor bumped 10→12 (current OWASP guidance). JWTs now
expire after 7 days and verification is pinned to
algorithms: ["HS256"] explicitly — the source issued
tokens that never expired. A global rate limiter now sits in front of
every /api route, closing 16 real CodeQL alerts at once
instead of patching each router individually.
The source relied on sequelize.sync({ alter: true })
at boot, which silently patched the live schema — exactly how two
missing foreign-key columns went unnoticed there. This repo's
migrations are authored from scratch and are the only thing that
creates schema now. See the
data model.
Found by a real failing test or a real CI run, not by reading the source twice
Every entry below was caught by something that actually ran — a test written to fail on the bug first, a CI job, or CodeQL — and every fix was re-verified by reverting it and watching the test fail again before being called done.
Comments association was copy-pasted from
Article.js and never corrected to the right key. Caught
and proven by a test written to fail on the original bug and pass on
the fix.
create-article/create-comment never
created the userId/articleId columns their
models' associations need — only worked in the source because
alter: true patched the live schema at boot. Every
migration now cross-checks its column shape against the real model
in its own test.
jwt.verify() accepted any
algorithm the token claimed. Fixed to a 7-day expiry and
algorithms: ["HS256"] pinned explicitly — both proven
by deliberately reverting each and watching the test catch the
regression.
return let request handling fall through to a
second next() call after an error response had already
been sent. Fixed and proven by reverting the fix and watching the
regression reappear.
error.message straight to API clients on any unexpected
error — a real information-disclosure gap, masked behind a generic
message instead.
deleteComment never checked that the comment being
deleted actually belonged to the article named in the URL slug.
Fixed to match the file's own established pattern.
app.get("/*any", ...) meant an unmatched POST/PUT/
DELETE fell through to Express's default HTML 404 instead of this
API's own JSON shape. Fixed to app.all(...), verified
by reverting and watching a POST come back with an empty body first.
Math.random()
to pick a random author. Fixed with one global
express-rate-limit instance ahead of every route mount,
and crypto.randomInt() in the seeder.
error.response from the Fetch
API's Response class, which has no
.data field — its assertion passed because of an
unrelated crash, not the code path it claimed to test. Fixed with a
real axios-shaped mock; proven by deliberately breaking the
extraction and watching all 5 cases fail first.
headers param at all,
unlike its siblings, even though the backend's GET-comments route
changes response shape based on whether a token is sent — comments
were always fetched as if the user were logged out.
getUser({ headers }).then((loggedUser) =>
setAuthState(...))
had no guard against an undefined resolution —
errorHandler can swallow certain errors and resolve
nothing, which would have overwritten a valid logged-in user with
undefined. TypeScript's
User | undefined return type on
getUser forced the fix.
14 more, smaller findings — a seeder that bypassed
bcryptHash() entirely, a missing
tagList default, a missing await on
setAuthor, two message-formatting bugs, a
FeedContext.tsx click handler reading
e.target instead of e.currentTarget, and more —
are logged in full in
todo.md's phase-by-phase record, not summarized away here.
Live CI, not a static screenshot
Every push runs the full suite — ESLint, Prettier, TypeScript, and Vitest — plus a separate CodeQL security scan. The badges at the top of the repository's README are live, not decorative.
Controller and route tests run against a genuine in-memory SQLite Sequelize instance running the real models — not hand-stubbed mocks. See the testing strategy.
Every number on this page is reproducible from the repository's own todo.md phase log, commit history, and live CI runs.
Where the interesting decisions actually happened
The frontend is being built bottom-up, the same way the backend was — leaf
modules (helpers, shared types, API services) before anything that depends
on them. That layer is done: all 16 service modules, then
AuthContext/FeedContext, both memoized only
after a review caught their Provider values causing unnecessary re-renders
on every consumer. The components layer is next, sequenced the same way —
a full dependency graph across the source's ~24 components was mapped up
front so zero-dependency leaves get built before anything that composes
them. One real architectural choice got decided rather than left open:
TanStack Query versus the source's hand-rolled fetch/loading/error
pattern, settled in favor of the latter after checking that the reference
app itself has no query-library dependency at all. A process gap surfaced
twice — the backend's convention of a real GitHub Issue and board card per
file quietly lapsed for a handful of frontend files, not once but twice —
caught both times from a stale sub-issue count, backfilled both times, and
now holding. A newer, still-open gap: the 20 frontend files built so far
have no behavioral tests, unlike the backend where every file has one —
disclosed and tracked on the board rather than quietly deferred.