Pairwise subject ids
The sub claim that your app receives identifies a member to your business and to nobody else. Here is what that means for the way you store and link accounts.
What you receive
Every ID token and every /userinfo answer carries sub, for example avp_mV3k0nT2xqZ8aB1cD2eF3g: the prefix avp_ and 22 random characters.
- The same for every app of your business. If you register three apps under one business, a member has one
subfor all three. Build one user table. - Different for every other business. Two businesses cannot match a member by
sub. That is the point: it protects members from being followed across businesses. - Never the member's Avonao ID account id, and never derived from their email address. The
sidclaim is separated per business in the same way. - Stable. It does not change when the member changes their email, their name or their sign-in methods, or when they revoke your app and sign in again.
- Gone with the account. If the member deletes their Avonao ID and later creates another, your business sees a new
sub: a different account, not the same person back. See Webhooks for the deletion event.
Store it as the key of your user
Add a column such as avonao_sub to your users, make it unique, and look members up by it. Do not use the email address as the key: members can change their primary email, and the next sign-in then carries the new one. You may keep the email as a contact detail and update it when it changes.
Linking to accounts you already have
If you already have users with passwords or other sign-in methods, attach Avonao ID to them at the first sign-in:
- Look up a user by
sub. Found: sign them in. - Not found, and the token has
email: look for a local user with that address. Attach only when you have verified that address on the local account too.email_verifiedis alwaystruefrom Avonao ID, so the Avonao ID side is covered; the local side is yours to check. - If that local user already has a different
avonao_sub, do not overwrite it. Ask the member to sign in the way they used before, then link from inside their account. - Otherwise create a new user and continue with your own onboarding.
// After finishSignIn() returned `claims`. Pseudocode: db is your own data layer.
let user = await db.users.findByAvonaoSub(claims.sub); // 1. returning member: the pairwise sub is the key
if (!user && claims.email) { // 2. first sign-in with Avonao ID
const existing = await db.users.findByEmail(claims.email); // claims.email is verified by Avonao ID, so you may attach
if (existing?.emailVerified) { // it to a local account whose email YOU also verified
await db.users.update(existing.id, { avonaoSub: claims.sub });
user = existing;
}
}
user ??= await db.users.create({ avonaoSub: claims.sub, email: claims.email, name: claims.name }); // 3. new member
await startYourSession(user); // your cookie, your session storeIds in events
The sub in grant.revoked and member.deleted webhooks is the same pairwise id as in your tokens, so you can find the user the event is about without any extra mapping.
Check it yourself
Sign in twice with the same member and compare: the sub is identical. Sign in with the same member to a second app of your business: still identical. Members of other businesses never match.

