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: truetrusts 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.passwordis 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