ZevAuth
Sign up

Concepts

Modelling your users

Separating your staff from your customers, and giving people types like seller, supplier or admin.

Most applications have more than one kind of person in them: customers and staff, or buyers and sellers, or clients and suppliers. This page is about which of those differences belongs in a separate user pool and which belongs inside one.

Two questions decide it:

  1. Should these people be able to sign in to the same place? If your staff sign in at admin.yourapp.com and your customers at yourapp.com, and no account should ever work on both, they are different pools.
  2. Do they belong to companies? If a person acts on behalf of an organisation with its own members and its own permissions, that is what organizations are for.

Your staff: a separate project

Use a second project for your internal admin or back office.

A project’s environment is a hard tenant boundary. Separate user pool, separate keys, separate issuer, separate JWKS. An account in one cannot sign in to the other, and a token minted for one fails verification against the other, not by policy but because the signature does not check out.

yourapp                → your customers
yourapp-admin          → your staff

That is the strongest separation we offer, and it is the right default for staff. It means a bug in your customer sign-up flow cannot create a staff account, and a leaked customer key does nothing to your admin tools.

Inside the admin project, use publicMetadata for seniority:

await zevauth.users.update('usr_...', {
  publicMetadata: { role: 'support', canRefund: false },
});

Kinds of user in one pool: publicMetadata

When everybody signs in at the same place but behaves differently (buyers and sellers on a marketplace, clients and suppliers on a procurement tool), keep one pool and give people a type.

await zevauth.users.update('usr_...', {
  publicMetadata: { type: 'seller', verifiedAt: '2026-01-01' },
});

publicMetadata travels inside the access token, so your backend reads it from the verified JWT with no call to us:

{
  "sub": "usr_...",
  "iss": "https://api.zevauth.net/v1/env_live_...",
  "public_metadata": { "type": "seller" }
}
const { payload } = await jwtVerify(token, jwks, { issuer, audience });
if (payload.public_metadata?.type !== 'seller') return forbidden();

Why this is safe to trust

publicMetadata is writable only with a secret key. The end user’s own client cannot change it: there is no field for it on PATCH /v1/me, by design. So a value you set on your server is a value only your server set.

The three scopes exist precisely so this question has an answer:

ScopeWho can readWho can writeUse for
publicMetadataThe user’s client, and your backend, via the tokenYour backend onlyTypes, roles, entitlements: anything an authorization decision reads
privateMetadataYour backend onlyYour backend onlyInternal references, a Stripe customer id, risk flags
unsafeMetadataAnyone with the tokenThe user’s own clientPreferences. Theme, language, a dismissed banner

Fetching one kind of user

publicMetadata is filterable, so a type you set is a type you can list:

const sellers = await zevauth.users.list({ metadata: { type: 'seller' } });
GET /v1/users?metadata[type]=seller

Keys combine with AND, and totalCount reflects the filter rather than your whole userbase. For a list you need constantly, keep your own table keyed by our user id and maintain it from webhooks instead of polling this.

People who belong to companies: organizations

If a seller is really a company with several staff, and those staff need different powers, that is an organization rather than a type.

await zevauth.organizations.create({ name: 'Acme Supplies', createdBy: userId });

The session then carries the organization and what that person may do inside it:

{
  "org_id": "org_...",
  "org_role": "org:admin",
  "org_permissions": ["org:members:manage", "invoices:approve"]
}

You define your own roles and permissions per environment in the console, so supplier:admin with invoices:approve is yours to create. See Organizations.

Choosing

You wantUse
Staff who must never sign in as customersA second project
Buyers and sellers signing in at one placepublicMetadata.type
Users acting for a company, with their own membersOrganizations
A back-office rank: support, finance, superadminpublicMetadata inside the admin project
A user preferenceunsafeMetadata

The mistake worth avoiding is using metadata where you needed a separate project. If an account should never be able to sign in to the other surface, a field on a shared user is one bug away from letting it.

A worked example

A marketplace with customers, sellers, and internal support staff:

Project market: everybody who shops or sells.

// A shopper. No type at all is fine; absent means customer.
await zevauth.users.create({ email: 'ada@example.com' });

// A seller, once they finish onboarding.
await zevauth.users.update(sellerId, {
  publicMetadata: { type: 'seller' },
  privateMetadata: { payoutAccount: 'acct_...' },
});

// A seller that is a business with staff.
await zevauth.organizations.create({ name: 'Acme', createdBy: sellerId });

Project market-admin: support and finance. Different keys, different sign-in page, different tokens.

await zevauth.users.update(agentId, {
  publicMetadata: { role: 'support' },
});

Your admin backend verifies against the admin project’s issuer, so a customer token presented to it fails on the signature. Nothing has to remember to check.

Updated at, Wednesday, October 21, 2026