Decisions, not technology names
A list of technologies says what was used. A decision record says what the problem was, what else was considered, why those were rejected, and what was accepted as a consequence — including the parts that hurt.
6 decisions
- typed-tool-registryportfolio-mcp · 2026-09
A typed tool registry between any model and the data
ProblemAn MCP client — someone else's agent, running on someone else's machine — needs to read portfolio content. The naive shapes, giving it a query interface or handing over the whole dataset, both make the caller responsible for correctness of access.
Constraints- The model must never be able to widen its own read scope.
- The same capabilities must serve MCP clients and any future transport.
- Adding a capability should be one file change, not a change per transport.
Considered and rejectedWhole dataset in the system prompt
Works at this size and fails at any other. It also removes every observation point: no per-capability latency, no tool-selection metric, no authorization seam.
A generic query or SQL tool
Maximum expressiveness for the model is maximum blast radius. It moves domain logic into generated queries, which is exactly where it cannot be tested.
Framework-managed tools (AI SDK primitives)
Couples the tool contract to one client library, when the contract has to be served over MCP to clients I do not control.
DecisionOne registry of named tools. Each declares a description, a Zod input schema, an access level and a handler. Transports adapt to the registry; the registry knows nothing about transports.
Reasoning- A Zod schema per tool turns model output into a parse step with a defined failure mode.
- An access level per tool makes authorization a property of the capability rather than a rule in a prompt.
- Transport independence is what makes the MCP endpoint a thin adapter rather than a second implementation.
- Named, described tools are what a calling model actually selects between — the description is part of the contract.
What this bought- The model orchestrates capabilities and never formulates a read.
- Each call is independently measurable — latency, outcome and argument validity.
- A new capability is one registry entry, available to every transport at once.
What it cost- The model can only answer what a tool exposes; unanticipated questions need a new tool.
- More ceremony than passing a JSON blob to the prompt — the cost is paid up front.
- mcp-handrolled-jsonrpcportfolio-mcp · 2026-09
Hand-roll the MCP JSON-RPC subset instead of adding the SDK
ProblemExposing the portfolio over MCP needs initialize, tools/list and tools/call. The MCP SDK targets stdio and long-lived sessions; this endpoint is a stateless route handler on a static-first deployment.
Constraints- The route must stay stateless and edge-friendly.
- tools/list must be generated from the same Zod schemas the tools already declare.
Considered and rejectedThe official MCP SDK
Session and transport machinery this deployment does not have, to implement three methods whose wire format is fully specified.
No MCP endpoint at all
Removes the part of the claim that is actually checkable by a reader.
DecisionImplement the JSON-RPC 2.0 subset directly in a route handler, generating JSON Schema for tools/list from the registry's Zod schemas.
Reasoning- Three methods of a published spec is less code than the adapter that would wrap the SDK.
- One registry stays the single source of truth, and JSON Schema is generated from the Zod schemas that actually run.
- Stateless request/response matches how the site is deployed.
What this bought- A real, connectable endpoint with no new runtime dependency.
- Schema drift between validation and the published contract is structurally impossible.
What it cost- Only the implemented subset is supported — no resources, prompts, or notifications.
- Spec changes are mine to track.
- server-components-defaultportfolio-mcp · 2026-09
Server Components by default, client islands by exception
ProblemA portfolio arguing for engineering quality has to be fast, and the usual failure is shipping an interactive framework to render static prose.
Constraints- Content pages must be prerendered and require no client JavaScript to read.
- Only one surface genuinely needs interactivity: the theme toggle.
Considered and rejectedClient-side app shell
Pays hydration cost on every page to serve interactivity that one component needs.
DecisionEvery route is a Server Component and statically prerendered. Interactivity is isolated to a named client island; the MCP route handler is the only dynamic endpoint.
Reasoning- Reading the site should cost a document, not a bundle.
- Isolating client code makes the interactive surface auditable at a glance.
What this bought- Static HTML for all content routes; JavaScript is scoped to real interaction.
What it cost- Interactive state cannot be lifted casually — crossing the boundary is a deliberate act.
- static-data-modulesportfolio-mcp · 2026-09
Typed modules as the content store, not a database
ProblemPortfolio content needs a home that both the pages and the MCP tools read from.
Constraints- Content changes at the speed of a git commit.
- Invalid content should fail loudly and early.
Considered and rejectedA CMS or database
Operational surface, latency and a runtime dependency for data that changes a few times a year and is already reviewed in pull requests.
Markdown with frontmatter
Weaker typing at exactly the boundary a caller reads across; the structure is relational, not prose.
DecisionTypeScript modules parsed through Zod at module load, read by a thin query layer.
Reasoning- Content errors become build failures.
- The whole site prerenders, so reads cost nothing at runtime.
- The query layer is the seam to move behind a database later, if that day comes.
What this bought- Type safety end to end; zero runtime data dependency; trivially cacheable.
What it cost- Editing content requires a deploy, and there is no non-technical editing path.
- css-variable-themingportfolio-mcp · 2026-09
Theme with CSS custom properties, not dark: variants
ProblemThe site needs light and dark themes that respect the system setting and an explicit toggle, without a flash of the wrong theme.
Constraints- The toggle must beat the system preference in both directions.
- No colour may be defined only inside a media query.
Considered and rejecteddark: utility variants throughout
Doubles every colour decision at each call site and scatters the palette across the codebase.
DecisionOne token set on :root, redefined under prefers-color-scheme for untouched visitors and under [data-theme] for an explicit choice. Components reference semantic tokens only.
Reasoning- The palette lives in one file and components never name a colour.
- A tiny inline script applies the stored choice before paint, so there is no flash.
What this bought- Single source of truth for colour; both themes stay correct by construction.
What it cost- Per-component theme overrides are deliberately awkward.
- zustand-client-stateathlio · 2025
Zustand for interactive client state
ProblemGlobal client state was required for an interactive training UI — state shared across routes and components, distinct from server data.
Constraints- Server state is already owned by the data layer; this is application state only.
- TypeScript ergonomics matter more than ecosystem size at this scale.
Considered and rejectedRedux Toolkit
Ceremony out of proportion to the amount of state involved.
React Context
Re-render behaviour degrades as the shared state grows.
Jotai
Atomic model is a good fit, but the store model matched how this state is used.
DecisionZustand.
Reasoning- Low ceremony and a small API surface.
- Strong TypeScript ergonomics without generics gymnastics.
- Appropriate for application state, with server state kept out of the store.
What this bought- Very little boilerplate; selectors keep re-renders contained.
What it cost- Fewer guardrails than Redux — store discipline has to be maintained by convention.
Division of work
Agents write a large share of the code. That changes what the engineering job is, not whether there is one.
- Product
- Architecture
- System design
- Trade-offs
- Security
- Code review
- Final decisions
- Implementation
- Test generation
- Refactoring
- Documentation
- Research
- Codebase exploration
- Debugging
My role isn't to write every line of code. It's to design the system that makes the right code get written.
Banded by how they are used
Core means daily production use. Production means real work, not every day. Learning means no production experience yet — listed as learning rather than claimed.
Frontend
Where most of five years has been spent: component architecture, rendering strategy and the parts of the browser that decide whether an app feels fast.
- Reactdaily
- Next.js (App Router)daily
- TypeScriptdaily
- Tailwind CSSdaily
- shadcn/uidaily
- GraphQL / Apollo Clientproduction
- Accessibility (WCAG)production
State & data
Choosing the right kind of state for the problem, and keeping server data out of client stores.
- Zustanddaily
- React Queryproduction
- Reduxproduction
- React Contextdaily
Backend & data layer
APIs, persistence and the boundaries between them.
- Node.jsdaily
- Expressproduction
- PostgreSQLproduction
- Supabaseproduction
- MongoDBproduction
- REST API designdaily
AI & agents
Tool design, orchestration and the boundary work that makes model-driven systems safe to run.
- LLM APIs (Anthropic)production
- Tool / function callingproduction
- Model Context Protocolproduction
- Agent skillsproduction
- Structured outputs (Zod)daily
- Agent evaluationproduction
- AI SDKproduction
Quality & delivery
Testing, review and the pipeline that carries work to production.
- Vitest / Jestproduction
- ESLint / Prettierdaily
- Git / GitHub Actionsdaily
- Verceldaily
- Dockerproduction
- Code reviewdaily
Microsoft 365
Enterprise platform work from the Avanade and Enzyme years — the context where most 'internal tooling' actually lives.
- SPFxproduction
- SharePoint Online / On-Premproduction
- PnP JSproduction
- SAPUI5production
Learning now
Honest about what is in progress rather than claimed.
- React Native (Expo)learning
- Stripe Connectlearning
Real commits, read at build time
Git history from this repository. Entries marked agent-assisted carry a co-author trailer written by the tooling at commit time — evidence, not a claim.
- 2026-09-25feat: update CV and add new skills documentation90b27c0
- 2025-12-01fix: Update CV files with improved formatting and recent experience details122 files changed, 11005 insertions(+), 2455 deletions(-) 6db53e4
- 2025-11-03feat: Add Vercel Analytics integration to layout and update dependencies3 files changed, 0 insertions(+), 0 deletions(-) c6c9ab2
- 2025-11-03fix: Reorder FinWallet entry in SIDE_PROJECTS for consistency3 files changed, 40 insertions(+), 3 deletions(-) 70d9807
- 2025-11-03fix: Correct date for Athlio Training App in WORKS section1 file changed, 6 insertions(+), 6 deletions(-) 034b69a
- 2025-11-03feat: Update header to include links and improve navigation1 file changed, 1 insertion(+), 1 deletion(-) 4b4fbce
- 2025-05-26Scaffolding curriculum pages19 files changed, 180 insertions(+), 40 deletions(-) c4da74e
- 2025-05-26mobile view5 files changed, 67 insertions(+) de8822b
- 2025-05-26Adjustments and languages added1 file changed, 2 insertions(+), 2 deletions(-) 426a2b5
- 2024-12-18last updates16 files changed, 151 insertions(+), 44 deletions(-) 0c9f67c
Source: git log, read at build time. Full history on GitHub