One account. A face for every app.

Your ATProto account signs you in everywhere, but every app keeps its own profile of you. Omote shows you where each one comes from, and lets you change a profile in one app, or in every app at once.

Which profile does each app show?

Change your name on Bluesky, and does it change on Grain? Nothing tells you. Grain's own account holds nine profile records, one per app, with three different display names between them.

Omote gives every app a way to say where its profile comes from, and gives you one place to see it. In Noh theatre, omote (表) are the masks a performer changes between roles while the audience still knows who is underneath.

How it works

Three steps, in your own account.

  1. 1

    Sign in with your handle

    Any ATProto account works. You confirm on your own server, and omote stores nothing: your profiles stay in your account.

  2. 2

    See where you appear

    Every app's profile of you, side by side, and for each field the profile it comes from.

  3. 3

    Change it here, or everywhere

    Edit one app's profile within that app's own rules, or your shared profile, which every app that builds on it follows.

For ATProto developers

Your lexicon. One more field. The same answer in every app.

  • One field in your lexicon

    Add extends to your profile record, and it builds on the profiles a person already has. Records take part by shape, so your lexicon never depends on ours.

  • Resolves like tsconfig

    Later bases win, the record's own fields win over all, and null hides what the bases say. The rules are written down, so every app lands on the same answer.

  • No AppView in the path

    @omote-social/profiles reads straight from the account's own server, in browsers, Node and Cloudflare Workers.

An app's own profile record
{
  "$type": "social.grain.actor.profile",
  "extends": ["app.bsky.actor.profile", "social.omote.actor.profile"],
  "displayName": "Alice (photos)",
  "description": null
}
Resolved in that app
import { getProfile } from "@omote-social/profiles";

const profile = await getProfile("alice.example.com", "social.grain.actor.profile");
profile.displayName; // "Alice (photos)": set by Grain's own record
profile.sources.avatar; // { collection: "app.bsky.actor.profile" }

Choose the face you show.

One account, as many profiles as you use, and never a question of which one an app shows.