Concepts
The identity key
The cross-app key is the email, hashed:
hash = sha256( lower( trim( email ) ) ) // 64 hex chars — byte-identical to Gravatar's spec
- Email is the identifier every app already has, and it's stable no matter how a user signed in — so it can be the shared key without any coordination.
- This is what makes adoption free. An app already knows its user's email, so it computes the hash itself and builds the URL. It never calls a Portrait API, holds a Portrait secret, or stores a Portrait id.
- Portrait derives the same hash from your email at sign-in. Both sides independently arrive at the same key — that's the join.
- The hash is not a secret. It's a public identifier, safe in client-side HTML. An unknown hash returns a generated avatar, so it's no email-existence oracle.
Three states for any hash
| State | Serves |
|---|---|
| Generated | Nothing saved — a deterministic avatar derived from the hash, the same everywhere, with nothing stored. |
| Designed | An avatar you built in the option editor (incl. ✨ Ask AI output). |
| Uploaded | A real photo, normalized to a square. |
Because a generated avatar is derived from the hash, the same person gets the same generated face in every app, every time — no account needed.
✨ Ask AI, briefly
Ask AI turns a photo into a designed avatar: drop a photo and Portrait suggests a set of editor
options that open in the Design editor, where you tweak and save through the normal designed path.
It's assisted, not an exact likeness, and the photo is discarded, never stored. See
POST /me/avatar/from-photo.
Accounts & claims
An account is not an email. A Portrait account anchors on your sign-in; emails are claims attached to it, each proven by a clicked magic link. One account may claim several addresses (work + personal), and every verified claim resolves to the same avatar — so one face follows a person across every address they use.
This dissolves "change my email" as a feature: you add a claim, verify it, set it primary, and drop the old one. There's always at least one primary — it can't be removed without promoting another first.
A verified claim is globally exclusive: one verified hash, one account, first claim wins. A second account attempting the same address is refused. Moving an address between accounts means releasing it from the first.
Verification and login are the same act
A clicked magic link proves control of an inbox. That single proof serves both purposes:
| Magic link sent to | Result |
|---|---|
| Unclaimed address | Create account; that address becomes primary |
| Address verified on account A | Log into A |
| Address claimed from a session on A | Verify it onto A |
| Address verified on another account B | Refuse — already connected |
Because verification is login, a magic link to a claimed address always logs into its owner — it never forks a new account off a secondary address. (This is a known Gravatar wart that Portrait fixes; see How Portrait differs from Gravatar.)
Reads and writes
- Reads (
media.portrait.intrasys.ai) are public and unauthenticated, like Gravatar. Every well-formed hash resolves to something — a saved avatar or a generated one. - Writes (
api.portrait.intrasys.ai) require a Portrait sign-in and only ever act on hashes the session user has verified.
When you save, reset, or claim an address, your one avatar stays consistent across every verified address on your account — you manage a single face, not one per email.
How Portrait differs from Gravatar
Gravatar is the reference design, matched wherever matching is free — identical hash spec, aligned
params, the same ~5-minute cache default, one-<img> integration. The deliberate differences:
| Gravatar | Portrait | |
|---|---|---|
| Sign in with secondary email | steals the address into a new account | logs into the owning account |
| Customise generated art | pick from a gallery | full option editor + ✨ Ask AI from a photo |
| Avatar upload API | none documented — web UI only | POST /me/avatar |
| Different image per email | yes | no — one avatar per account |
Content rating (r=) | user self-rates | none — r= accepted, ignored |
| Profile data (bio, links) | yes, expanding | non-goal — the image only |
| Hosting | Automattic SaaS | self-hosted by Intrasys |