01 /

How this site works

the portfolio as a project

Static pages, three small server functions, one notification service and one language model. Everything measurable on it is measured, and the measurements are on the page.

Framework
Astro 7, static output, React islands only where there is state (the palette, the contact trace, the ledger). Deployed on Vercel; the API routes run as serverless functions.
Content
MDX case studies with a Zod schema. A project marked measured must carry at least one metric with a source; the build fails otherwise. Diagrams are data (nodes and edges) rendered to SVG.
Type and colour
Satoshi, Newsreader for display, Geist Mono for numbers and labels, all self-hosted. Neutral paper, one accent for links and measured marks, green only for live status. Light and dark from the same tokens.
Avatar
The voxel figure behind the photo is 2,051 cubes described in code and drawn by a software renderer under 3 KB gzipped on a 2D canvas: surface faces only, greedy-merged to 211 quads, painter's sort, three-tone shading. No WebGL, no model file, loaded after the page is idle and only on pointer devices.
Findability
Person, WebSite and ProfilePage JSON-LD on every page, CreativeWork per case study, a sitemap, llms.txt, and a robots file that allows AI crawlers.
02 /

Ask this site

retrieval, then an answer, then a receipt

Press ⌘K and type a question. The palette is a small retrieval-augmented pipeline over this site's own content. It answers only from what it retrieved and refuses when nothing qualifies.

Try asking
embedkept chunksCase studiesMDX, profile, changelog188 chunksper section, overlapEmbeddingsbge-m3 on Workers AI,1024dQuestionfrom the paletteCosine top 8threshold 0.42Answer + citationsgpt-oss-120b on Groq
Build time on the left, request time on the right. The index is a JSON file; nothing about the reader is stored.
56/56questions passed
100%cited correctly
100%off-topic refused
1.8 smedian answer time

Evaluated on 56 questions on 15 Sept 2026: expected citations, expected refusals and expected facts, scored by scripts/run-evals.ts. The threshold came from a calibration run on the current embedding model: on-topic questions score at or above 0.44, off-topic ones at or below 0.37. The palette footer repeats this line so the claim travels with the feature. The full benchmark, per-question results, retrieval metrics, adversarial cases and run history are on the evaluation lab.

03 /

Contact and the ledger

the second tenant of Notify

The contact form does not use a mail provider directly. It posts an event to Notify, the notification service I built, as tenant portfolio. Notify commits the event, fans it out by its rules table into an in-app job and an email job, delivers each through its worker, and the page polls the status endpoint to show every attempt as it lands. Only delivery state reaches the browser; the message stays inside Notify.

The reliability ledger reads GET /api/v1/metrics with a tenant-scoped key, cached five minutes in the function. A daily GitHub Action commits a snapshot so the numbers survive an outage, and the footer's heartbeat is the same call in its smallest form.

checkingRunning the boot health check.
1,348jobs, all tenants
98.2%1,323 sent, 25 failed
32retries across 1,380 attempts
10.1 smedian ingest to delivered
ChannelJobsSentFailedRate
Email, all tenants2952851096.6%
In-app, all tenants7137130100.0%
Push, all tenants3403251595.6%
This site, tenant portfolio220100.0%
Email110100.0%
In-app110100.0%

Source: GET /api/v1/metrics?tenantId=portfolio&days=30 on 2026-09-14T18:35:23.038910606Z (1650 ms)

04 /

Security posture

what leaves the browser, and what does not
  • No secrets in the browser. The Groq, Cloudflare, Gemini and Notify keys live in server functions; the palette and the contact form talk only to /api/* on this domain.
  • Only delivery state comes back from Notify. Message content, recipients and API keys never appear in a response; the metrics endpoint returns counts.
  • Rate limits: 20 questions a minute per address on /api/ask with a one-hour answer cache; three messages per ten minutes on /api/contact, plus a honeypot field.
  • Prompt injection is tested, not assumed: seven adversarial cases run with every evaluation and are published on the lab.
  • Response headers: nosniff, frame denial, strict referrer policy, permissions policy, HSTS (vercel.json). A security.txt says where to report anything.
  • Measurement: Vercel Web Analytics and Speed Insights, which are cookieless and keep no identifier across visits. Microsoft Clarity also loads, and sets first-party cookies. What is measured is page views, Core Web Vitals and a handful of named interactions (a question asked, a lab used, an outbound link); never the text of a question or a message.
05 /

House rules

what the copy is allowed to do
  • Every number links to the query, run file or command that produced it, with a date.
  • No invented metrics, no rounded-up claims, no client screenshots where the work was confidential.
  • Failures are published next to the wins, with what changed because of them.
  • No em dashes, no emoji, no skill bars, no logo walls.
  • The site is its own project: Astro, a RAG palette, a contact form on Notify, and a published evaluation set. The stack and the decisions are on this page; the numbers are on the lab.
keysKeyboard
j / k
next / previous row
Enter
open the focused row
⌘K or /
search, or ask this site
g then h w a c
go home, work, about, contact
t
toggle light and dark
Esc
close the palette or this map
?
this map