Skip to main content

Architecture

Staff only

Internal design notes — stripped from the public build. Do not copy this mechanism into the public pages.

Hosting

Portrait is a self-hosted Cloudflare Worker with an object store for rendered avatars and a database for accounts, email claims, and saved avatar configs. Two hosts:

  • Readsmedia.portrait.intrasys.ai, public and unauthenticated, like Gravatar.
  • Writesapi.portrait.intrasys.ai, session-gated. Requires an iron-sealed WorkOS session cookie (portrait_session, scoped to .portrait.intrasys.ai), set by the headless Magic Auth flow and sent automatically by the console. Writes only ever act on hashes the session user has verified.

Identity is WorkOS. The WorkOS user id differs per environment/app, so it can't be the shared key; the email is the only identifier stable across WorkOS environments, which is why the cross-app key is sha256(lower(trim(email))).

The read path never touches the database

This is the central design property. Both saved states (designed, uploaded) are rendered to PNG variants in the object store when the user saves, so a read is:

object-store lookup → hit means serve the stored PNG · miss means generate from the hash

No database on the hot path. The DB can be down and not a single avatar breaks anywhere in the estate.

Generated avatars

Generated avatars use DiceBear personas by default. The read path's d= also accepts initials, identicon, and bottts. Because the seed is the hash, the same person gets the same generated face in every app, forever, with zero storage. Designed avatars — including those seeded by ✨ Ask AI, which target the avataaars collection — may use any collection the editor supports.

One face across many emails — write fanout, not lookup

"One avatar per account" means forcing identical bytes into each verified hash's independently-keyed object-store slot. Portrait does this on write (a rare, human-speed save), never on read:

EventStorage action
Save avatar (design or upload)Render variants once → write under every verified hash
Claim a new email (verified)Server-side copy the existing variants to the new hash
Release a claimDelete that hash's objects → reverts to generated
Delete avatarDelete objects under every verified hash

An alias table (hash → account) would be tempting but puts a lookup in front of every read, including every miss — trading away the one property worth most here. Portrait rejects it by design.