Portfolio Manifest — Prepared 2026-09-18

Terrence Daniels

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 and frontend both complete — the full stack runs end to end, verified live against a real Postgres database. This includes a chain of real, previously-invisible backend bugs found only by actually running it: three missing database tables for favoriting, following, and tagging, a Node-version incompatibility in the migration tooling, and a migration ordering bug — none caught by 228+ passing tests, since none of them exercise the real database path.

139phases shipped
228/228backend tests passing
12/12frontend route pages done
70real bugs found & fixed
0open CodeQL alerts
100%TypeScript, both sides

Why this project

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.

React 19 Vite + SWC TypeScript Express 5 Sequelize 6 PostgreSQL Vitest GitHub Actions CodeQL

One backend, soft auth by default

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.

Deliberate deviations, not defaults

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.

Real migrations, not sync-at-boot

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.

Real bugs, not just features

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.

01
models/User.js — wrong foreign key on Comments
The 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.
high · correctness
02
migrations — missing FK columns, masked by sync-at-boot
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.
high · schema integrity
03
helper/jwt.js — tokens that never expired
No expiry was ever set, and 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.
high · security
04
controllers/user.ts — always-true password guard
The password-hashing guard evaluated true on every profile update, crashing any update that didn't touch the password field at all — the single most severe bug found in the backend build.
high · correctness
05
controllers/users.ts — login issued tokens with username: undefined
The signed JWT payload never actually included the username it claimed to — every logged-in session silently carried a broken claim until this was caught and fixed.
high · correctness
06
middleware/authentication.js — double next()
A missing 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.
medium · correctness
07
middleware/errorHandler.js — raw error.message leaked to clients
The generic 500 handler forwarded the real error.message straight to API clients on any unexpected error — a real information-disclosure gap, masked behind a generic message instead.
medium · security
08
controllers/comments.ts — deleteComment skipped an ownership check
Unlike its sibling functions in the same file, 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.
medium · correctness
09
index.ts — catch-all 404 only matched GET
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.
medium · API consistency
10
CodeQL — 16× missing rate limiting, 1× insecure randomness
Every authorizing route handler across 5 route files lacked a rate limiter; the article seeder used 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.
medium · security
11
frontend/errorHandler.test.ts — a mock that passed for the wrong reason
The source's mock built 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.
medium · test correctness
12
frontend/getComments.ts — missing headers, wrong data on every fetch
The service never accepted a 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.
medium · correctness
13
frontend/AuthContext.tsx — a failed refresh could silently wipe the logged-in user
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.
medium · correctness
14
frontend/types.ts — a flat AuthState let isAuth and headers disagree
{ headers: {...} | null; isAuth: boolean } let TypeScript construct a state where isAuth was true but headers was null — checking one never narrowed the other, so every auth-gated component needed its own defensive null check. Redefined as a discriminated union; the repeated login guard itself was then extracted to a plain helper, not a hook, since a hook would have to alert on every render for a logged-out visitor instead of only on click.
medium · type safety

56 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, four instances of the same trailing-space className bug across four different components, an <a> nested inside a <button> in ArticleAuthorButtons.tsx, a missing return that let an unauthenticated comment-delete request through with headers: undefined, a stale-closure bug in a list's optimistic favorite-toggle update, a Link passed an entirely wrong-shaped state object, three separate components that navigated a user home on a silently-failed request as if it had succeeded, three missing Sequelize join-table migrations that left favoriting, following, and tagging silently broken against any real deployed database, a react-paginate CJS/UMD interop crash the moment the home page had real articles to paginate, an unparsed pagination query value that could smuggle NaN into a live Sequelize query, and more — are logged in full in todo.md's phase-by-phase record, not summarized away here.

Verifying it actually works

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.

228/228 backend, real SQLite behind it

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.

Checkable, not asserted

Every number on this page is reproducible from the repository's own todo.md phase log, commit history, and live CI runs.

Judgment calls

Where the interesting decisions actually happened

The frontend was 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, then all 32 components, sequenced the same way — a full dependency graph across the source's 31 components was mapped up front so zero-dependency leaves got built before anything that composes them. Two real architectural choices 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; and, once a second component needed the same "reject an unauthenticated action" guard, a flat AuthState interface that let isAuth and headers disagree got redefined as a discriminated union, with the duplicated guard itself extracted to a plain helper rather than a custom hook — a hook would have to fire its alert on every render for a logged-out visitor instead of only on click. That same helper-not-a-hook reasoning held up through the whole rest of the components layer: when the identical "resolve or fail" auth-callback logic showed up a second time in the very last two components (LoginForm/SignUpForm), it was extracted the same way rather than left duplicated, precisely because no third occurrence was ever coming. A third early call: a component's fallback-prop logic was briefly changed from the source's || to ??, reasoning that || could silently overwrite a deliberately empty string — technically sound, but reverted once a check against the source's own real future callers showed nothing in this app ever passes one; the deviation wasn't earned by an actual need, so it didn't ship. That same discipline caught a subtler bug near the end of the build: a simplification to AuthorInfo's data-fetching effect looked like a clean win until a review checked it against how a sibling component actually navigates between profiles and found it would leave stale data on screen under a new username — reverted to a more defensive version before it ever shipped, the same "verify against real callers, not just plausible reasoning" standard applied throughout. A process gap surfaced twice earlier in the build — 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 held for the rest of the components layer — until it lapsed a third time during the route-pages and local-dev-setup work that followed, this time across 12 consecutive commits during a long debugging stretch, caught by re-reading the session's own commit history rather than assuming every commit had been tracked, and backfilled the same way as the first two lapses. Same standard, same discipline of fixing it in the open rather than quietly moving on.

The route pages layer surfaced its own real finds: Article.tsx needed an empty-sentinel pattern for its article state, the third use of the same technique first established for auth and settings forms; main.tsx's router choice (hash-based, not path-based) was verified rather than assumed, by actually reading how the Express backend serves the built frontend and confirming it has no server-side fallback route — the "wrong" router choice would have looked identical in development and only broken on a production deep link; and the reference's dead reportWebVitals boilerplate was deliberately not ported after confirming, not assuming, that it was both inert as invoked and incompatible with the pinned dependency version. Getting the whole stack running for the first time was its own exercise in verifying rather than guessing: a react-paginate crash was root-caused by reading the package's actual minified build output, not just its type declarations; three missing database tables were confirmed by inspecting real Sequelize association metadata at runtime, not by trusting the model source alone; and the new Docker Postgres setup deliberately defaults to the standard port for anyone else cloning the repo, rather than hardcoding this machine's own port conflict into a committed file. The still-open gap: 62 of the 69 files tracked by issue #71 have no behavioral tests yet (7 done so far), unlike the backend where every file has one — disclosed and tracked on the board rather than quietly deferred.

A DRY/SOLID audit run across the whole codebase (2026-09-07) found 12 backend and 8 frontend real findings — not just duplicated code, but genuine SRP, OCP, and DIP violations too: a shared requireAuth middleware replacing 10 reimplementations of the same auth check, appendFollowers split apart once it turned out to be two functions wearing one name, an authenticated user's email removed from a mutable req.headers side channel, and a tangled ArticleEditorForm.tsx pulled apart into a dedicated hook. All 20 findings were fixed. A second, narrower audit targeting test files specifically (2026-09-18) found 9 backend and 2 frontend findings: all 9 backend findings and 1 of 2 frontend findings were fixed, and the second frontend finding was reviewed and deliberately rejected as not a real violation at this codebase's size.