Skip to main content

App integration

Adopting Portrait is one <img> tag. It works today — no account, no key, no coordination.

The whole thing

const hash = sha256(email.trim().toLowerCase()) // your app already has the email
<img src={`https://media.portrait.intrasys.ai/${hash}?s=64`} alt="" />
  • sha256(lower(trim(email))) — the identity key. Same key every app derives, byte-identical to Gravatar's spec.
  • s=64 — square pixel size (16–512, default 80). Portrait serves the nearest stored variant ≥ your request.

No SDK, no secret, no avatar_version column, no API call. You can adopt Portrait before it has a single registered user, because the generated avatar is already better than an initials chip.

Keep initials as a fallback

Portrait never 404s for a well-formed hash (unless you explicitly ask via d=404), so the onError path fires only on a network failure — but keep your existing initials chip for it:

<img
src={`https://media.portrait.intrasys.ai/${hash}?s=64`}
alt=""
onError={(e) => { e.currentTarget.style.display = 'none'; /* show initials chip */ }}
/>

IdP photo as fallback

If a user came via a social IdP and WorkOS has a profile_picture_url, pass it as d=<url> so the IdP photo is the fallback ahead of the generated one. An explicit Portrait avatar always wins:

const d = encodeURIComponent(profilePictureUrl)
<img src={`https://media.portrait.intrasys.ai/${hash}?s=64&d=${d}`} alt="" />

Caching

Reads are cacheable and effectively unlimited. By default:

Cache-Control: public, max-age=300, stale-while-revalidate=86400

A saved change goes live estate-wide within ~5 minutes — the same default Gravatar has used for two decades. Because your app holds no Portrait state, this is the contract you get for free.

Want instant, immutable caching? Poll /{hash}.json for the version, then pass it as v=:

<img src={`https://media.portrait.intrasys.ai/${hash}?s=64&v=${version}`} alt="" />

With v= supplied, the response is max-age=31536000, immutable. This is opt-in — most apps don't need it.

A convenience client, later

A tiny @intrasys/portrait-client (hash + URL builder) is a convenience, not a dependency — worth publishing once two apps have wired it by hand, not before. Until then, the two lines above are the whole integration.