auth.admin
Administering users, roles, grants and OAuth applications.
Server client only, and every method here acts as the platform — not as a signed-in person. That means no permission is enforced on the caller and nothing records who asked. Log that on your side; the audit trail cannot recover it for you.
The line to hold in mind: auth.grants is a user doing
something, auth.admin.grants is your back office doing something. Reaching for
admin because it is easier removes the enforcement you wanted.
Four namespaces sit under auth.admin:
admin.usersAdminUsersClientOptionalCreating, listing, updating, banning — and the support paths for sessions and factors.
admin.rolesAdminRolesClientOptionalDefining roles and the scopes they carry.
admin.grantsAdminGrantsClientOptionalAssigning access directly, with no invitation.
admin.oauthClientsAdminOAuthClientsClientOptionalRegistering applications allowed to request tokens from your issuer.
admin.users
create · list · get · update · delete · ban · unban
await nulvless.auth.admin.users.create(params);
await nulvless.auth.admin.users.list({ page, page_size });
await nulvless.auth.admin.users.get(userId);
await nulvless.auth.admin.users.update(userId, params);
await nulvless.auth.admin.users.delete(userId);create
emailstringOptionalphonePhoneOptionalusernamestringOptionalpasswordstringOptionalOmit to create a user who has no way to sign in yet — useful when you invite them to set one.
profileUserProfileOptionalmetadataRecord<string, string | number>Optionalemail_verifiedbooleanOptionalCreate the user with the email already verified.
phone_verifiedbooleanOptionalCreate the user with the phone already verified. Requires a plan with phone authentication.
Unlike auth.signUp nothing is strictly required here,
because an admin may be seeding a record ahead of the person. A user with no
identity cannot sign in, so pass at least one of email, phone or username
unless that is the point.
When every credential you send is marked as verified (email with
email_verified, phone with phone_verified), the user is created confirmed:
no confirmation is sent, no session is created, and they can sign in right away.
Otherwise the user goes through the confirmation your platform's sign-up settings
define, such as a link sent to the email.
list
pagenumberOptionalpage_sizenumberOptionalsortPaginationSortOptionalemailstringOptionalusernamestringOptionalphonestringOptionalidstringOptionalis_confirmedbooleanOptionalis_bannedbooleanOptionalReturns Paginated<User>.
update
Takes userId, then a narrower set than create:
usernamestringOptionalprofileUserProfileOptionalmetadataRecord<string, string | number | null>OptionalMerges into what is stored, so you only send the keys you are changing. Send a
key as null to remove it. Omit the field to leave everything untouched.
Identities are deliberately absent. An email or phone is changed through its own
confirmed flow in auth.users, never edited in place by an
admin — otherwise an administrator could point an account at an address nobody
proved they own.
The same fields are editable by the user themselves through
auth.users.update. Reach for the admin path only when
acting on someone else.
Cutting access
await nulvless.auth.admin.users.ban(userId);
await nulvless.auth.admin.users.unban(userId);Banning stops sign-in and leaves everything else intact, so the history behind the account still resolves. Deleting is for when the record itself must go — remember their grants do not disappear with them, so clean those up too.
Sessions and factors
await nulvless.auth.admin.users.getSessions(userId);
await nulvless.auth.admin.users.revokeSession(userId, sessionId);
await nulvless.auth.admin.users.signOutAll(userId);
await nulvless.auth.admin.users.getFactors(userId);
await nulvless.auth.admin.users.removeFactor(userId, factorId);signOutAll is what you call after a compromise — it ends every session the
user has, everywhere.
removeFactor is the support path for someone who lost both their authenticator
and their recovery codes. It strips a security control with no confirmation from
the user, so it must sit behind whatever verification your support process
requires. The platform will not second-guess an admin key.
admin.roles
list · create · update · delete
A role is a named bundle of scopes inside a context. Changing the scopes on a role changes what every holder can do, without touching a single grant — which is why it, and not the grant, is where permission changes belong.
create
const { data: role } = await nulvless.auth.admin.roles.create({
name: 'Administrator',
key: 'admin',
context: 'org',
scopes: ['members:read', 'members:write', 'projects:read'],
});namestringRequiredHuman-readable. What a members screen shows.
keystringRequiredThe stable identifier, and what verify({ roles }) matches against. Not
the name.
contextstringRequiredWhich kind of resource this role applies to. Fixed once created.
scopesstring[]RequiredWhat the role grants.
descriptionstringOptionallist
list(context) → Role[]
const { data: roles } = await nulvless.auth.admin.roles.list('org');The roles defined for one context. This is where the role_id values for
grants.invite come from — read them once at start-up
and map them, rather than looking them up per invitation.
update
Takes role_id, then name, description or scopes. key and context are
fixed, because changing either would silently reinterpret every existing grant.
admin.grants
list · set · delete · deleteByResource
set
Assigns access directly — no invitation, no acceptance step. The grant exists
immediately with status: 'granted'.
await nulvless.auth.admin.grants.set({
resource: org.slug,
granted_to_user_id: creatorId,
role_id: ownerRoleId,
});resourcestringRequiredgranted_to_user_idstringRequiredThe user must already exist. Unlike invite, this cannot address someone by
email.
role_idstringOptionalOmit to grant bare scopes with no role — useful for a service account that no role should describe.
scopesstring[]OptionalThe canonical use is seeding the first owner. Whoever created a resource should not have to accept an invitation to it, and doing this in the same transaction that creates the resource means a failure leaves no orphan nobody can administer.
list
pagenumberOptionalpage_sizenumberOptionalsortPaginationSortOptionalresourcestringOptionalcontextstringOptionalgranted_to_user_idstringOptionalrole_idstringOptionalReturns Paginated<Grant>. Filtering by granted_to_user_id answers "what does
this person reach?" — the back-office question auth.grants.list answers only
for the caller.
deleteByResource
await nulvless.auth.admin.grants.deleteByResource(org.slug);Clears every grant on one resource at once. Call it when a project or organization is deleted.
Skipping this leaves grants pointing at nothing. Harmless until a slug is reused — at which point the old members quietly reappear on the new resource.
admin.oauthClients
create · list · get · update · delete · regenerateSecret
await nulvless.auth.admin.oauthClients.create(params);
await nulvless.auth.admin.oauthClients.list();
await nulvless.auth.admin.oauthClients.get(id);
await nulvless.auth.admin.oauthClients.update(id, params);
await nulvless.auth.admin.oauthClients.delete(id);
await nulvless.auth.admin.oauthClients.regenerateSecret(id);These are the applications allowed to request tokens from your issuer — the
other side of auth.oauth.
regenerateSecret rotates a client secret and the old one stops working
immediately. There is no overlap window, so coordinate it with whoever
operates that integration rather than treating it as routine hygiene.