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:

bash
BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32"
SHOPIFY_CUSTOMER_CLIENT_ID="your-customer-client-id"
SHOPIFY_CUSTOMER_CLIENT_SECRET="your-customer-client-secret"

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

  1. Go to Shopify Admin → Settings → Customer accounts
  2. Enable Customer Account API
  3. Create a Customer Account API client (under "API clients")
  4. Set the redirect URI to {YOUR_DOMAIN}/api/auth/callback/shopify
  5. Copy the client ID and client secret to your environment variables
  6. 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:

ModulePurpose
lib/auth/index.tsUniversal isAuthEnabled flag. Safe to import from server and client code.
lib/auth/server.tsCore 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.tsClient-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/login and /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:

RouteDescription
/account/loginAuto-redirects to Shopify OIDC. Not indexed by search engines.
/accountRedirects to /account/profile.
/account/profileDisplays customer name and email.
/account/ordersOrder history (scaffold — wire with Customer Account API operations).
/account/orders/[id]Order detail (scaffold).
/account/addressesAddress 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

  1. Customer visits /account/login → auto-redirected to Shopify OIDC
  2. After Shopify consent → redirected to /api/auth/callback/shopify
  3. better-auth exchanges the code for tokens, decodes the ID token, and creates a session
  4. Session stored in an httpOnly cookie with PKCE verification

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

ts
import { getCustomerSession, requireSession } from "@/lib/auth/server";

// In a server component — returns null if not authenticated
const session = await getCustomerSession();

// In a protected page — redirects to /account/login if not authenticated
const session = await requireSession();
// session.accessToken is available for Customer Account API calls

Client-side usage

ts
"use client";
import { useSession, signIn, signOut } from "@/lib/auth/client";

function AccountMenu() {
  const { loading, authenticated, customer } = useSession();

  if (loading) return null;
  if (!authenticated) return <button onClick={() => signIn()}>Sign in</button>;
  return <button onClick={() => signOut()}>Sign out</button>;
}

Guardrails

  • Never expose access tokens to the client — getSession() and requireSession() 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 httpOnly and secure flags automatically via better-auth
  • PKCE is enabled for the OAuth flow — never disable it
  • isAuthEnabled must read a NEXT_PUBLIC_ variable — server-only env vars cause hydration mismatches with cache components. The flag is derived at build time in next.config.ts; don't replace it with an inline process.env.BETTER_AUTH_SECRET check

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().

Chat

Tip: You can open and close chat with I

0 / 1000