ZevAuth Docs
Sign up

Building with the SDK

Backend

Manage users and organizations from your server, with a secret key.

npm install @zevauth/backend
import { createZevAuthBackend } from '@zevauth/backend';

const zevauth = createZevAuthBackend({
  secretKey: process.env.ZEVAUTH_SECRET_KEY!,
});

const user = await zevauth.users.create({
  email: 'ada@example.com',
  publicMetadata: { plan: 'pro' },
});

Users

// One page. `next` is opaque: pass it back as `after` to continue.
const page = await zevauth.users.list({ limit: 50, query: 'ada@' });

const user = await zevauth.users.get('usr_...');

Creating

Creates the account directly. No sign-up flow runs and no email is sent.

await zevauth.users.create({
  email: 'ada@example.com',
  password: 'optional',
  emailVerified: true,
  publicMetadata: { plan: 'pro' },
  privateMetadata: { stripeCustomerId: 'cus_...' },
});

Two fields here are only available to a backend, and that is the reason this client needs a secret key:

  • emailVerified: true trusts the address without proving it. That is the migration case: the system you are replacing already confirmed it. Reachable from a browser it would make verification decorative.
  • password is optional. An account without one still signs in by magic link or a social provider, which is usually what an import wants — create the accounts, let people set a password when they first arrive.

Updating

await zevauth.users.update('usr_...', {
  firstName: 'Augusta',
  publicMetadata: { plan: 'enterprise' },
});

Omitting a field leaves it alone. Sending null clears it, which is a different thing: most patches touch a name and should not clear an avatar on the way past.

Setting password here does not revoke sessions. A person changing their own password is usually reacting to a suspicion and wants every other device gone; a backend setting one is usually provisioning, and signing everybody out mid-migration would be its own outage. Use revokeSessions when that is the intent.

Blocking, revoking and deleting

const { sessionsRevoked } = await zevauth.users.setBlocked('usr_...', true);
await zevauth.users.revokeSessions('usr_...');
await zevauth.users.delete('usr_...');

Blocking revokes every session the person holds, in the same transaction as the status change. That is the point of it: an access token lives for minutes and a refresh token for weeks, so an account “blocked” without revocation stays signed in on every device it was already signed in on — which is every device that matters, if the reason for blocking is that somebody else has one.

delete is a real delete, not a flag. Somebody exercising a right to erasure is not served by a row that still holds their address. Sessions, identities and organization memberships go with it.

Organizations

const { organization } = await zevauth.organizations.create({
  name: 'Acme Corp',
  createdBy: 'usr_...',   // becomes the owner
});

const detail = await zevauth.organizations.get(organization.id);
await zevauth.organizations.update(organization.id, { name: 'Acme' });
await zevauth.organizations.addMember(organization.id, { userId: 'usr_...' });
await zevauth.organizations.removeMember(organization.id, 'usr_...');
await zevauth.organizations.delete(organization.id);

There is no permission check on these, and that is deliberate. The equivalent end-user calls re-read the caller’s live org:manage membership, because the caller is a person whose standing can change. This caller is your backend holding a secret key: it owns the environment, so there is no membership to consult.

Deleting an organization removes it and its memberships. The people survive — deleting users because the company they belonged to was deleted would be a data-loss bug wearing a cascade.

Retries

Requests that could not be answered are retried automatically: a dropped connection, a timeout, a 429, or a 502/503/504.

A bare 500 is not retried. Our code ran and may have written something before it failed, so repeating it is how one create becomes two records — in a nightly job, silently, with nobody watching. Nothing 4xx is retried either; it would fail identically forever.

createZevAuthBackend({
  secretKey: process.env.ZEVAUTH_SECRET_KEY!,
  timeoutMs: 15_000,   // one attempt gives up after this
  maxRetries: 2,
});

Errors

Everything throws ZevAuthError, carrying the message the API returned, a code, and the HTTP status.

import { ZevAuthError } from '@zevauth/backend';

try {
  await zevauth.users.get('usr_nope');
} catch (err) {
  if (err instanceof ZevAuthError && err.status === 404) {
    // ...
  }
}

A user belonging to another environment is a 404, not a 403. Telling the two apart would confirm that a guessed id belongs to somebody, which is a membership oracle across tenants.

Updated at, Friday, August 28, 2026