React
A session that survives reloads, and routes that redirect when it ends.
Everything here runs in the browser, against the publishable key. A complete flow: a client, a sign-in form that handles MFA, a hook that tracks the session, and a protected route.
For permission checks that a user must not be able to skip, pair this with Node on your own API.
Install
npm install @nulvless/nulvless-jsCreate the client
One module, imported everywhere. The publishable key is safe in the bundle.
import { createClient, Storage } from '@nulvless/nulvless-js';
export const nulvless = createClient(
import.meta.env.VITE_NULVLESS_URL,
import.meta.env.VITE_NULVLESS_KEY,
{ storage: Storage.Local, trackUser: true },
);Storage.Local keeps people signed in between visits. trackUser lets the
platform record the device and location on each session.
Track the session
The SDK emits events when authentication changes anywhere in the app, so one hook is enough.
import { useEffect, useState } from 'react';
import type { Session } from '@nulvless/nulvless-js';
import { nulvless } from './nulvless';
export function useSession() {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
nulvless.auth.getSession().then(({ data }) => {
setSession(data);
setLoading(false);
});
const offIn = nulvless.auth.on('signed-in', () => {
nulvless.auth.getSession().then(({ data }) => setSession(data));
});
const offOut = nulvless.auth.on('signed-out', () => setSession(null));
const offDead = nulvless.auth.on('unauthorized', () => setSession(null));
return () => {
offIn();
offOut();
offDead();
};
}, []);
return { session, loading };
}on returns its unsubscribe function — return them from the effect and React
cleans up on unmount.
Sign in
The form has to handle two outcomes: a session, or a challenge when the user has MFA enrolled.
import { useState } from 'react';
import type { Challenge } from '@nulvless/nulvless-js';
import { nulvless } from './lib/nulvless';
export function SignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [challenge, setChallenge] = useState<Challenge | null>(null);
const [error, setError] = useState<string | null>(null);
async function submit(event: React.FormEvent) {
event.preventDefault();
setError(null);
const { data, error } = await nulvless.auth.signIn({ email, password });
if (error) return setError('Check your email and password.');
if (data.challenge) return setChallenge(data.challenge);
// Signed in — the session is stored and the event fired.
}
if (challenge) return <VerifyCode challenge={challenge} />;
return (
<form onSubmit={submit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" />
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
{error && <p role="alert">{error}</p>}
<button type="submit">Sign in</button>
</form>
);
}Collect the code
One component serves every challenge — MFA, OTP sign-in, sudo — because
challenge.subject says which flow you are in.
import { useState } from 'react';
import type { Challenge } from '@nulvless/nulvless-js';
import { nulvless } from './lib/nulvless';
export function VerifyCode({ challenge }: { challenge: Challenge }) {
const [code, setCode] = useState('');
const [error, setError] = useState<string | null>(null);
async function submit(event: React.FormEvent) {
event.preventDefault();
const { error } = await nulvless.auth.verifyChallenge({
token: challenge.token,
code,
});
if (error) setError('That code did not work.');
}
return (
<form onSubmit={submit}>
<p>We sent a code to {challenge.identifier}</p>
<input value={code} onChange={(e) => setCode(e.target.value)} inputMode="numeric" />
{error && <p role="alert">{error}</p>}
<button type="submit">Confirm</button>
</form>
);
}challenge.identifier is already masked — u***@example.com — so it is safe to
show.
Protect a route
import { useSession } from './lib/use-session';
import { SignIn } from './SignIn';
export function Protected({ children }: { children: React.ReactNode }) {
const { session, loading } = useSession();
if (loading) return null;
if (!session) return <SignIn />;
return children;
}Sign out
<button onClick={() => nulvless.auth.signOut()}>Sign out</button>Pass 'others' to drop every other session and keep this one, or 'global' to
end them all.
Social sign-in
<button onClick={() => nulvless.auth.authorize('google', {
redirect_uri: `${window.location.origin}/callback`,
})}>
Continue with Google
</button>The SDK redirects for you. On the callback route:
useEffect(() => {
const code = new URLSearchParams(window.location.search).get('code');
if (code) nulvless.auth.exchangeCodeForToken(code);
}, []);Render the buttons from getMethods() rather than
hard-coding providers — turning one on in the console then needs no redeploy.