auth
Sign-up, sign-in, OAuth, tokens and session checks.
The top-level namespace. Everything here is available on both clients, but the
browser client also stores what it gets back — a successful signIn leaves the
session on disk and the token refreshing in the background, while the same call
on the server returns tokens and forgets them.
Where a method needs a session, an Authorization header you already supply
wins — per call in options.headers, or for every call through the client's
headers option, in any letter case, so the lowercase one forwardUserHeaders
produces works as is. Otherwise the browser client reads the token from storage.
Neither guesses: a call with no token available fails locally with no_session
and never leaves the process.
signUp
signUp(params) → Authenticated
Creates a user. What comes back depends on how the environment confirms accounts, so branch on the response rather than assuming.
const { data, error } = await nulvless.auth.signUp({
email: '[email protected]',
password,
profile: { name: 'Ada Lovelace' },
});emailstringOptionalRequired unless phone is given.
phonePhoneOptional{ country_code, number } — not a formatted string.
usernamestringOptional3–30 characters: letters, numbers, dots, underscores. Not sufficient on its own — sign-up always needs an email or a phone as well.
passwordstringRequiredChecked against the environment's length, character and breach rules.
profileUserProfileOptionalName, avatar and the rest of the person's details.
Returns Authenticated. Read challenge first — the account is not usable
until confirmation completes, and with code confirmation the user record does
not exist yet at all. See Sign-up and verification for
the full branch.
A single sign-up can be rejected for several fields at once, in which case
error.message is an array. Handle that centrally rather than at each form.
signIn
signIn(params) → Authenticated
Password sign-in. Identify the user by whichever credential your product uses.
const { data, error } = await nulvless.auth.signIn({ email, password });
if (error) return showError();
if (data.challenge) return goToVerify(data.challenge);emailstringOptionalphonePhoneOptionalusernamestringOptionalpasswordstringRequiredoptionsAuthOptionsOptionalPer-call overrides, including the captcha token when the environment requires one for sign-in.
Two outcomes, in order:
data.challenge— the user has a second factor, or the environment requires one. Nothing is signed in yet; carry the challenge toverifyChallenge.data.access_token— signed in. The browser client has already stored the session and emittedsigned-in.
A wrong password answers 401 invalid_credentials. Do not distinguish "no such
user" from "wrong password" in your copy — the platform deliberately does not.
signInWithOTP
signInWithOTP(params) → Challenge
Passwordless sign-in: sends a code and returns the challenge to collect it with.
const { data: challenge } = await nulvless.auth.signInWithOTP({
email: '[email protected]',
});emailstringOptionalphonePhoneOptionaloptionsAuthOptionsOptionalAnswers 202 Accepted with a Challenge, not a session — the session only
appears after verifyChallenge. The method must be enabled
for the environment, or the call is refused.
Because it sends a message, this call is rate limited.
lookup
lookup(params) → LookupResult
Asks what a given identifier can do, before asking for a password.
const { data } = await nulvless.auth.lookup({ email });
if (data.challenge) return goToVerify(data.challenge);emailstringOptionalphonePhoneOptionalusernamestringOptionalThis is what an adaptive sign-in screen is built from: collect the email, ask
the platform, then render either a password field or a code field instead of
guessing. data.lookup describes the match.
lookup may return a challenge already started when the user's only
method is OTP — a code has been sent. Check for it before rendering a password
field, or you will send a second code when the user submits.
authorize
authorize(provider, options) → { url, provider }
Starts a social sign-in.
await nulvless.auth.authorize('google', {
redirect_uri: `${window.location.origin}/callback`,
});providerOAuthProviderRequiredMust also be enabled for the environment.
googlegithubgitlabbitbucketfacebooklinked_inNote the underscore.discordtwitchslackazureappleoptions.redirect_uristringOptionalWhere the flow lands when it finishes. Must be on the environment's allowlist, or the request is rejected before any token exists.
In a browser it redirects for you — the returned URL is assigned to
window.location and the promise never usefully resolves. On the server nothing
is redirected; use data.url yourself:
const { data } = await nulvless.auth.authorize('google', {
redirect_uri: 'https://app.example.com/callback',
});
return Response.redirect(data.url);Prefer rendering provider buttons from getMethods() rather than
hard-coding them, so enabling one in the console needs no redeploy.
connect
connect(provider, options) → { url, provider }
The same OAuth redirect, for a user who is already signed in. The provider identity is linked to the current account instead of starting a new sign-in.
await nulvless.auth.connect('github', {
redirect_uri: `${window.location.origin}/settings/connections`,
});Requires a session. This is the call behind a "connected accounts" screen — and the reason a user can end up with a password, a Google identity and a GitHub identity all resolving to one id.
signInWithIdToken
signInWithIdToken(provider, idToken) → Authenticated
Signs in with an ID token you already obtained natively — a mobile app using the Google Sign-In SDK, for instance, where the redirect dance has already happened outside the browser.
const { data } = await nulvless.auth.signInWithIdToken('google', googleIdToken);Stores the session automatically when one comes back.
exchangeCodeForToken
exchangeCodeForToken(code) → Authenticated
Exchanges an OAuth authorization code for a session
(grant_type=authorization_code).
const code = new URLSearchParams(location.search).get('code');
if (code) await nulvless.auth.exchangeCodeForToken(code);codestringRequiredFrom the code query parameter on your callback URL.
You need this only when you handle the callback yourself. The browser flow
started by authorize is exchanged for you.
verifyChallenge
verifyChallenge({ token, code }) → Authenticated
Submits the code for any challenge — OTP sign-in, MFA after a password, or a sign-up confirmation. One call closes all three.
const { data } = await nulvless.auth.verifyChallenge({
token: challenge.token,
code,
});Alias for auth.challenges.verify, which is also
where the attempt limit and the rest of the challenge lifecycle are described.
Session is stored automatically on success.
activate
activate(token) → null
Confirms an account from the link emailed during sign-up.
const token = new URLSearchParams(location.search).get('token');
await nulvless.auth.activate(token);tokenstringRequiredFrom the token query parameter on your confirmation page.
Returns nothing — no session. Confirming by link proves the address; it does not sign the person in. Send them to sign-in afterwards.
recover
recover({ email }) → null
Sends a password-recovery email.
await nulvless.auth.recover(
{ email },
{ redirect_uri: 'https://app.example.com/reset' },
);emailstringRequiredoptions.redirect_uristringOptionalWhere the link in the email lands.
Resolves the same way whether or not the address exists. That is deliberate — the response must not tell an attacker which addresses are registered — so your UI should say "if that address is registered, we sent a link" rather than confirming anything.
resetPassword
resetPassword({ token, password }) → null
Completes the recovery started above.
await nulvless.auth.resetPassword({ token, password: newPassword });tokenstringRequiredFrom the recovery link.
passwordstringRequiredChecked against the same rules as sign-up.
For a user who is signed in and simply wants a new password, use
auth.security.changePassword instead — it requires the
current password, which this path cannot.
getMethods
getMethods() → AuthMethods
Everything the current user can authenticate with, on this environment.
const { data } = await nulvless.auth.getMethods();
// data.sign_in · data.mfas · data.oauthRequires a session. Returns three lists:
sign_inAuthMethod[]OptionalPassword, OTP and anything else enabled for signing in.
mfasAuthMethod[]OptionalSecond factors, each with a status of configured or not_configured.
Note the field is mfas, plural.
oauthAuthMethod[]OptionalSocial providers, including the ones this user has not connected.
Two screens are built from this: the provider buttons on sign-in, and the security settings page listing what is set up and what is still available.
getSession
getSession() → Session
The current session — device, location, the methods used to reach it, and whether sudo is active.
const { data: session, error } = await nulvless.auth.getSession();Requires a session, and fails locally with no_session when there is none, so
it doubles as "is anyone signed in?" without a network round trip when storage
is empty.
getAccessToken
getAccessToken() → string | null
The current access token, refreshed first if it is expired or close to it.
This is the one method that does not return a NulvlessResult — it resolves
to the token string, or null when there is no session. There is no error to
check.
Call it per request rather than caching the result. Holding the string defeats the refresh, and a token that was valid when you stored it may not be when you send it:
const token = await nulvless.auth.getAccessToken();
await fetch('/api/projects', {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});refreshToken
refreshToken({ refresh_token }) → Authenticated
Exchanges a refresh token for a new session, and updates storage.
await nulvless.auth.refreshToken({ refresh_token: stored.refresh_token });The browser client does this for you — see SessionManager. Call it directly when you hold a refresh token yourself, typically on a server that persisted one.
verify
verify(grant?, options) → Verified
Answers two questions in one call: is this token still valid, and does its holder have the permission you named.
resourcestringOptionalWhich resource the question is about. Omit to check the token only.
scopesstring[]OptionalThe holder must have all of them.
rolesstring[]OptionalMatched against the role's key. Any one is enough. Ignored when no
resource is given.
Returns Verified — the user id, and a session carrying the resolved scopes
for that resource, the amr of how they authenticated, and whether sudo is
active.
const { data, error } = await nulvless.auth.verify(
{ resource: 'acme', scopes: ['members:write'] },
{ headers: { Authorization: `Bearer ${token}` } },
);
if (error?.status_code === 401) return signInAgain();
if (error?.status_code === 403) return forbidden();Role-based access control covers what the two dimensions mean and why you should usually ask for a scope.
signOut
signOut(scope?) → null
Ends the session and clears local storage.
await nulvless.auth.signOut(); // this device
await nulvless.auth.signOut('others'); // everywhere else
await nulvless.auth.signOut('global'); // everywherescopeSignOutScopeOptionalWhich sessions to end. Defaults to local.
localOnly this one. The default.globalEvery session this account has, on every device.othersEvery session except this one.global is what a "sign out everywhere" button does after a password change.
others is the safer version of the same idea when the user is on the device
they trust.
Storage is cleared only when the request succeeds, so a failed sign-out leaves the client in a consistent state rather than locally signed out and remotely alive.
Captcha
When the environment protects an endpoint with a captcha, the call needs a
token in options.captcha_token. It applies to lookup and signInWithOTP,
signIn, signUp and recover, each one only if that group is turned on in
the environment's captcha settings.
You do not have to show the widget up front. Call without a token, and only when
the answer is captcha_required or captcha_verification_failed show the
captcha and repeat the call with the token it returns:
const signIn = (captcha_token?: string) =>
nulvless.auth.signIn({ email, password, options: { captcha_token } });
let result = await signIn();
if (result.error?.code === 'captcha_required' || result.error?.code === 'captcha_verification_failed') {
const token = await showCaptcha();
result = await signIn(token);
}showCaptcha is your Turnstile or hCaptcha widget, configured with the site key
that pairs with the secret set in the environment. A token is accepted once, so
render the widget again for every new attempt.