Skip to content

Repository files navigation

Heraldia logo

Heraldia

Research the probable context of a surname and turn the evidence into an original artistic emblem.

Explore Heraldia →

Heraldia home screen

Purpose

Heraldia helps people explore the probable historical, linguistic, and geographic context of a surname. It organizes sourced findings, shows the researched place on an interactive map, and creates an original AI-generated emblem inspired by the evidence.

Heraldia does not prove ancestry, nobility, membership in a historical lineage, ownership of a coat of arms, or heraldic rights.

Experience

  • Evidence-first surname research with visible confidence and sources
  • Probable historical locations, country flags, and an interactive origin map
  • Original AI-generated emblems kept private until the visitor chooses to share
  • Interactive 3D inspection with a front-facing orbit, drag, rotation, and zoom
  • Permanent public surname pages and explicit, unguessable emblem links
  • Spanish and English interface with generated content preserved in its original language

Interactive 3D shared emblem page Canonical surname research page

Interactive shared emblem · Permanent surname archive

How it works

  1. Enter one surname. Family geographic context and an approximate period can be added only when known; the entire section is optional.
  2. Heraldia searches public sources and separates documented evidence from inference.
  3. The application presents probable origin, etymology, earliest references, modern distribution, confidence levels, sources, and an interactive origin map.
  4. A server-owned visual brief turns the supported findings into an original artistic emblem.

Sources and evidence

Heraldia prioritizes institutional archives, dictionaries, academic publications, libraries, statistical institutes, and independent corroboration. It uses Forebears as a secondary signal for modern surname distribution, not as proof of historical origin or family descent. Material claims require corroboration, and uncertain findings are labeled accordingly.

Modern distribution, etymological origin, historical place, earliest documented reference, user-provided family context, lineage-specific heraldry, and generated artwork remain separate throughout the experience.

Stack

  • Next.js App Router and strict TypeScript
  • Bun
  • Biome
  • Vercel AI SDK with OpenAI web search
  • Official OpenAI SDK with gpt-image-1
  • Zod validation
  • React Leaflet with OpenStreetMap and CARTO tiles
  • Three.js for the interactive emblem inspector
  • Supabase Postgres and Storage for rate limits, shared research, canonical image reuse, and explicitly published emblem links
  • Browser localStorage for draft metadata and IndexedDB for local PNG blobs
  • Privacy-friendly Vercel Web Analytics for anonymous page-view metrics

Deployment

The application is deployed on Vercel at heraldia.carlostarmeno.com. Pushing to main triggers a production deployment; every other branch gets a preview URL. The apex domain stays with its current registrar — only a single CNAME record for the heraldia subdomain points at Vercel.

Local development

bun install
cp .env.example .env.local
bun dev

Set OPENAI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and a random RATE_LIMIT_HASH_SECRET of at least 32 characters in .env.local. RESEARCH_API_KEY is optional and falls back to the same OpenAI key. Generate the hash secret with:

openssl rand -hex 32

SUPABASE_SERVICE_ROLE_KEY accepts either Supabase's recommended sb_secret_... server key or the legacy service_role key. Apply the migrations in supabase/migrations/ in filename order before starting the production application. Elevated Supabase keys are server-only and must never use a NEXT_PUBLIC_ prefix.

For network-free development, set HERALDIA_USE_FIXTURES=true and RATE_LIMIT_BACKEND=memory. Fixture content is synthetic and is never presented as real historical research.

Commands

bun dev
bun test
bun run test:e2e
bun run check
bun run typecheck
bun run build

Install the E2E browser engines once with bunx playwright install chromium webkit. The Playwright suite runs the full fixture-backed journey in desktop Chromium, Android Chromium, and iPhone WebKit. It never contacts OpenAI or Supabase and includes automated WCAG A/AA, keyboard, responsive-overflow, download, sharing, regeneration, draft restore, and cleanup checks.

Research cache

Surname research is public-record material and does not vary between visitors, so results are cached in Supabase and reused. A hit skips the model entirely, which removes both the per-request cost and the dominant share of the latency.

The cache is keyed on the normalized surname and generated-content language. Family context is user-declared, is not evidence, and never enters the key or the stored payload — writeCachedResearch strips it at the boundary, and the caller's own context is re-applied on read. Entries become eligible for refresh after RESEARCH_CACHE_TTL_DAYS because every source carries a visible retrievedAt. The permanent /surname/[slug] registry may keep the last cached version available while a refresh is pending, so canonical URLs do not disappear.

English and Spanish interface copy is selected with the stable ?lang=en or ?lang=es query parameter. The selected language is sent with a new research request, so generated research and visual-brief prose are produced and cached in that language. Changing the toggle afterward translates only the interface; it never rewrites or machine-translates an existing result. Canonical surname pages also carry content=en|es to pin the generated research language while lang remains free to change with the interface.

