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.