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
302to 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.