Enter one GitHub handle → a dossier on your dev persona, read from public commits. Enter two → are you mergeable? Dating-app voice, strictly about code.
Live: https://gitmatch.nvps742.workers.dev
A Vite frontend + one Cloudflare Worker + KV, served from a single origin. One handle becomes a "case file" persona card; two handles become a compatibility face-off. Every word on a card is generated from the numbers by rule — no LLM, no API keys beyond a free GitHub token, genuinely $0 to run.
Each card is also a shareable image: /api/card.svg?handle=x and
/api/compare.svg?a=x&b=y render self-contained SVGs (embeddable in a README or
a tweet), and a shared link unfurls into that image via server-injected OG tags.
The image endpoints never build a card for a handle nobody has looked up —
an <img> is fetched unattended by crawlers and proxies, and inventing handles
is free for an attacker, so that path is refused and renders a placeholder
pointing at the page instead. A handle a human has looked up is remembered for
90 days and keeps refreshing, so an embedded badge stays alive long after the
card behind it expires.
Public activity only, plus one thing more:
- Public commits, repos, profile via the REST API — the last 100 commits, languages, stars, fork ratio, abandoned repos.
- Commit hours in the author's local time. GitHub returns each commit date
with its original UTC offset (
…T02:51:59.000+05:30); we read the hour on the author's own clock, not UTC. "Night owl" means night where they are. - An anonymized count of private work via the GraphQL
contributionsCollection(restrictedContributionsCount). We can see how much someone keeps private — never what it is. For many developers this is the majority of their year, so a quiet public profile is never read as "inactive."
A zero-scope GitHub PAT is enough for all of it: it only ever returns public data plus these anonymized counts. No private repo, commit, or diff is reachable.
Pure core, thin I/O shell. The functions that decide what a card says are pure and deterministic; everything that touches the network or KV is isolated at the edges, so the interesting logic is trivially testable and can never invent data.
src/types.ts— locked data contracts (Facts,Card,MatchCard,PrivateSignal, …).src/facts.ts— pure.extractFacts()turns raw GitHub data intoFacts(local-time commit hours, garbage-date filtering, private share);compatibility()scores twoFacts0–100 from public behavior only (language 0.4 + hours 0.3 + habits 0.3).src/persona.ts— pure. TurnsFactsinto a developer's dating profile: a curated archetype ("The Tinkerer") from a combination of public behaviors, a witty bio, trait chips, and the merge face-off (a dating-match blurb + the click/clash signals). Deterministic; every number it prints is interpolated fromFacts, so nothing can be invented.src/github.ts— I/O. 4 calls per uncached handle: user, repos, commit search (REST) + contributions (GraphQL, best-effort — a failure never blocks a card).src/cache.ts— KV get/put. Two versions, because the layers cost different amounts to rebuild: bumpFACTS_VERSIONonly when theFactsshape changes (that eviction costs 4 GitHub calls per handle), andCOPY_VERSIONfreely for any wording change (regenerating copy from cached facts is free). The same asymmetry sets the TTLs — facts live 30 days, cards 7 — so a card rebuild reuses cached facts instead of going back to GitHub.src/ratelimit.ts— per-IP, per-hour counter in KV. Approximate by construction and documented as such; it fails open. It guards/api/*; the image paths are bounded structurally instead (seecachedOnlyincard.ts).src/config.ts— pure. Parses and validates the numeric vars once per request, with documented defaults, so a bad value can't surface as an opaque 500 from inside a KV write.src/card.ts— orchestration:getSoloCard()/getMatchCard(). Owns handle validation and thecachedOnlyswitch the image endpoints run under: cached card → serve it; cold but known → rebuild; cold and unknown → placeholder.src/index.ts— Worker router:/api/card,/api/compare, the.svgcard endpoints, per-handle OG-tag injection on/, CORS, static assets. Also emits one Analytics Engine data point per request (path, GitHub calls, outcome) — the numbers that say whether "$0 to run" still holds.src/shared/lang.ts— pure. The language palette and contrast guard, used by both renderers so the shareable image can't drift from the web card.src/client/— Vite frontend ("The File On You"): case-file solo card, the merge face-off, local-time commit clock, and the private-work redaction bar. Builds todist/, which the Worker serves as static assets.
The Worker serves dist/ and handles /api/* in the same script ([assets] in
wrangler.toml), so the frontend's relative fetch("/api/card…") calls are
same-origin by construction — no routing rule to configure, and no secret ever
reaches the client.
npm install
# create the KV namespace, then paste its id into wrangler.toml
npx wrangler kv namespace create GITMATCH_KV
# the only secret (never committed)
npx wrangler secret put GITHUB_TOKEN # PAT with NO scopes — public data onlynpm test # two projects: Worker code on Miniflare, client code on node
npm run typecheck # tsc --noEmit, strict mode
npm run build # build the client into dist/
# Full stack, one origin. Two terminals (deliberately not chained — the
# shell operators for backgrounding differ across platforms):
npm run dev # terminal 1: wrangler dev, serves dist/ + /api/*
npm run watch # terminal 2 (optional): rebuild dist/ on client editsnpm run deploy # vite build → dist/, then wrangler deployFirst deploy only: register a free *.workers.dev subdomain in the Cloudflare
dashboard, and set the GITHUB_TOKEN secret (above). Secrets apply on the next
request, no redeploy.
Accounts/auth/swiping, a user database, deeper history via GraphQL beyond the contribution count, anything romantic about a real person.
MIT — see LICENSE.