Authentication
Built-in customer authentication with better-auth and Shopify Customer Account API OIDC.
The template includes built-in customer authentication using better-auth with the Shopify Customer Account API. When configured, customers can sign in via Shopify OIDC, view their profile, order history, and address book.
Authentication is opt-in via environment variables. Without them, the storefront works as a guest-only experience and no auth UI is rendered.
Configuration
Set the feature flag and three server-only secrets:
next.config.ts throws at build time if NEXT_PUBLIC_ENABLE_AUTH="1" is set without the three secrets. The flag must be NEXT_PUBLIC_ so its value is available identically on server and client — probing the server-only secrets directly inside a component causes hydration mismatches under cache components.
Generate the auth secret with openssl rand -base64 32. The client ID and secret come from Shopify's Customer Account API.
Shopify Admin setup
- Go to Shopify Admin → Settings → Customer accounts
- Enable Customer Account API
- Create a Customer Account API client (under "API clients")
- Set the redirect URI to
{YOUR_DOMAIN}/api/auth/callback/shopify - Copy the client ID and client secret to your environment variables
- Ensure the store domain matches
SHOPIFY_STORE_DOMAIN
See Environment Variables for the full variable reference.
How it works
Authentication is built on these modules:
| Module | Purpose |
|---|---|
lib/auth/index.ts | Universal isAuthEnabled flag. Safe to import from server and client code. |
lib/auth/server.ts | Core better-auth configuration with Shopify OIDC via the genericOAuth plugin, plus server-side session helpers: getCustomerSession(), getSession(), requireCustomerSession(), requireSession(). Uses React cache() for per-request memoization. Server-only. |
lib/auth/client.ts | Client-side hooks and actions: useSession(), signIn(), signOut(). |
The API route at /api/auth/[...all] handles all OAuth callbacks, session management, and token operations via better-auth's toNextJsHandler.
Feature gating
The isAuthEnabled flag (from lib/auth) reads auth.enabled from lib/config, which is process.env.NEXT_PUBLIC_ENABLE_AUTH === "1". When false:
- The account icon does not appear in the nav
/account/loginand/account/*return 404- No auth-related code runs at request time
This means auth infrastructure has zero runtime cost when disabled.
Routes
Authentication uses Shopify-native URL paths:
| Route | Description |
|---|---|
/account/login | Auto-redirects to Shopify OIDC. Not indexed by search engines. |
/account | Redirects to /account/profile. |
/account/profile | Displays customer name and email. |
/account/orders | Order history (scaffold — wire with Customer Account API operations). |
/account/orders/[id] | Order detail (scaffold). |
/account/addresses | Address book (scaffold). |
The account pages use a (authenticated) route group so the auth-gated layout applies to protected pages without blocking /account/login.
Architecture
Session flow
- Customer visits
/account/login→ auto-redirected to Shopify OIDC - After Shopify consent → redirected to
/api/auth/callback/shopify - better-auth exchanges the code for tokens, decodes the ID token, and creates a session
- Session stored in an
httpOnlycookie with PKCE verification
Nav account icon
The nav uses a fixed-size container (size-5) with the fallback icon rendered inline and the async NavAccount component positioned absolutely on top via Suspense. This ensures the icon space is always reserved and there is no layout shift when the Suspense boundary resolves.
Server-side usage
Client-side usage
Guardrails
- Never expose access tokens to the client —
getSession()andrequireSession()are server-only - Always call
requireSession()before any Customer Account API operation - The Customer Account API uses a separate GraphQL endpoint from the Storefront API — validate fields with the Shopify schema
- Session cookies use
httpOnlyandsecureflags automatically via better-auth - PKCE is enabled for the OAuth flow — never disable it
isAuthEnabledmust read aNEXT_PUBLIC_variable — server-only env vars cause hydration mismatches with cache components. Don't replace it with an inlineprocess.env.BETTER_AUTH_SECRETcheck
Next steps
The account pages are scaffolds. To populate them with real data, create Customer Account API operations in lib/shopify/operations/customer.ts for fetching profile, orders, and addresses using the access token from requireSession().