Guides · November 17, 2022
Auth & Customer Accounts in Headless Commerce
Headless commerce authentication means treating login, sessions, and customer accounts as a separate system from your storefront UI, connected through an API. The reliable pattern is short-lived tokens in httpOnly cookies, server-side session verification at the edge, and a clear split between what your commerce backend owns and what your auth provider owns.
By Polo Themes
Headless commerce authentication means treating login, sessions, and customer accounts as a separate system from your storefront UI, connected through an API rather than baked into a monolithic theme. The reliable pattern that has emerged across Next.js storefronts is: issue short-lived tokens, store them in httpOnly cookies rather than client-side JavaScript, verify sessions on the server for every protected request, and keep a clean boundary between what your commerce backend owns (customer records, order history, addresses) and what your auth layer owns (credentials, MFA, social login). Get that boundary wrong and you end up with either a security hole or a UX that logs people out for no reason.
This guide walks through the actual mechanics: how customer accounts differ from generic app auth, the token and cookie decisions that matter, how this looks concretely in a Next.js App Router storefront, and how the two most common headless commerce backends — Medusa and Shopify's Customer Account API — handle it today. It ends with a build-vs-buy framework, because for a lot of teams the right call is not to hand-roll any of this.
Why Commerce Auth Is Its Own Problem
Generic app authentication mostly cares about one question: is this person who they say they are? Commerce auth has to answer that plus a second question that generic auth libraries were never designed for: what is this person allowed to see and act on right now, given that carts, orders, and payment methods carry real financial and legal weight. A logged-out visitor still needs a working cart. A logged-in customer needs their cart to merge with whatever they had before logging in. An order page needs to refuse access to anyone but the customer who placed it (and staff, through a completely different auth path). None of that is exotic, but it is easy to get subtly wrong if you bolt a generic auth library onto a storefront without thinking about the commerce-specific state machine underneath it.
The practical implication is that "add login" is not really the task. The task is: decide where sessions live, decide how the cart identity relates to the customer identity, decide what happens at checkout for a guest, and decide how account data flows from your commerce backend into server-rendered pages without leaking it to the client bundle. Get the plumbing right first, then the UI (which is the easy part) sits on top of it cleanly.
Tokens, Sessions, and Where They Should Live
Most headless commerce backends authenticate customers with some form of bearer token — a JWT or an opaque session token exchanged for credentials at login. The question that actually matters for security is not JWT-vs-opaque, it is: where does that token live between requests? There are three common answers, and only one of them is good practice for a production storefront.
- localStorage or client-side state: convenient to wire up, and wrong for anything beyond a prototype. Any script running on your page — including a compromised third-party tag — can read localStorage, which turns any XSS bug into full account takeover.
- A JavaScript-readable cookie: marginally better than localStorage in some edge cases, but still exposed to the same XSS class of attack since client-side code can read it.
- An httpOnly, Secure, SameSite cookie set by your server: the token never becomes visible to page JavaScript at all. Your Next.js server (route handlers, server actions, middleware) reads it on the request and forwards it to your commerce API; the browser just carries it along automatically. This is the pattern to default to.
Pair httpOnly cookies with short-lived access tokens and a longer-lived refresh token, rotated on use. If your commerce backend's auth module issues a JWT with a fifteen-minute or one-hour expiry, your server layer should silently refresh it in the background using the refresh token rather than forcing a re-login — customers abandon carts fast when they get logged out mid-checkout for no visible reason. Set SameSite to Lax at minimum for the session cookie so normal navigation and payment-provider redirects still work, and never set SameSite to None unless you have a specific cross-site embed requirement and have thought through the CSRF implications that come with it.
The Next.js Pattern: Server-First, Not Client-First
In a Next.js App Router storefront, the cleanest implementation keeps almost all of the auth logic on the server. A typical shape looks like this: a login form posts to a server action, which calls your commerce backend's auth endpoint, receives a token, and sets it as an httpOnly cookie via the response — the client component never touches the token directly. Protected routes (account, order history, saved addresses) read the session cookie in a server component or in middleware, verify it against the backend (or verify a signed JWT locally when the backend supports that), and redirect unauthenticated requests before any protected data is fetched. This is meaningfully different from older client-side auth patterns where a token sat in a global store and every component decided for itself whether to render — that approach leaks a flash of protected UI before the redirect fires, and it means the actual authorization check only ever happens in the browser.
Middleware is a good place for the cheap check — is there a session cookie at all — but treat it as a routing convenience, not your security boundary. The real authorization check belongs at the data-fetching layer, right where you call the commerce API for order details or account data, because that is the one place that cannot be bypassed by a client-side route change or a direct API call. A customer editing the URL to try to view another shopper's order-detail page should get refused by the server component that fetches that order, not just by a redirect that happened to run on the way in.
Guest checkout deserves its own note here. Most commerce backends generate an anonymous cart or customer record for unauthenticated shoppers, then need a merge step if that shopper logs in or creates an account mid-session. Design this explicitly rather than leaving it as an afterthought — decide up front whether login merges the guest cart into the account cart, replaces it, or asks the customer, because all three are defensible product decisions but only one of them matches what your QA team will actually test.
How This Looks in Medusa
Medusa's auth module issues JWTs scoped to an actor type — customer, admin, or a custom actor you define — and exchanges them through its auth workflows (email/password by default, with providers available for social login). The practical pattern for a Next.js storefront talking to Medusa is: call the auth endpoint from a server action, receive the JWT, set it as an httpOnly cookie, and pass it as a bearer token on subsequent server-side calls to Medusa's store API for cart, order, and customer data. Because Medusa's customer and order data lives behind the same query layer regardless of storefront, this is also where a lot of teams get tripped up mixing up admin-scoped and store-scoped API calls — keep those two token types (and the routes that use them) firmly separated, since a customer JWT should never be usable against admin endpoints and vice versa.
How This Looks with Shopify's Customer Account API
Shopify moved customer accounts to an OAuth-based flow with its Customer Account API, replacing the older classic customer accounts model. That means a headless Shopify storefront hands off login to a Shopify-hosted account page via OAuth, then receives an authorization code it exchanges server-side for tokens — closer to "Login with Shopify" than a form your storefront owns outright. If you're building a Shopify-backed storefront (as opposed to a themed Online Store 2.0 setup, which is the layer our own Shopify theme catalog lives on), budget real time for the OAuth redirect handling, token storage, and the account-page-versus-custom-UI decision that comes with it — it is a different shape of problem than wiring up a self-hosted commerce backend's auth module.
Social Login and Passwordless Options
Social login (Google, Apple, etc.) and passwordless flows (magic links, one-time codes) reduce password-reset support volume and generally improve conversion on account creation, since customers skip inventing yet another password. The tradeoff is that both add a dependency on an external identity provider being reachable during checkout — a Google outage or a slow email deliverability window becomes your outage. If you offer either, always keep a fallback path (email/password, or at minimum a way to retry) rather than making a third-party identity provider a hard requirement to complete a purchase.
Build vs. Buy: When to Reach for Clerk, Auth0, or WorkOS
Not every storefront needs custom auth code. If your commerce backend's built-in customer auth covers what you need — email/password or magic link, basic session handling, no enterprise SSO requirement — building directly on it is usually the right call; it's one fewer system to operate and one fewer place customer PII lives. Reach for a dedicated identity provider (Clerk, Auth0, WorkOS, or similar) when you need things commerce backends generally don't ship well out of the box: enterprise SSO for a B2B storefront, fine-grained MFA policy, passkeys, or a shared login across multiple storefronts and apps under one brand. In that setup, the identity provider issues the session and your commerce backend becomes a data source keyed off a stable customer ID that the provider hands you — which is a clean architecture, but it does mean designing the mapping between "identity provider user" and "commerce backend customer record" carefully, since that link is what breaks silently if either side changes an ID scheme later.
A Practical Checklist
- Session tokens live in httpOnly, Secure cookies — never in localStorage or a client-readable cookie.
- Access tokens are short-lived; refresh happens silently server-side, not by forcing a re-login.
- Authorization checks happen at the data-fetching layer (server components, route handlers), not only in middleware or client-side route guards.
- Guest cart-to-account merge behavior is an explicit, tested decision — not incidental behavior.
- Admin-scoped and customer-scoped tokens never share a code path.
- Social login and passwordless flows have a fallback path so a third-party outage never blocks checkout.
- PII (order history, addresses, saved payment references) is fetched server-side and passed to client components as already-scoped data — not fetched client-side with a customer ID that a browser could swap.
Frequently Asked Questions
Is a JWT more secure than an opaque session token for customer auth?
Not inherently — both are fine when stored correctly. The security question that actually matters is where the token lives (httpOnly cookie versus client-readable storage) and how quickly a stolen token expires, not whether it's a JWT or an opaque string.
Do I need middleware to protect account pages in Next.js?
Middleware is useful as a fast, cheap first check and for redirecting unauthenticated requests early, but treat it as routing convenience rather than your security boundary. The real check belongs at the point where you fetch protected data, since that is the one place a client-side route change cannot bypass.
Should I build customer accounts myself or use an identity provider?
If your commerce backend's built-in customer auth already covers your requirements — standard email/password or magic-link login, no enterprise SSO — building on it directly is usually simpler and keeps customer data in one system. Bring in a dedicated identity provider when you need enterprise SSO, passkeys, or a shared login across multiple properties.
How does guest checkout interact with customer accounts?
Most commerce backends create an anonymous cart or customer record for guests, which then needs an explicit merge decision if the shopper logs in or registers mid-session. Decide and test that merge behavior deliberately rather than leaving it as incidental behavior of whatever auth library you pick.
For more on how headless and design-system decisions fit together across a storefront build, browse the rest of the Polo Themes blog — and if you're assembling the account and checkout UI itself, our Figma UI kits are a solid starting point for the screens (login, account, order history) this auth layer needs to sit behind.