---
title: JavaScript
description: The framework-agnostic client.
---

import Callout from '../../../components/Callout.astro';


```bash
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.

```ts
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](/concepts/hosted-pages) if the URL carries one. Call it
once at start-up; calling a flow before it resolves is fine.

## Signing in

```ts
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.

<Callout type="info">
`startMagicLink` and `startEmailCode` answer identically whether or not the
address belongs to anybody. That is deliberate: a different response would turn
either into a way to test whether somebody has an account with you.
</Callout>

## Signing up

```ts
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

```ts
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:

```ts
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:

```ts
createZevAuth({
  publishableKey: 'pk_test_...',
  storage: { get, set, remove },
});
```

<Callout type="warning">
Never refresh a session yourself, in parallel with the SDK. Refresh tokens
rotate and a token presented twice is treated as theft, which ends the session
on every device. The SDK serialises refreshes across tabs with a lock for
exactly this reason.
</Callout>

## 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.

```ts
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.

```ts
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.

<Callout>
`imageUrl` still accepts any https URL. If you host your users' pictures
yourself, or their picture came from a social login, nothing changes. Uploading
is an addition, not a replacement.
</Callout>

Uploads need a secure context, because the hash is computed with
`crypto.subtle`. That means https in production and `localhost` in development.