---
title: Troubleshooting
description: The failures people actually hit, what each one means, and the fix.
---

## "Signed out every time the tab reopens"

The session is persistent by default: the refresh token lives in
`localStorage`, namespaced per environment, and a fresh page load restores the
session from it. If your users are asked to sign in again after closing a tab
or pressing back, something is interrupting that restore. In order of
likelihood:

**1. The very first request is being refused.** On load the SDK fetches
`/v1/environment` before restoring the session. If that request fails, the
restore never runs and the app renders signed out even though the session is
sitting in storage. Open the network tab and look at the first `environment`
request. A `403` with a message about your origin is the origin lock, not a
session problem; see the next section.

**2. Storage is unavailable.** In a private window, or a browser configured to
block site data, `localStorage` throws. The SDK falls back to memory, which
works perfectly and lasts exactly one tab. If your audience includes embedded
webviews or kiosk browsers, test there. You can supply your own adapter:

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

**3. You are on `@zevauth/js` 0.2.4 or older.** Those versions had exactly
this bug: the refresh on page load succeeded and then forgot who the session
belonged to, leaving a valid session in storage next to a signed-out UI.
Upgrade to 0.2.5 or later; nothing else is needed.

**4. Something else clears storage.** A logout button that calls
`localStorage.clear()`, an "accept cookies" library that wipes site data, a
service worker cache purge. The SDK's key is `zevauth.session.<environmentId>`;
if it vanishes between loads, search your codebase for who removes it.

> **Note:**
> What you never need to handle yourself: refreshing across tabs. Refresh tokens
> are single use with no grace window, so two tabs refreshing at once would burn
> the session. The SDK serialises refresh across tabs with a cross-tab lock; do
> not build your own refresh loop alongside it.

## "This publishable key is not allowed from this origin"

The origin lock, not CORS. Our API sends CORS headers for every origin;
security rests on the key and, for live keys in browsers, on this lock. It
admits three kinds of origin: your proved domain, its subdomains, and the exact
origins of your registered callbacks. Everything else is refused.

Two distinct messages:

- **"awaiting DNS verification"**: the origin matches your domain, but the
  domain's proof is missing. That happens after changing the domain, which
  clears the proof on purpose. Go to **Go live**, prove the new domain, and the
  pause lifts on its own.
- **"not allowed from this origin"**: the origin does not match at all. Either
  the domain in **Configuration → General** is not what the page runs on, or
  the URL should be added under **Domains & callbacks**.

Development keys are not origin-locked, which is why localhost always works
and the first refusal appears in production.

## "The verification code arrived but there is nowhere to type it"

Your environment sends codes, but a screen built for links is showing. Check
`GET /v1/environment` → `emailVerification.strategies`. `<SignUp />` renders
the right screen from that automatically; a custom UI should branch on it too.

## "Sign in with ZevID comes straight back with an error"

The person is returned to your callback with `?error=` and a human-readable
`error_description`; `<SignIn />` shows it automatically. The codes and what to
do about each are in
[the API reference](/api/authentication/#how-it-comes-back). The common one is
`zevid_expired`: more than 15 minutes passed across OTP, MFA and consent.

## "DNS record added but the check says not found"

A check that ran before the record existed teaches resolvers "no such name"
for up to 30 minutes. It clears on its own; press Check after adding the
record, not before. Details in
[Development and production](/guide/environments/).

## Reading an error like the API means it

Every error is `{ "error": { "code", "message" } }`. The `message` is written
for people and safe to show; the `code` is what to branch on. A `401` is the
credential, a `403` is permission (origin lock included), a `404` on a
documented path usually means the wrong host for that surface.