The cache is an optimization, never a dependency. A missing migration, an unreachable database, or a row written by an older schema all degrade to a live model call rather than failing the request, so the code and the migration can be deployed in either order.

Cost controls

Image generation dominates the running cost, so the model and quality are environment variables rather than constants. OPENAI_IMAGE_MODEL accepts gpt-image-1-mini, gpt-image-1, gpt-image-1.5, or gpt-image-2, and OPENAI_IMAGE_QUALITY accepts low, medium, or high. The default pairing, gpt-image-1-mini at low, costs roughly an eighth of gpt-image-1 at medium for a 1024×1024 emblem. Emblems are flat, limited-palette designs, which is the shape low handles well.

gpt-image-1 and gpt-image-1-mini are scheduled for API removal on 2026-12-01. Because the model lives in the environment, migrating to gpt-image-1.5 or gpt-image-2 is a redeploy rather than a code change.

Rate limits

Research, image, and publish routes enforce separate daily limits per HMAC-anonymized IP, plus a per-kind cooldown and a concurrent-request cap. The image cooldown defaults to 300 seconds, so a client generates at most one emblem every five minutes on top of the daily cap. Publishing has its own lighter limit because it writes a multi-megabyte object without calling an image model.

MAX_CONCURRENT_IMAGE_REQUESTS must stay at 1. The cooldown is only evaluated when no lease is active, so a higher value lets that many requests through before the first cooldown window starts.

Supabase executes acquisition atomically inside Postgres, so limits remain consistent across server instances and restarts. The API fails closed when the rate-limit service is unavailable. Only short-lived HMAC identifiers and counters are retained, and rows older than seven days are deleted automatically.

Private production testing

The optional /tester-access owner tool enables unrestricted production testing without adding user accounts. Set a high-entropy TESTER_ACCESS_SECRET only in the production environment:

openssl rand -base64 48

On each trusted phone or computer, open /tester-access and paste the secret once. Heraldia exchanges it for a signed __Host- cookie that is HttpOnly, Secure, SameSite=Strict, scoped to /, and valid for 12 hours. Heraldia never persists the secret in browser storage, a URL, the repository, or the public client bundle. Visit the same page to revoke the current device; rotate TESTER_ACCESS_SECRET and redeploy to invalidate every device.

Tester access removes daily quotas and cooldowns only. MAX_CONCURRENT_IMAGE_REQUESTS remains 1, and the shared Supabase bucket prevents two authorized devices from generating images concurrently. Failed enrollment attempts use their own durable daily limit and cooldown. Leave TESTER_ACCESS_SECRET empty to make the route return 404.

Privacy

  • The surname and optional family context are sent to the configured research provider.
  • Visual briefs are sent to OpenAI for image generation.
  • API keys remain on the server.
  • Shared research is persisted by normalized surname and output language, with family context removed.
  • A canonical surname-and-style image may be stored internally and reused, but it has no public personal page by default.
  • Personal variations remain in the browser until the visitor explicitly chooses Share. Publishing creates an immutable public link and stores the PNG in a private Supabase Storage bucket; Postgres stores only its object path identity and structured visual brief.
  • Draft metadata never contains a base64 PNG. Published images restore from their server URL; an IndexedDB blob remains the local fallback and keeps unpublished variations private.
  • Unpublished personal variations have zero server retention: clearing the draft removes their local IndexedDB blobs. Published links and canonical cache images are retained until an explicit removal request or operational cleanup.
  • Storage writes create the database row first and delete it when an upload fails, minimizing partial records. bun run cleanup:storage audits old Storage objects without a matching row in dry-run mode. It ignores objects newer than 24 hours and deletes only when run with --apply.
  • Supabase also stores HMAC-anonymized rate-limit identifiers and counters, not raw IP addresses.
  • Vercel Web Analytics records anonymized page-view metrics without cookies or visitor profiles.
  • Drafts remain in the visitor's browser and can be cleared from the interface.
  • The full public policy is available at /privacy.

Canonical surname pages

Every successfully researched surname is assigned a permanent, URL-safe slug through a transaction-protected Postgres function. Equivalent normalized input reuses the same slug. If distinct spellings collapse to the same ASCII slug, the later entry receives a stable numeric suffix such as pena-2.

/surname/[slug]?lang=en|es&content=en|es is the indexable public-record surface. content pins the generated research language and lang controls only the interface. Personal emblem pages use /e/[surname]-[uuid], remain noindex, and are available only after the visitor chooses Share. Legacy /e/[uuid] links redirect permanently to the cosmetic surname URL.

About

Surname research and AI-generated artistic emblems grounded in historical, linguistic, geographic, and modern distribution evidence. Live at heraldia.carlostarmeno.com.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages