> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Authentication verifies the identity of the user interacting with your storefront. Commerce Engine uses a robust token-based system (JWT) and offers flexible login methods, including passwordless options via Email, Phone, and WhatsApp.

Commerce Engine distinguishes public requests from live user sessions.

| State | How it is used | Typical capabilities |
| - | - | - |
| Public | Public SDK accessor or operation's publishable-key auth | Product browsing, categories, search, recommendations, store configuration |
| Anonymous session | Session SDK creates or restores an anonymous token pair | Cart, live session continuity, checkout preparation, session analytics |
| Logged-in session | OTP or password verification upgrades the session | Account, addresses, loyalty, orders, authenticated checkout |

Public browsing does not require anonymous authentication. Establish a live session only when the application needs session-bound behaviour or when it intentionally bootstraps one at startup.

## Recommended implementation paths

<CardGroup cols={2}>
  <Card title="Hosted Checkout" icon="bag-shopping" href="/docs/hosted-checkout/overview">
    Recommended when login is primarily needed for checkout. Hosted Checkout handles OTP, account switching, addresses, payment, and order confirmation.
  </Card>

  <Card title="Custom auth UI" icon="user-shield">
    Build this when the storefront needs login state outside checkout—for example, account, orders, loyalty, wishlist, or saved addresses.
  </Card>
</CardGroup>

## Session bootstrap

For a browser SPA:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const sdk = storefront.session();
await sdk.ensureAccessToken();
```

For Next.js, TanStack Start, Astro, and SvelteKit, mount `storefront.bootstrap()` once in a root client boundary. Returning users reuse existing cookie-backed sessions.

Do not create anonymous sessions during SSG, ISR, prerendering, or public server rendering. Use the public accessor in those contexts.

<Frame caption="Anonymous session bootstrap">
  <iframe title="Anonymous session bootstrap" allowFullScreen width="100%" height="520" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20App%20as%20Storefront%20Application%0A%20%20%20%20participant%20SDK%20as%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%20API%0A%20%20%20%20App-%3E%3ESDK%3A%20public%28%29.catalog.listProducts%28%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20Public%20catalog%20request%20%28X-Api-Key%29%0A%20%20%20%20CE--%3E%3ESDK%3A%20Products%20%28no%20user%20session%29%0A%20%20%20%20SDK--%3E%3EApp%3A%20Render%20catalog%0A%20%20%20%20App-%3E%3ESDK%3A%20First%20session-bound%20action%0A%20%20%20%20SDK-%3E%3ESDK%3A%20ensureAccessToken%28%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fauth%2Fanonymous%0A%20%20%20%20CE--%3E%3ESDK%3A%20Anonymous%20user%20%2B%20token%20pair%0A%20%20%20%20SDK-%3E%3ESDK%3A%20Persist%20tokens%0A%20%20%20%20SDK--%3E%3EApp%3A%20Continue%20session%20action" />
</Frame>

## Passwordless OTP login

Commerce Engine supports email, phone, and WhatsApp OTP flows. Set `register_if_not_exists: true` to provide one login/register entry point.

### 1. Request an OTP

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: challenge, error } = await sdk.auth.loginWithEmail({
  email: "customer@example.com",
  register_if_not_exists: true,
});

if (error || !challenge) {
  throw new Error(error?.message ?? "OTP could not be sent");
}
```

Phone and WhatsApp flows include the country code:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: challenge, error } = await sdk.auth.loginWithPhone({
  phone: "9876543210",
  country_code: "+91",
  register_if_not_exists: true,
});

if (error || !challenge) {
  throw new Error(error?.message ?? "OTP could not be sent");
}
```

Persist both `otp_token` and `otp_action` in temporary form state. They belong to this verification attempt.

### 2. Verify the OTP

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: verified, error } = await sdk.auth.verifyOtp({
  otp: userEnteredOtp,
  otp_token: challenge.otp_token,
  otp_action: challenge.otp_action,
});

if (error) throw new Error(error.message);
```

The SDK stores the new logged-in token pair when managed token storage is configured.

