Building with the SDK
JavaScript
The framework-agnostic client.
npm install @zevauth/js
@zevauth/react is built on this. Use it directly for Vue, Svelte, vanilla
JavaScript, or anywhere you want the flows without the components.
import { createZevAuth } from '@zevauth/js';
const zevauth = createZevAuth({ publishableKey: 'pk_test_...' });
await zevauth.load();
load() reads your environment, restores any stored session, and completes a
hosted-page handoff if the URL carries one. Call it
once at start-up; calling a flow before it resolves is fine.
Signing in
await zevauth.signIn.withPassword({ identifier, password });
await zevauth.signIn.startMagicLink({ email });
await zevauth.signIn.startEmailCode({ email });
await zevauth.signIn.verifyEmailCode({ email, code });
zevauth.signIn.withZevId(); // full-page redirect
identifier takes an email or a username. The server tells them apart, so
your form needs one field.
Signing up
const { requiresEmailVerification } = await zevauth.signUp.create({
email,
password,
firstName,
});
Whether a new account is signed in immediately or must confirm its address first is your setting, so read the result rather than assuming.
The session
zevauth.user; // the signed-in user, or null
zevauth.organization; // the active organization, or null
await zevauth.getToken(); // a valid access token, refreshed if needed
await zevauth.signOut();
Subscribe to changes:
const unsubscribe = zevauth.addListener((state) => {
render(state.user);
});
The listener is called immediately with the current state, so you never render a frame against something you have not been told about.
Storage, and what it costs
The access token is held in memory only. The refresh token is persisted, so a session survives a reload.
That means the refresh token is readable by any script on your page. It is worth being plain about: an XSS on your site can take it. What makes that survivable rather than fatal is rotation with reuse detection. A stolen token buys at most one refresh before the real client’s next refresh trips the alarm and the whole session is burned.
It is not a substitute for not having XSS.
You can supply your own storage:
createZevAuth({
publishableKey: 'pk_test_...',
storage: { get, set, remove },
});
The signed-in person
client.me reads and writes the current user’s own profile. It is deliberately
narrow: publicMetadata is not here because it travels inside the access token
and your backend trusts it for authorization, and email is not here because
changing an address is a flow that needs proof rather than a field edit.
const { user, organizations, organization } = await client.me.fetch();
await client.me.update({ firstName: 'Ada', lastName: 'Lovelace' });
Profile pictures
Uploading is two calls, not one. The bytes go to storage first and the URL is saved second, which is what lets you show the picture the moment it is chosen. It also keeps the two failures apart: an upload that fails is a network problem worth retrying, and a save that fails is a permission problem worth reading.
const url = await client.me.uploadAvatar({
file,
onProgress: (fraction) => setProgress(fraction),
});
await client.me.update({ imageUrl: url });
The file never touches the ZevAuth API. It goes straight to object storage with a short-lived signed URL, and the object is keyed by the hash of its contents, so uploading the same picture twice costs one upload.
Pass imageUrl: null to remove a picture. Leaving the field out leaves it
alone, which is a different thing: most profile patches touch a name and should
not clear an avatar on the way past.
Uploads need a secure context, because the hash is computed with
crypto.subtle. That means https in production and localhost in development.
Updated at, Friday, August 28, 2026