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.
Enabling authentication
Add these three server-only secrets to enable the auth UI:
That's the only knob. next.config.ts derives NEXT_PUBLIC_AUTH_ENABLED at build time from the presence of all three secrets and exposes it via the env config so client and server agree at hydration. Don't set NEXT_PUBLIC_AUTH_ENABLED yourself — probing the server-only secrets directly inside a component (instead of going through the build-time flag) 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 NEXT_PUBLIC_AUTH_ENABLED, which next.config.ts derives from secret presence at build time. 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. The flag is derived at build time innext.config.ts; 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().