<Frame caption="Passwordless login and registration">
  <iframe title="Passwordless login and registration" allowFullScreen width="100%" height="760" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20User%0A%20%20%20%20participant%20App%20as%20Storefront%20Application%0A%20%20%20%20participant%20SDK%20as%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%20API%0A%20%20%20%20participant%20OTP%20as%20OTP%20Provider%0A%20%20%20%20User-%3E%3EApp%3A%20Enters%20email%2C%20phone%2C%20or%20WhatsApp%20number%0A%20%20%20%20App-%3E%3ESDK%3A%20loginWithChannel%28register_if_not_exists%3A%20true%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fauth%2Flogin%2F%7Bchannel%7D%0A%20%20%20%20CE-%3E%3EOTP%3A%20Send%20OTP%0A%20%20%20%20OTP--%3E%3EUser%3A%20Deliver%20OTP%0A%20%20%20%20CE--%3E%3ESDK%3A%20otp_token%20%2B%20otp_action%0A%20%20%20%20SDK--%3E%3EApp%3A%20Show%20OTP%20form%0A%20%20%20%20User-%3E%3EApp%3A%20Enters%20OTP%0A%20%20%20%20App-%3E%3ESDK%3A%20verifyOtp%28otp%2C%20otp_token%2C%20otp_action%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fauth%2Fverify-otp%0A%20%20%20%20alt%20OTP%20valid%0A%20%20%20%20%20%20%20%20CE-%3E%3ECE%3A%20Login%20or%20complete%20registration%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Logged-in%20user%20%2B%20new%20token%20pair%0A%20%20%20%20%20%20%20%20SDK-%3E%3ESDK%3A%20Replace%20anonymous%20tokens%20automatically%0A%20%20%20%20%20%20%20%20SDK--%3E%3EApp%3A%20Authenticated%20session%2C%20cart%20preserved%0A%20%20%20%20else%20OTP%20invalid%20or%20expired%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Validation%20error%0A%20%20%20%20%20%20%20%20SDK--%3E%3EApp%3A%20Allow%20retry%20or%20resend%0A%20%20%20%20end" />
</Frame>

### UX requirements

A production OTP flow should include:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[ ] clear contact destination
[ ] resend with rate-limit/countdown state
[ ] expired and incorrect OTP messages
[ ] loading and duplicate-submit protection
[ ] editable email/phone before verification
[ ] post-login query invalidation
[ ] accessible segmented or single OTP input
```

Resend by requesting a new challenge and replacing the old `otp_token` and `otp_action`.

## Anonymous cart continuity

When an anonymous user logs in, Commerce Engine merges the active anonymous cart into the account session. Do not:

* create a new cart after login
* copy cart items client-side
* discard the returned token pair

Refresh or invalidate the cart query after successful verification so the UI displays the merged state.

## Password authentication

Password login is available for stores that enable it:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.auth.loginWithPassword({
  email: "customer@example.com",
  password,
});

if (error) throw new Error(error.message);
```

Password registration and reset can require OTP verification. Retrieve the exact operation contract from the LLM-first reference before building those flows rather than assuming one common request shape.

## Reading authentication state

