---
title: Use cases
description: Complete, working setups for the shapes most products actually need.
---

Reference pages answer "what does this field do". This page answers "how
do I build the thing I came here to build". Each one is a whole setup:
what to switch on in the console, what to write in code, and what it
gets you.

## A SaaS that confirms every address before letting anyone in

Financial tools, anything with payouts, anything where an unconfirmed
address is a liability.

**In the console**

1. **Authentication → Identifiers → Email**: Verify the address
   **At sign-up**.
2. Turn **Let them in before it is confirmed** off.

**In code**

```tsx
import { ZevAuthProvider, SignIn, SignUp } from '@zevauth/react';

<ZevAuthProvider publishableKey={import.meta.env.VITE_ZEVAUTH_PUBLISHABLE_KEY}>
  <SignUp afterSignUpUrl="/welcome" />
</ZevAuthProvider>
```

**What happens.** Sign-up creates the account and issues **no session**.
`<SignUp />` shows the confirmation screen, with a code field when your
environment sends codes and link instructions when it sends links.

If they close the tab and come back a week later, signing in is refused
with `email_not_verified` and we send a fresh message. They are returned
to the confirmation screen rather than stranded with an expired link.

> **Note:**
> Enforcement is server side. No session exists, so there is no token to
> present to your API. A policy enforced only in the browser is not
> enforced: an access token works against the API whatever the UI renders.

## A product that lets people in, then nags them

Marketplaces, social products, anything where friction at sign-up costs
you more than an unconfirmed address does.

**In the console**

1. Verify the address **At sign-up**.
2. Leave **Let them in before it is confirmed** on.

**In code**

```tsx
import { SignedIn, EmailVerified, EmailUnverified } from '@zevauth/react';

<SignedIn>
  <EmailUnverified>
    <Banner>
      Confirm your email to enable payouts. <ResendButton />
    </Banner>
  </EmailUnverified>

  <EmailVerified>
    <PayoutSettings />
  </EmailVerified>
</SignedIn>
```

Resending is one call:

```ts
await zevauth.signUp.resendVerification({ email: user.email });
```

**On your own API**, read the claim from the token you already verify:

```ts
const { payload } = await jwtVerify(token, jwks, { issuer, audience });
if (!payload.email_verified) {
  return Response.json({ error: 'Confirm your email first.' }, { status: 403 });
}
```

> **Warning:**
> `<EmailVerified>` decides what to render, not what is allowed. Hiding a
> button does not stop a request. Every endpoint that needs a confirmed
> address has to check `email_verified` itself.

## Sign in with ZevID, tested on localhost

The ZevID button needs one thing beyond being switched on, and it is the thing
almost everyone misses on the first try.

**In the console**

1. **Authentication → Social providers**: turn **ZevID** on.
2. **Project settings → Domains & callbacks**: add where the person should land
   afterwards. For local work that is your dev server, for example
   `http://localhost:5173/`. Development environments accept `http` on
   loopback; production must be `https`.

**In code**

```tsx
<SignIn afterSignInUrl="/dashboard" />
```

That is all. The button redirects to the consent screen and back to the
registered callback, where the SDK exchanges the returned code for a session on
your own origin.

**If the person comes straight back without a session**, they arrived with
`?error=` on your callback. `<SignIn />` already shows why; the codes are in
[the API reference](/api/authentication/#how-it-comes-back). The common one is
`zevid_expired`: they took more than 15 minutes across OTP, MFA and consent.
Offer the button again.

**If you see `No callback is registered for this environment`**, step 2 is
missing. If you see a URL named as *not a registered callback*, it does not
match an entry exactly: check the scheme, the port and the trailing slash.

## A marketplace with buyers, sellers and staff

**Staff go in a second project.** An environment is a hard tenant
boundary: separate user pool, separate keys, separate issuer. A customer
token presented to your admin API fails on the signature, not on a check
somebody has to remember to write.

**Buyers and sellers share one project**, typed with metadata:

```ts
await zevauth.users.update(userId, { publicMetadata: { type: 'seller' } });
```

The type travels in the access token, so your API reads it with no extra
call, and lists come back filtered:

```ts
const { data } = await zevauth.users.list({ metadata: { type: 'seller' } });
```

`publicMetadata` is writable only with a secret key, so a value you set
is a value only your server set. Full reasoning in
[Modelling your users](/concepts/modelling-users/).

## A sign-in page that looks like the rest of the product

The default is a centered card. This is the split screen most products
in this category ship:

**In the console**: **Customisation → Layout → Form right**, then pick a
**Gradient** panel and give it a colour and a line of copy.

**Or entirely in code**:

```tsx
<SignIn
  layout="split-right"
  aside={
    <div style={{ padding: 48, color: 'white' }}>
      <blockquote>"We moved 40,000 accounts in an afternoon."</blockquote>
      <cite>Ada Lovelace, CTO</cite>
    </div>
  }
/>
```

The panel hides below 900px and the form takes the whole screen, because
a stacked picture pushes the form below the fold on a phone.

## A form embedded in a page you have already designed

You have a header, a panel and a font. Our card inside your panel is a
box inside a box:

```tsx
<ZevAuthProvider
  publishableKey="pk_live_..."
  appearance={{
    cardStyle: 'flat',        // no border, background or padding
    showIdentity: false,      // your logo is already above it
    fontFamily: 'inherit',    // your font
    contentAlign: 'top',      // clears your fixed header
    signInTitle: 'Welcome back',
  }}
>
  <MyHeader />
  <main className="my-panel">
    <SignIn secondaryMethods="collapsed" />
  </main>
</ZevAuthProvider>
```

`secondaryMethods="collapsed"` puts the email and password form first and
folds social sign-in behind **More options**, which is what you want when
most of your users are returning customers with a password.

## A B2B app where people belong to companies

```ts
const { organization } = await zevauth.organizations.create({
  name: 'Acme Supplies',
  createdBy: userId,
});
```

The session then carries `org_id`, `org_role` and `org_permissions`. Use
the claims to decide what to render, and re-read the membership before
anything that changes data: a token minted before somebody was demoted
still carries the old permissions until it refreshes.

```tsx
<OrganizationSwitcher />
```

See [Organizations](/sdk/organizations/).

## Testing all of it

```ts
// A user, and a token, with no password anywhere in your fixtures.
const user = await zevauth.users.create({ email: 'ada@example.com' });
const { accessToken } = await zevauth.sandbox.mintToken(user.id);

// What we would have emailed, including the code.
const { data } = await zevauth.sandbox.emails({ to: 'ada@example.com' });
await zevauth.signUp.verifyCode({ email: 'ada@example.com', code: data[0].code });

// Between CI runs.
await zevauth.sandbox.resetUsers();
```

Test environments only, and the API refuses every one of these with a
live key. See [Testing](/guide/testing/).
