01-projects/onoma

Implementation notes — onoma v1 global cache (2026-07-31)

2026-07-31·reference

Follows [[IMPLEMENTATION-NOTES-2026-07-30-onoma-v0]]. Founder ask (iMessage, 2026-07-30 17:05 + 2026-07-31 08:54): cache results so repeat names are not regenerated, key on name type, reuse name parts across names, add autocomplete over previously-requested names, and make the cache global so visitors without a key can explore. Framing constraint from him: "this is just a fun project... we don't have to over complicate it."

Branch v1-global-cache on RayDataCo/onoma.

The shape: reads free, writes BYOK

v0 was a pure static page — the visitor's key went browser-direct to api.anthropic.com and nothing else existed. Global caching needs a backend, so v1 adds a Cloudflare Worker + D1 which also serves the static app (assets binding), replacing the quick-sites static deploy.

The load-bearing decision: the Worker never sees anyone's API key. Generation stays browser-direct exactly as in v0; the Worker is only a cache store. So:

This preserves v0's best property (no key custody, no server-side secret) while getting the global behavior he asked for.

Decisions

Privacy — the one call escalated to the founder

A global cache makes every submitted name world-readable, and people type their own family's names. Resolution: the line is drawn at suggestability, not storage. Multi-word person names are stored and servable but flagged suggestable = 0, so they never surface in autocomplete. Their parts (Benjamin, Andrew, Wilson — dictionary entries, nothing personal) and all non-person names stay fully browsable.

Verified: q=benj returns only Benjamin (part), never Benjamin Andrew Wilson; q=amst returns both Amsterdam and Amstel.

Flagged to founder 2026-07-31 08:5x with the alternative (fully-open autocomplete) as his call. Built the conservative option because it is the reversible direction — opening it later is a one-line change, un-publishing names is not.

Abuse surface (new in v1, did not exist in v0)

/api/contribute is unauthenticated by design — the whole model is that any visitor with a key can feed the shared cache. Mitigations, all verified live:

Control Result
Full server-side re-validation of payload shape {} → 400, bad role enum → 400, beat/part count mismatch → 400
Body cap 24KB 40KB synthesis → 413
Angle brackets rejected in all string fields <img src=x onerror=...> → 400 bad part token
Per-IP rate limit, 20 writes / 60s, salted-hashed IP 25 rapid writes → 429 after the window filled
INSERT OR IGNORE cannot overwrite existing entries

XSS specifically: grepped the whole client for innerHTML / outerHTML / insertAdjacentHTML / document.write / eval — zero sinks, only a comment mentioning the rule. Cached content is inert even before the bracket rejection. The bracket rule is anti-vandalism (inert markup rendering as literal text in a shared card), not the XSS control.

Threat model genuinely changed here and is worth stating: in v0 all rendered content came from the user's own API call. In v1 it comes from strangers.

Tradeoffs / known limits

NOT verified (honest gap)

Explicit gate before any public opening

Founder, 2026-07-31 09:05: "this is still toy/demo territory. If we were really going to open it up to the public we would need to add in some bad word detection and such." Agreed and recorded as a gate, not a backlog item.

The surface stays Cloudflare-Access-gated. The cache being "global" currently means global among people who can already reach the app, which today is the founder. Nothing built here opens the door on its own.

What a genuine public launch would need beyond what exists:

  1. Input moderation on the submitted name — the autocomplete is the exposure, since a slur typed once becomes a suggestion served to everyone.
  2. Output moderation on the derived reading before it enters the shared cache.
  3. A takedown path — currently no way to delete a cached row without hand-editing D1.
  4. Reconsideration of whether person-name parts should be browsable at all at public scale.

Items 1–3 do not exist. Do not treat the Access gate coming off as a config change.

Verification log

Local wrangler dev + D1 (migrations/0001_cache.sql applied clean, 7 commands). Endpoint tests via curl, browser tests via Playwright at 1200px and 393px.

Copy correction made in passing

v0's settings fineprint claimed "nothing passes through RDCO servers." That became false the moment a cache existed — lookups and contributions do transit an RDCO Worker (the key still does not). Reworded to say exactly what is and is not shared, rather than leaving a privacy claim that had quietly stopped being true.