Skip to content

Profiles and claims

Every Omxus account carries a name, a picture and a line about the person. Your site reads them; it never builds a profile system. OIDC sites get them as standard claims. Sites on the SDK get a settings screen in one call.

Over OIDC: four claims

Ask for the profile scope. The id_token and /oidc/userinfo carry:

Claim What it holds
preferred_username The username in your site's scope (see below). Absent if they have none.
name Their display name, else their username.
picture Always present. A fixed address for their current picture.
profile Their bio, when they wrote one.

Any off-the-shelf OIDC library draws a correct profile from these. No Omxus code needed.

picture never changes

picture is https://auth.omxus.com/picture/<opaque>. The opaque part seals one account on one site (AES-GCM, keyed from the provider's pairwise salt), so:

  • the same person on your site always gets the same address: store it, put it in an <img>, cache it;
  • two sites get two unrelated addresses, so they cannot match people up;
  • it answers 302 to whatever picture the person has now. Change it at account.omxus.com and every site shows the new one within five minutes (cache-control: public, max-age=300). No token, no sign-in bounce.

An unknown or forged address answers 404.

The picture itself

A picture is a short string, called a spec:

Spec What it draws
aurora:3 A style and a palette
mono:7:TA A monogram: palette 7, the letters TA
url:https://… A picture hosted elsewhere, or one they uploaded

Drawn pictures render at https://auth.omxus.com/avatar/<style>-<palette>.svg, for example /avatar/aurora-3.svg. The file name is the picture: nothing is looked up, no session is read, and everyone who picked the same one shares one URL. They cache for a year (immutable).

Five styles: aurora, mono, bloom, strata, orbit. Twelve palettes, numbered 0 to 11: ember, dusk, reef, gold, rose, moss, ink, plum, sand, tide, flame, mint. Sixty pictures, plus initials on the monogram.

Nobody is a blank circle. An account that picked nothing gets a picture drawn from its id. The picture claim and GET /accounts/profile fill that in for you. GET /accounts/me does not: it returns only what the person chose (avatar.spec and avatar.url both null until they pick), so two untouched accounts look identical there. Use accounts.avatarOf(account), which draws the same default the server would.

Scope: shared or your own

A profile follows the same scope as the username:

  • shared (default): every Omxus site in the shared scope sees the same name and picture.
  • site: a client registered with username_scope: "site" gets its own world. Its own username, name and picture; changing one leaves the other alone.

With the SDK

<div id="settings"></div>
<script type="module">
  import { createAccounts } from 'https://auth.omxus.com/sdk/omxus-accounts.mjs'
  import { mountSettings } from 'https://auth.omxus.com/sdk/omxus-settings.mjs'

  const accounts = createAccounts({ issuer: 'https://auth.omxus.com', clientId: 'YOUR_ID' })
  mountSettings(document.querySelector('#settings'), { accounts, signInHref: '/login' })
</script>

Untested end to end; both modules are live at those addresses (checked 30 Oct 2026) and the calls match their exports.

mountSettings draws the picture picker, display name, username (with a live availability check), bio, password, passkeys, devices, sign out and delete account. It renders into a shadow root: your stylesheet cannot reach in, its styles cannot leak out. It inherits the page's font and colour; the rest are custom properties on the host element:

#settings {
  --omxus-accent: #c9a84c;
  --omxus-accent-ink: #0b0b0d;
  --omxus-line: rgba(201, 168, 76, 0.25);
  --omxus-panel: rgba(255, 255, 255, 0.03);
  --omxus-radius: 2px;
  --omxus-danger: #b42318;
}

The same data without the screen:

Method Does
accounts.profile() Their profile, default picture filled in
accounts.setProfile({ displayName, bio, avatar }) Changes only the keys you pass; null clears one
accounts.avatarChoices(letters) All sixty pictures, monograms drawn with letters
accounts.uploadAvatar(dataUrl) Uploads a photo and makes it the picture
accounts.avatarOf(account) A URL to draw, chosen or default
accounts.setUsername(name) / usernameAvailable(name) Change or check a username

HTTP

All on https://auth.omxus.com. "Bearer" means the accounts session token.

Route Auth Body Returns
GET /accounts/profile Bearer { profile }
POST /accounts/profile Bearer Any of display_name, bio, avatar { profile, account }
POST /accounts/avatar/upload Bearer { image: "data:image/webp;base64,…" } { profile, account }
GET /accounts/avatars?client_id=…&letters=TA None { avatars: [{ spec, style, palette, paletteName, url }] }
GET /avatar/<style>-<palette>.svg None The drawn picture
GET /picture/<opaque> None 302 to the current picture

profile is { id, username, displayName, name, bio, avatar: { spec, url, chosen }, verified }. name is what to print above the picture: display name, else username.

/accounts/avatars needs a registered client_id; without one it answers 400 unknown_client.

Limits and errors

Field Rule Error
display_name NFKC, whitespace squeezed, 40 characters, no control characters 400 invalid_display_name
bio NFKC, 300 characters 400 invalid_bio
avatar A spec that parses; url: must be https:, 512 characters at most 400 invalid_avatar
image (upload) PNG, JPEG or WebP data URL, 64 KB at most; shrink it in the page first 400 invalid_picture

An upload is stored under a random name (/avatar/u/<id>.<ext>), so its address says nothing about who. The provider keeps each account's three most recent uploads. If the provider has no upload storage switched on, the route answers 503 uploads_off; offer the drawn pictures instead.

None of these need re-authentication: a name or a picture is not a way in.

Changing a username

POST /accounts/username with { username }, as often as the person likes. The old name is released the moment the new one is taken. Names that look alike (confusable characters) count as the same name. Check first with GET /accounts/username?username=…; it is rate limited per IP.

Not provided

There is no route to read someone else's profile. Your site keeps its own public pages, keyed on the sub it already holds.