Engineering, in the open
A portfolio tells you what someone has built. This page shows you how I build — using this site itself as the example. The repository is public: the architecture docs, the decision records, and the release history are all inspectable, not just claimed.
Everything below is kept honest by the repo. If the docs say releases are tagged from release branches, the git history shows the tags and the merges.
The stack, and why
Next.js 16 (App Router)
Every page here is static — so the framework is used for what it's good at: file-based routing, build-time rendering, and zero-config code splitting. No server runtime to patch, scale, or wake up.
TypeScript
The content layer is typed (Experience, Project, …), so a malformed entry fails the build instead of rendering broken UI. Types are the contract between content and components.
Tailwind CSS 4
Design tokens (colors, animation curves) live in one @theme block in CSS — v4's config-less setup. Utilities keep styles co-located with markup, and only the classes actually used are generated at build time.
Framer Motion + raw Canvas
Framer Motion for declarative scroll reveals; a hand-rolled canvas/rAF loop for the hero's market tape, where per-frame control matters. Both respect prefers-reduced-motion.
Self-hosted fonts
Space Grotesk, JetBrains Mono, and the signature script ship from the same origin via @fontsource. No Google Fonts request: no third-party dependency at build or runtime, no layout-shifting late font swap.
Vercel
The static output deploys to a global CDN. main is the production branch; a deploy is a git operation, and rolling back is redeploying the previous immutable build.
Decision records
All copy lives in one typed data file
- Decision
- Every string on this site comes from src/data/content.ts. Components are purely presentational.
- Why
- Editing content never risks breaking layout, and reskinning never risks losing copy. It's a CMS's separation-of-concerns without a CMS's moving parts.
- Trade-off
- No non-technical editing UI — acceptable when the only editor is the engineer.
Fully static over server rendering
- Decision
- No API routes, no server components doing runtime work, no database. The build emits plain HTML/CSS/JS.
- Why
- A portfolio's content changes at commit time, not request time. Static means CDN-cacheable everywhere, no cold starts, and a near-zero attack surface.
- Trade-off
- Any future dynamic feature (contact form, view counts) needs an external service — a deliberate boundary, decided when needed.
Canvas for the hero, not a video or GIF
- Decision
- The market-data backdrop is ~200 lines of canvas code, not a looping video asset.
- Why
- It's resolution-independent, weighs kilobytes instead of megabytes, pauses itself off-screen via IntersectionObserver, and renders a single static frame under prefers-reduced-motion.
- Trade-off
- More code to own than dropping in an mp4 — but the code is the portfolio.
Release branching, even solo
- Decision
- main is production-only, develop integrates, features branch, releases are cut on release/x.y.z branches and tagged.
- Why
- Process that only exists when a team enforces it isn't process. Running the full flow solo keeps the discipline honest and makes the git history itself a demonstration.
- Trade-off
- More ceremony than trunk-based — the right call here because the history is a deliverable. On a high-velocity team I'd bias to trunk-based with feature flags.
Release engineering
This repo follows a git-flow-style release branching strategy. Nothing lands on main except a release or a hotfix, every production state is a tag, and the merge history is the audit trail.
Branches
mainProduction. Every commit is deployable; every release is an annotated tag (v1.0.0, v1.1.0, …). Vercel deploys from here.developIntegration. Completed features accumulate here until a release is cut.feature/*One scoped change each, branched from develop, merged back with --no-ff so the branch stays visible in history.release/x.y.zRelease prep: version bump, final checks. Merges to main (tagged) and back to develop.hotfix/x.y.zEmergency path: branched from main, fixed, tagged, merged to both main and develop.
A change ships like this
- 01Branch feature/<name> from develop; build and verify (npm run build must pass).
- 02Merge the feature into develop with --no-ff.
- 03Cut release/x.y.z from develop; bump the version; final verification.
- 04Merge to main with --no-ff; tag vx.y.z; deploy.
- 05Merge the release back into develop so nothing drifts.
System design notes
Serving model
Build-time rendering to immutable static assets, pushed to a CDN's edge. There is no origin server in the request path — the 'backend' is the build step. Practical consequences: global p50 latency is CDN latency, availability is the CDN's, and traffic spikes are someone else's capacity-planning problem.
Performance budget
First Load JS is held around ~135 kB. The only heavy dependency is Framer Motion; the hero animation is raw canvas specifically to avoid importing a charting library for decoration. Fonts are subset, self-hosted, and preloaded by the framework.
Security posture
No runtime inputs, no database, no auth, no cookies — the exploitable surface is essentially the framework's static serving path and the supply chain. So the real security work is dependency hygiene: pinned versions, npm audit triage against the actual threat model (a static site doesn't have Server Actions to attack), and patch releases taken promptly.
Failure modes & rollback
A bad deploy can't corrupt state because there is none. Rollback is re-pointing to the previous immutable build — or reverting the merge commit on main, which the branching strategy keeps atomic. The scheduled-pipeline and streaming systems I build for trading have much harder failure semantics; this site is deliberately at the boring end of that spectrum.
The full write-ups — architecture, decision records (ADRs), branching strategy, and system-design notes — live in the repo.