Use token helpers for fast UI state without an extra API call:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const userInfo = await sdk.getUserInfo();
const isLoggedIn = await sdk.isLoggedIn();
const isAnonymous = await sdk.isAnonymous();
const userId = await sdk.getUserId();
const customerId = await sdk.getCustomerId();
```

Use these values for presentation and route decisions. Server-side authorization must still rely on the request-bound session and the API operation result.

## Automatic refresh and session recovery

With managed storage, the SDK refreshes expired access tokens, persists rotated tokens, and retries the original operation once. Concurrent requests should share one refresh rather than launching several refresh calls.

<Frame caption="Automatic token refresh">
  <iframe title="Automatic token refresh" allowFullScreen width="100%" height="640" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20App%20as%20Storefront%20Application%0A%20%20%20%20participant%20SDK%20as%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%20API%0A%20%20%20%20App-%3E%3ESDK%3A%20Session-bound%20API%20operation%0A%20%20%20%20SDK-%3E%3ECE%3A%20Request%20with%20access%20token%0A%20%20%20%20CE--%3E%3ESDK%3A%20401%20token%20expired%0A%20%20%20%20SDK-%3E%3ESDK%3A%20Deduplicate%20concurrent%20refreshes%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fauth%2Frefresh-token%0A%20%20%20%20alt%20Refresh%20succeeds%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Rotated%20token%20pair%0A%20%20%20%20%20%20%20%20SDK-%3E%3ESDK%3A%20Persist%20new%20tokens%0A%20%20%20%20%20%20%20%20SDK-%3E%3ECE%3A%20Retry%20original%20request%20once%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Successful%20response%0A%20%20%20%20%20%20%20%20SDK--%3E%3EApp%3A%20Operation%20result%0A%20%20%20%20else%20Refresh%20rejected%0A%20%20%20%20%20%20%20%20CE--%3E%3ESDK%3A%20Invalid%20or%20expired%20refresh%20token%0A%20%20%20%20%20%20%20%20SDK--%3E%3EApp%3A%20Enter%20session%20recovery%0A%20%20%20%20end" />
</Frame>

After a long absence, recovery should restore a usable anonymous session without blocking public browsing. Protected routes can then ask the customer to authenticate again.

<Frame caption="Session recovery after a long absence">
  <iframe title="Session recovery after a long absence" allowFullScreen width="100%" height="560" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20App%20as%20Storefront%20Application%0A%20%20%20%20participant%20SDK%20as%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%20API%0A%20%20%20%20App-%3E%3ESDK%3A%20Bootstrap%20returning%20session%0A%20%20%20%20SDK-%3E%3ECE%3A%20Refresh%20stored%20session%0A%20%20%20%20CE--%3E%3ESDK%3A%20Refresh%20rejected%0A%20%20%20%20SDK-%3E%3ECE%3A%20Establish%20anonymous%20session%0A%20%20%20%20CE--%3E%3ESDK%3A%20Anonymous%20user%20%2B%20new%20token%20pair%0A%20%20%20%20SDK-%3E%3ESDK%3A%20Persist%20recovered%20session%0A%20%20%20%20SDK--%3E%3EApp%3A%20Public%20and%20cart-capable%20anonymous%20state%0A%20%20%20%20alt%20Protected%20route%20requested%0A%20%20%20%20%20%20%20%20App-%3E%3EApp%3A%20Redirect%20to%20login%2C%20preserve%20return%20URL%0A%20%20%20%20else%20Public%20or%20cart%20route%0A%20%20%20%20%20%20%20%20App-%3E%3EApp%3A%20Continue%20storefront%0A%20%20%20%20end" />
</Frame>

## User versus customer ID

For many B2C stores, user and customer IDs align. B2B accounts can contain multiple users under one customer.

Prefer SDK overloads that resolve identity from the active session:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: addresses } = await sdk.customer.listAddresses();
const { data: orders } = await sdk.order.listOrders();
const { data: cart } = await sdk.cart.getUserCart();
```

Auth profile methods may require an explicit user ID:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const userId = await sdk.getUserId();
if (!userId) throw new Error("No active user");

const { data, error } = await sdk.auth.getUserDetails({ id: userId });
```

Do not accept an arbitrary browser-supplied user ID for a protected server operation when the SDK can infer the authenticated identity.

## Profile updates

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const userId = await sdk.getUserId();
if (!userId) throw new Error("Login required");

const { data, error } = await sdk.auth.updateUserDetails(
  { id: userId },
  { first_name: "Asha", last_name: "Sharma" },
);

if (error) throw new Error(error.message);
```

Changing a verified email or phone may require a dedicated OTP action. Confirm the exact operation and verification sequence from the current LLM reference.

## Logout

