My personal site. It's a quiet little page — a photo, a short bio, a few links — with one playful trick up its sleeve.
Below the biography there's a button: Generate Fun Versions. Click it and a small AI (GPT-5 Nano) dreams up five tongue-in-cheek concepts for retelling the bio — Fantasy Wizard CTO, Suspicious Pigeon Expert, Medieval Town Crier, that kind of thing. Pick one and it rewrites the bio in that voice, right below the original. The real bio never goes away; the rewrite is just an alternate telling.
The concepts change every time, the facts stay the same (the model is told to keep jobs, places, and people intact and only play with tone), and the whole thing is built to stay cheap and safe to run.
- Next.js 16 (App Router) + React 19, mostly Server Components
- TypeScript + Tailwind CSS v4
- Server Actions for the two AI calls — no API routes, no client-side keys
- OpenAI SDK with Structured Outputs (Zod schemas, no hand-parsed JSON)
- Redis for rate limiting and a response cache, behind a small abstraction so it's local Redis in dev and Upstash in production
- PostHog for web analytics + LLM observability (lightweight
posthog-nodecapture — no LangChain, no extra AI SDK) - Deployed on Vercel
npm install
docker compose up -d # local Redis on :6379
cp .env.example .env.local # then add your OPENAI_API_KEY
npm run devOpen http://localhost:3000.
You only strictly need an OPENAI_API_KEY to try the feature. Redis is optional
in development — if it's not running, rate limiting simply turns off with a
warning and everything else works.
| Variable | Required | What it's for |
|---|---|---|
OPENAI_API_KEY |
yes (prod) | Calls GPT-5 Nano for suggestions and rewrites. |
OPENAI_MODEL |
no | Override the model id. Defaults to gpt-5-nano. |
BIO_SIGNING_SECRET |
yes (prod) | HMAC secret for signing rewrite prompts. Dev uses a fallback. |
USE_LOCAL_REDIS |
dev | Set true to use a local Redis via REDIS_URL. |
REDIS_URL |
dev | e.g. redis://localhost:6379. |
KV_REST_API_URL |
prod | Upstash REST URL (rate limiting + cache). Vercel's Redis integration injects it. |
KV_REST_API_TOKEN |
prod | Upstash REST token (injected alongside the URL). |
POSTHOG_TOKEN |
no | PostHog project token. Unset → analytics are a no-op (e.g. locally). |
POSTHOG_HOST |
no | Server-side PostHog host. Defaults to https://us.i.posthog.com. |
In development the app is lenient: a fallback signing secret is allowed and Redis is optional. In production it fails fast — if a required variable is missing, it throws rather than running without protection.
| Command | Does |
|---|---|
npm run dev |
Start the dev server. |
npm run build |
Production build. |
npm run start |
Serve the production build. |
npm run lint |
ESLint (Next core-web-vitals). |
npm run typecheck |
tsc --noEmit. |
npm test |
Jest (signing/security tests). |
CI runs lint + typecheck + test on every push and PR to main
(.github/workflows/ci.yml). None of it needs secrets.
- The page is a Server Component. It renders the image, bio, and links from
lib/bio.ts— the single source of truth for all content. No AI calls happen on load. - Generate Fun Versions calls the
generateSuggestionsServer Action. It rate-limits the caller, asks GPT-5 Nano (via Structured Outputs) for five{ title, prompt }concepts, and signs each prompt with HMAC-SHA256 before sending it to the browser. - Picking a concept calls
rewriteBiowith the prompt and its signed token. The server re-verifies the signature and expiry, confirms the prompt matches the signed payload, rate-limits, checks the cache, and only then asks the model to rewrite the bio.
The browser may show a prompt but is never trusted to supply one. Every
prompt is signed when generated and re-verified before any rewrite runs — the
signed payload, not the request body, is authoritative. Tokens expire after 15
minutes. See lib/signing.ts (and its tests).
- Suggestions are generated fresh each click (cheap — five short items from a nano model) so the ideas always feel new.
- Rewrites are cached in Redis for 30 days, keyed by the biography + the exact prompt, so a repeated concept is served from cache instead of the model.
- Daily per-IP limits: 5 suggestion generations and 25 rewrites.
Nothing outside lib/cache/ and lib/rate-limit/
knows whether it's talking to a local Redis or Upstash. The factory picks the
implementation from the environment. Caching fails open (a backend hiccup is
just a cache miss); rate limiting fails closed in production (it never
silently disables protection) and degrades to "allow with a warning" in
development.
PostHog handles both web analytics and LLM observability, and it's entirely
gated on POSTHOG_TOKEN — unset (e.g. local dev), nothing fires.
- Web:
posthog-jsvia a small provider (components/posthog-provider.tsx), with manual$pageviewcapture for App Router navigations. Browser events go through a first-party/ingestreverse proxy (rewrites in next.config.ts) so ad-blockers don't eat them. - LLM: each OpenAI call emits a
$ai_generationevent from the server (lib/posthog.ts) with model, token counts, and latency — usingposthog-nodedirectly, so PostHog's LLM dashboards light up without pulling in any AI framework. Events flush vianext/server'safter(), so analytics never add latency to a response.
(Google Analytics from the original site is preserved alongside PostHog.)
- Import the repo into Vercel. It auto-detects Next.js — no build config needed.
- Add a Redis store from the project's Storage tab (Vercel's Upstash/KV
integration). It injects
KV_REST_API_URLandKV_REST_API_TOKENautomatically — the app reads those directly, so there's nothing to copy. - Set the remaining environment variables in Project Settings → Environment
Variables:
OPENAI_API_KEYBIO_SIGNING_SECRET(openssl rand -hex 32)POSTHOG_TOKEN(optional — enables analytics + LLM observability)- Redis: the
KV_REST_API_*pair from step 2 is injected automatically. LeaveUSE_LOCAL_REDISunset.
- Deploy.
mainis production; pull requests get preview deployments.
app/ layout (+ analytics), homepage, server actions
components/ BioRewriter + PostHog provider/pageview
lib/ bio (content), env, signing, schemas, openai, posthog
cache/ cache interface + local (ioredis) and Upstash impls
rate-limit/ limiter interface + local and Upstash impls
public/ ntr600.jpg
See AGENTS.md for conventions if you're contributing (human or AI).