auth.users
The signed-in user acting on their own profile and identities.
Everything here acts as the current user, on themselves. For acting on other
people, that is auth.admin.users, which needs a
secret key and lives on the server.
Five of these need a secret key
update, sendOTPToUpdateEmail, confirmEmailUpdate, sendOTPToUpdatePhone
and confirmPhoneUpdate are reachable only with sk- — call them from a
browser client and the request is refused before it reaches your code. Everything
else here, including me and the whole identities surface, works with either
key.
The rule behind it: an endpoint that does not declare its accepted key types defaults to secret-only. So the split is not arbitrary — replacing a primary credential is server-side, while adding an identity beside an existing one is not.
The distinction that runs through this page: a user is one person with one id, and an identity is one way of proving they are that person. A user with a password, a Google login and a work email has one id and three identities — which is why signing in with Google after signing up with email does not create a second account.
me
me() → User
The full profile of whoever is signed in.
const { data: user } = await nulvless.auth.users.me();idstringRequiredStable for the life of the account, across every identity added later.
emailstringOptionalThe primary email. The full list is in identities.
phonePhoneOptionalusernamestringOptionalprofileUserProfileOptionalname, biography, gender, avatar_uri, date_of_birth.
identitiesIdentity[]OptionalEvery way this person can sign in.
metadataRecord<string, string | number>OptionalYour own data. Only an admin can set it.
is_confirmedbooleanOptionalWith confirmed_at.
is_bannedbooleanOptionalWith banned_at. A banned user cannot sign in, but the record and its history
survive.
password_enabledbooleanOptionalWhether the user has a password, whatever sign-in methods the platform enables. Use it to offer "Set a password" or "Change password".
last_sign_in_atstringOptionalupdate
update(params) → User
Updates the signed-in user's own profile. Omitted fields are left alone.
const { data: user } = await nulvless.auth.users.update({
profile: { name: 'Ada Lovelace', biography: 'Analytical engines.' },
});usernamestringOptionalSubject to the same rules as sign-up, and still has to be unique.
profileUserProfileOptionalname, biography, gender, avatar_uri, date_of_birth.
Identities are deliberately absent. An email or phone is never edited in place — you add the new one and confirm it, which is what the rest of this page is about.
getIdentities
getIdentities(type?) → Identity[]
Everything linked to the account.
const { data: identities } = await nulvless.auth.users.getIdentities();
const { data: emails } = await nulvless.auth.users.getIdentities('email');typeIdentityTypeOptionalNarrows the result.
emailEmail addresses.phonePhone numbers.oauthSocial providers, each with provider and provider_id.Each identity carries an id — which is what updateIdentity and
removeIdentity take — plus identifier (the address, number, or provider
account) and created_at.
This is the "connected accounts and addresses" screen.
addEmailIdentity
addEmailIdentity({ email, nonce }) → Challenge
Starts adding a second email to the account. A code is sent to the new address.
const { data: challenge } = await nulvless.auth.users.addEmailIdentity({
email: '[email protected]',
nonce: code,
});emailstringRequiredThe address being added.
noncestringOptionalSudo confirmation. See below.
Returns a Challenge — nothing is added yet. Finish with
verifyIdentity.
Requires sudo
Protected under manage_identities. Adding an address you control to someone
else's account is a takeover, so a stolen session must not be able to do it
quietly.
addPhoneIdentity
addPhoneIdentity({ phone, nonce }) → Challenge
The same flow for a phone number.
await nulvless.auth.users.addPhoneIdentity({
phone: { country_code: '+55', number: '11987654321' },
nonce: code,
});phonePhoneRequired{ country_code, number }.
noncestringOptionalverifyIdentity
verifyIdentity({ token, code }) → User
Confirms an addition started by either of the two calls above.
const { data: user } = await nulvless.auth.users.verifyIdentity({
token: challenge.token,
code,
});tokenstringRequiredFrom the challenge you got back.
codestringRequiredWhat the user received at the new address or number.
Returns the updated User, with the new identity present. Until this succeeds
the address is not attached and cannot be used to sign in.
updateIdentity
updateIdentity({ id, primary, nonce }) → User
Changes which identity is primary.
await nulvless.auth.users.updateIdentity({ id, primary: true, nonce: code });idstringRequiredFrom getIdentities().
primarybooleanRequiredThe only property that can be changed today. An identifier is never edited in place — you add the new one and remove the old.
noncestringOptionalRequires sudo
manage_identities.
removeIdentity
removeIdentity({ id, nonce }) → Identity
Unlinks an identity. Returns the one that was removed.
const { data: removed } = await nulvless.auth.users.removeIdentity({
id,
nonce: code,
});idstringRequirednoncestringOptionalRemoving the last identity that can sign in locks the account out. The platform does not decide for you which combinations leave a usable account, so check before offering the button — a user with only a Google identity who unlinks it has nothing left.
sendOTPToUpdateEmail
sendOTPToUpdateEmail({ email, nonce }) → Challenge
Starts replacing the primary email, rather than adding another one.
const { data: challenge } = await nulvless.auth.users.sendOTPToUpdateEmail({
email: '[email protected]',
nonce: code,
});emailstringRequiredThe new address. The code goes there, which is what proves the user controls it.
noncestringOptionalFinish with confirmEmailUpdate.
Requires sudo
manage_credentials — a different group from adding an identity, and an
environment can require one without the other.
confirmEmailUpdate
confirmEmailUpdate({ token, code }) → User
Completes the change. Returns the updated User.
const { data: user } = await nulvless.auth.users.confirmEmailUpdate({
token: challenge.token,
code,
});The old address stops being the primary at this point. Whether it remains as a
secondary identity depends on how it was attached in the first place — read
identities from the response rather than assuming.
sendOTPToUpdatePhone
sendOTPToUpdatePhone({ phone, nonce }) → Challenge
The same replacement flow for the phone number.
await nulvless.auth.users.sendOTPToUpdatePhone({
phone: { country_code: '+55', number: '11987654321' },
nonce: code,
});confirmPhoneUpdate
confirmPhoneUpdate({ token, code }) → User
Completes it, returning the updated User.
await nulvless.auth.users.confirmPhoneUpdate({ token: challenge.token, code });Add or replace?
The two pairs look similar and are not interchangeable:
addEmailIdentity→verifyIdentityleaves the existing address in place and gives the user a second way in. Sudo groupmanage_identities, and callable with either key.sendOTPToUpdateEmail→confirmEmailUpdateswaps the primary address. Sudo groupmanage_credentials, and secret key only — this pair cannot run from the browser, so the swap has to go through your own server.
Both send a code to the new address, and neither changes anything before that code comes back.