Logout downgrades the logged-in session to a new anonymous session linked to the continuing journey.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error } = await sdk.auth.logout();
if (error) throw new Error(error.message);
```

With managed session storage, the SDK stores the returned anonymous tokens. Do not call `clearTokens()` after a successful logout.

After logout:

* remove protected account data from local caches
* invalidate cart/session queries
* update header UI from the new anonymous user state
* redirect away from protected routes

The anonymous cart and session identity can continue.

<Frame caption="Logout with anonymous continuity">
  <iframe title="Logout with anonymous continuity" allowFullScreen width="100%" height="620" src="https://mermaid.tarkai.dev/embed?definition=sequenceDiagram%0A%20%20%20%20participant%20User%0A%20%20%20%20participant%20App%20as%20Storefront%20Application%0A%20%20%20%20participant%20SDK%20as%20Storefront%20SDK%0A%20%20%20%20participant%20CE%20as%20Commerce%20Engine%20API%0A%20%20%20%20participant%20Checkout%20as%20Hosted%20Checkout%0A%20%20%20%20User-%3E%3EApp%3A%20Clicks%20Log%20out%0A%20%20%20%20App-%3E%3ESDK%3A%20auth.logoutUser%28%29%0A%20%20%20%20SDK-%3E%3ECE%3A%20POST%20%2Fauth%2Flogout%0A%20%20%20%20CE-%3E%3ECE%3A%20Invalidate%20logged-in%20refresh%20token%0A%20%20%20%20CE--%3E%3ESDK%3A%20Same%20user%20anonymous%20%2B%20new%20token%20pair%0A%20%20%20%20SDK-%3E%3ESDK%3A%20Store%20returned%20anonymous%20tokens%0A%20%20%20%20SDK--%3E%3EApp%3A%20Update%20user%20state%20from%20response%0A%20%20%20%20SDK--%3E%3ECheckout%3A%20onTokensUpdated%20-%3E%20updateTokens%28%29%0A%20%20%20%20App-%3E%3EApp%3A%20Clear%20protected%20caches%2C%20preserve%20cart%20continuity" />
</Frame>

## Hosted Checkout authentication

### Checkout-only integration

When Hosted Checkout is the only Commerce Engine integration, use its default managed mode. Checkout owns anonymous bootstrap, OTP login, refresh, and logout.

### Storefront SDK plus Hosted Checkout

When the app also uses `@commercengine/storefront`, the Storefront SDK is the session owner. Use `authMode: "provided"` and two-way token synchronization.

This requirement applies even when the app has no custom auth form. Otherwise the SDK and checkout create separate users/carts.

See [Hosted Checkout configuration](/docs/hosted-checkout/configuration#recommended-token-sync-pattern-storefront-sdk--hosted-checkout).

## Route protection

Protect account routes using the framework's request-bound session accessor where possible.

A useful guard distinguishes:

* no session / anonymous → redirect to login
* logged-in session → render
* upstream failure → error/retry state, not automatic login redirect

Do not treat every API error as proof the user is logged out.

## Token storage and security

Use the storage model designed for the runtime:

* browser SPA: `BrowserTokenStorage`
* first-party SSR wrapper: built-in aligned cookie storage
* custom SSR/BFF: request-bound `ServerTokenStorage` or server-side session storage
* Node.js: request- or user-isolated storage

The Storefront API key is publishable. Private admin keys, payment credentials, and webhook secrets remain server-only.

Never log access tokens, refresh tokens, OTPs, passwords, or full authentication responses.

## Direct REST integration

Direct API clients must manage:

* `/auth/anonymous` session creation
* Bearer tokens
* refresh and retry
* response `content` envelopes
* token replacement after login, refresh, and logout

Use the generated API reference for exact headers and operation contracts. SDK examples access unwrapped `data` and must not be copied into a direct REST client unchanged.

## Production checklist

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[ ] public pages do not create sessions unnecessarily
[ ] one root session bootstrap for live flows
[ ] OTP challenge state includes token and action
[ ] resend replaces the previous challenge
[ ] login preserves/merges the anonymous cart
[ ] logout stores returned anonymous tokens
[ ] protected caches cleared after logout
[ ] route guards distinguish anonymous state from upstream failure
[ ] Storefront SDK and Hosted Checkout share one session
[ ] tokens, OTPs, and passwords are never logged
[ ] auth journeys tested in the production build
```

<Card title="Exact auth operations" icon="robot" href="https://llm-docs.commercengine.io/sdk/storefront">
  Retrieve current auth method signatures and types with a `.md` URL or `Accept: text/markdown` before implementation.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.