- How to Add Shopify Authentication to a Headless Storefront Using the Customer Account API
How to Add Shopify Authentication to a Headless Storefront Using the Customer Account API
A step-by-step guide to secure logins, registrations, and order history with full UX control

A few weeks back, I found myself deep in the weeds of building a fully headless Shopify e-commerce store. Everything was going smoothly—until I hit that inevitable wall: user accounts. I needed the basics—login, registration, viewing account information, order history, the usual suspects. In a traditional Shopify setup, these are just there out of the box. But in a headless build? You’re suddenly flying without a safety net. Every auth flow and customer interaction becomes your responsibility.
Luckily, Shopify has a solution: the Customer Account API. On paper, it promises to give you all the account management features you know and expect, and you get to wire up fully custom interfaces and flows just for your users. Sounds awesome, right? The catch: there’s a learning curve, and more than a few “gotchas” along the way—from configuring your API access, handling tricky token flows, to making sure your customers actually stay signed in.
After spending way longer than I expected wrangling with setup, API quirks, and debugging my own mistakes, I decided to pull it all together in this guide—for future me, and for the next developer who goes headless and wonders “where are the login forms and order pages I was promised?”
In this article, I’ll walk you through everything you actually need to do to add a robust customer account system to your headless Shopify store: from OAuth setup, to secure logins, order history views, and all the hidden details in-between. If you want enterprise-grade security and total UX control, you’re in the right place. Let’s ship something awesome.
The Complete User Authentication Flow
Understanding the user journey: Before diving into code, let's walk through exactly what happens when a customer interacts with your authentication system. This flow shows how an anonymous visitor becomes an authenticated customer with full access to their account.
Step-by-Step User Journey
1. Initial Visit - Unauthenticated State
- User arrives at your site (anonymous visitor)
- Navigation shows "Login" button instead of account menu
- Components involved:
components/auth/login-button.tsx
,components/auth/auth-context.tsx
- State:
isAuthenticated: false, customer: null
2. User Clicks "Login"
- User clicks the "Login" button in navigation
- What happens:
startLogin()
function from auth context is triggered - Code location:
components/auth/auth-context.tsx:79
- Process:
- Generates secure PKCE parameters (code verifier, state, nonce)
- Stores parameters in browser's sessionStorage
- Redirects user to Shopify's authentication server
3. Shopify Authentication Page
- User is redirected to
https://shopify.com/authentication/{SHOP_ID}/oauth/authorize
- What user sees: Shopify's branded login page
- User actions: Enters email/password or creates new account
- Security: All handled by Shopify's secure servers (no sensitive data touches your app)
4. Shopify Authentication Success
- After successful authentication, Shopify redirects back to your app
- Redirect URL:
https://your-domain.com/auth/callback?code=abc123&state=xyz789
- What happens: Browser navigates to your callback page
- Page location:
app/auth/callback/page.tsx
5. OAuth Callback Processing
- Callback page extracts
code
andstate
from URL parameters - Security check: Validates state parameter matches stored value (CSRF protection)
- Code location:
components/auth/auth-context.tsx:handleCallback()
- Process:
- Retrieves stored PKCE verifier from sessionStorage
- Calls your API endpoint with code and verifier
- API call:
POST /api/auth/callback
6. Server-Side Token Exchange
- Your API endpoint receives the authorization code
- API location:
app/api/auth/callback/route.ts
- Process:
- Exchanges authorization code for access tokens using PKCE verifier
- Shopify API call:
POST https://shopify.com/authentication/{SHOP_ID}/oauth/token
- Receives:
access_token
,refresh_token
,id_token
,expires_in
- Stores tokens in secure HTTP-only cookies
- Fetches customer data using new access token
7. Customer Data Retrieval
- Using the fresh access token, fetch customer profile
- API call: Customer Account API GraphQL query
- Query location:
lib/shopify/queries/customer.ts:getCustomerQuery
- Data retrieved: Customer name, email, addresses, etc.
- Security: Token automatically formatted with
shcat_
prefix
8. Authentication Complete
- User is now authenticated and redirected to account dashboard
- State update:
isAuthenticated: true, customer: {customerData}
- UI changes:
- "Login" button becomes user menu/account dropdown
- Account pages become accessible
- Order history loads automatically
9. Ongoing Session Management
- Automatic token refresh: Monitors token expiration every 5 minutes
- Background process:
components/auth/auth-context.tsx:272
- Refresh trigger: When token expires, automatically calls refresh endpoint
- API endpoint:
POST /api/auth/refresh
- User experience: Seamless, no interruption to browsing
10. Account Features Available
- Order History:
components/account/orders-tab.tsx
- Profile Management:
components/account/profile-tab.tsx
- Address Management: Customer can update shipping/billing addresses
- Session persistence: Remains logged in across browser sessions
Integration with product browsing: Once authenticated, customers can seamlessly browse your product catalog and collections. The pagination patterns we covered in our Shopify Headless Storefront Cursor Pagination guide work perfectly with authenticated sessions, and the product count strategies help authenticated users navigate large catalogs efficiently.
11. Logout Process
- User clicks logout button
- Process:
logout()
function from auth context - Steps:
- Calls
POST /api/auth/logout
to revoke tokens server-side - Clears all authentication cookies
- Redirects to Shopify's logout endpoint for complete session termination
- Returns user to your site in unauthenticated state
- Calls
Visual Flow Representation
🌐 User visits site (unauthenticated)
↓
🔐 Clicks "Login" button
↓
🛡️ Redirected to Shopify auth server
↓
✅ User authenticates with Shopify
↓
🔄 Shopify redirects back with authorization code
↓
🔐 Your app exchanges code for tokens
↓
👤 Fetch customer data from Shopify
↓
🎉 User now authenticated & can access account
↓
📊 Order history, profile management available
↓
🔄 Background token refresh maintains session
↓
🚪 User logout → tokens revoked → back to step 1
Key Components in the Flow
Flow Step | Component/File | Purpose |
---|---|---|
Initial State | components/auth/auth-context.tsx | Manages authentication state |
Login Button | components/auth/login-button.tsx | Triggers authentication flow |
OAuth Redirect | lib/shopify/oauth-client.ts | Builds secure authorization URL |
Callback Handler | app/auth/callback/page.tsx | Processes OAuth callback |
Token Exchange | app/api/auth/callback/route.ts | Exchanges code for tokens |
Session Storage | lib/auth/session.ts | Secure cookie management |
Customer Data | lib/shopify/fetchers/customer-account/customer.ts | Fetches customer information |
Account UI | components/account/ | Account dashboard and features |
Token Refresh | app/api/auth/refresh/route.ts | Automatic token renewal |
Logout | app/api/auth/logout/route.ts | Secure session termination |
📸 Screenshot Opportunity: Create a visual flowchart showing this user journey with screenshots of each step - the login button, Shopify's auth page, the callback URL, the authenticated state, and the account dashboard.
Why this flow matters: This comprehensive flow ensures security at every step while providing a seamless user experience. Each component has a specific responsibility, making the system maintainable and secure.
What You'll Accomplish
By following this guide, you'll implement:
- User Authentication: Login and logout functionality
- Account Creation: Customer registration with Shopify
- Session Management: Secure token handling and automatic refresh
- Order History: Retrieve and display customer orders
- Profile Management: Update customer information and addresses
- Full E-commerce Experience: Complete customer account functionality
📸 Screenshot Opportunity: Take a screenshot of your final customer account dashboard showing the login/logout button, order history, and profile sections. This gives readers a clear vision of what they'll build.
Understanding Shopify's Three Core APIs
Why we need to understand this: Before diving into implementation, it's crucial to understand where the Customer Account API fits in Shopify's ecosystem. Many developers get confused about which API to use for what purpose, leading to security issues or architectural problems. Understanding these three APIs will help you make informed decisions about your implementation strategy.
Shopify provides three main APIs that form the cornerstone of headless e-commerce development:
1. Storefront API
- Usage: 90% of use cases
- Purpose: Product catalog, collections, cart management
- Access: Public API, can be used client-side
- Authentication: Storefront access token
Deep dive resources: For comprehensive coverage of Storefront API implementation, check out our detailed guides on Shopify Storefront API Product Counts and Shopify Headless Storefront Cursor Pagination. These complement the Customer Account API by handling the product catalog and navigation aspects of your headless store.
2. Admin API
- Usage: Specific administrative tasks
- Purpose: Metaobjects, store configuration, inventory management
- Access: Server-only (contains sensitive data)
- Authentication: Admin access token (must be kept secret)
3. Customer Account API
- Usage: Customer authentication and data management
- Purpose: User authentication, order history, profile management
- Access: OAuth-based authentication
- Authentication: Customer access tokens with OAuth flow
Important: The Customer Account API is the latest addition to Shopify's API family, designed specifically for privacy protection and customer data handling.
📸 Screenshot Opportunity: Create a simple diagram or screenshot showing how these three APIs work together in your application. Show the Storefront API handling products, Admin API managing configurations, and Customer Account API handling user authentication.
Architecture Overview
Why architecture matters: A well-designed architecture is the foundation of maintainable code. The Customer Account API implementation touches many parts of your application - from client-side components to server-side API routes. Understanding this architecture upfront will help you navigate the code and make modifications confidently.
The implementation follows a layered architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Client Layer │
├─────────────────────────────────────────────────────────────────┤
│ components/auth/ │ components/account/ │
│ - auth-context.tsx │ - orders-tab.tsx │
│ - login-button.tsx │ - profile-tab.tsx │
│ - account-dropdown │ - account-server-layout.tsx │
├─────────────────────────────────────────────────────────────────┤
│ API Layer │
├─────────────────────────────────────────────────────────────────┤
│ app/api/auth/ │
│ - callback/route.ts │ - logout/route.ts │
│ - session/route.ts │ - refresh/route.ts │
├─────────────────────────────────────────────────────────────────┤
│ Business Logic Layer │
├─────────────────────────────────────────────────────────────────┤
│ lib/shopify/ │ lib/auth/ │
│ - oauth-client.ts │ - session.ts │
│ - oauth-server.ts │ │
│ - queries/customer.ts│ │
├─────────────────────────────────────────────────────────────────┤
│ Shopify APIs │
├─────────────────────────────────────────────────────────────────┤
│ Customer Account API │ Storefront API │ Admin API │
└─────────────────────────────────────────────────────────────────┘
📸 Screenshot Opportunity: Take a screenshot of your project's file structure in VS Code, highlighting the key directories mentioned in the architecture diagram (components/auth, app/api/auth, lib/shopify, lib/auth).
Environment Configuration
Why environment configuration is critical: OAuth authentication requires precise configuration of redirect URIs and client credentials. A single misconfigured environment variable can break the entire authentication flow. This section ensures you have all the necessary credentials and URLs configured correctly before diving into the implementation.
Required Environment Variables
# Public variables (accessible client-side) NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=your-store.myshopify.com NEXT_PUBLIC_SHOPIFY_SHOP_ID=your-shop-id NEXT_PUBLIC_SHOPIFY_CUSTOMER_API_CLIENT_ID=your-client-id NEXT_PUBLIC_BASE_URL=https://your-domain.com # Server-only variables SHOPIFY_ADMIN_API_ACCESS_TOKEN=your-admin-token
Development vs Production
Development Setup:
NEXT_PUBLIC_BASE_URL=https://abc123.ngrok-free.app
Production Setup:
NEXT_PUBLIC_BASE_URL=https://yoursite.vercel.app
📸 Screenshot Opportunity: Show your
.env.local
file (with sensitive values redacted) to demonstrate proper environment variable setup. Also capture the Shopify Partner Dashboard where you configure the Customer Account API app settings.
OAuth Implementation
Why OAuth is essential: OAuth 2.0 with PKCE (Proof Key for Code Exchange) is the gold standard for secure authentication. It prevents common security vulnerabilities like authorization code interception and provides a secure way for your application to access customer data without storing passwords. Understanding this flow is crucial for maintaining security and compliance.
1. Client-Side OAuth Utilities
File: lib/shopify/oauth-client.ts
The client-side OAuth utilities handle the initial authentication flow:
import {
generateCodeVerifier,
generateState,
generateNonce,
buildAuthorizationUrl,
} from "lib/shopify/oauth-client";
// Generate PKCE parameters for secure authentication
const verifier = generateCodeVerifier();
const state = generateState();
const nonce = generateNonce();
// Build authorization URL
const authUrl = await buildAuthorizationUrl(verifier, state, nonce);
Key Features:
- PKCE (Proof Key for Code Exchange): Cryptographically secure code verifier generation
- CSRF Protection: State parameter validation
- Replay Attack Protection: Nonce generation
- Client-Safe: Uses only
NEXT_PUBLIC_
environment variables
⚠️ Critical OAuth Configuration: The most common authentication failure is missing the required OAuth scope. During our implementation, we discovered that without the customer-account-api:full
scope, Shopify rejects ALL requests regardless of token format:
// ❌ Common mistake - Missing customer-account-api:full scope
const scopes = "openid email";
// ✅ Correct scope configuration
const scopes = "openid email customer-account-api:full";
Why this matters: The customer-account-api:full
scope is essential for accessing customer account data. Without it, you'll encounter "Invalid token" errors that can be misleading because the token format might be correct, but the scope permissions are insufficient.
📸 Screenshot Opportunity: Capture the browser's Network tab during login showing the OAuth authorization URL being generated, highlighting the PKCE parameters (code_challenge, state, nonce) and the complete scope string.
2. Server-Side OAuth Operations
Why server-side operations are crucial: While the client initiates the OAuth flow, the server must handle the secure token exchange. This separation ensures that sensitive operations like token refresh and revocation happen in a secure environment where tokens can't be intercepted by malicious client-side code.
File: lib/shopify/oauth-server.ts
Server-side utilities handle token exchange and management:
import {
exchangeCodeForTokens,
refreshAccessToken,
revokeTokens,
} from "lib/shopify/oauth-server";
// Exchange authorization code for tokens
const tokens = await exchangeCodeForTokens(code, verifier);
// Refresh expired tokens
const newTokens = await refreshAccessToken(refreshToken);
// Revoke tokens during logout
await revokeTokens(idToken);
Key Operations:
- Token Exchange: Convert authorization code to access/refresh tokens
- Token Refresh: Automatically refresh expired tokens
- Token Revocation: Secure logout with token invalidation
📸 Screenshot Opportunity: Show the server logs during token exchange, demonstrating the successful OAuth callback with token response (ensure to redact actual token values).
Session Management
Why secure session management matters: Once you have authentication tokens, you need to store them securely and manage their lifecycle. Poor session management is a common source of security vulnerabilities. HTTP-only cookies provide the perfect balance of security and usability - they're inaccessible to JavaScript (preventing XSS attacks) but automatically included in requests.
Secure Cookie-Based Sessions
File: lib/auth/session.ts
The session management system uses secure HTTP-only cookies:
// Cookie configuration
const COOKIE_OPTIONS = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax" as const,
path: "/",
};
// Store authentication tokens
await setTokens(accessToken, refreshToken, idToken, expiresIn);
// Retrieve stored tokens
const tokens = await getTokens();
// Clear all tokens
await clearTokens();
Security Features:
- HTTP-Only Cookies: Prevents XSS attacks
- Secure Flag: HTTPS-only in production
- SameSite Protection: CSRF prevention
- Token Formatting: Automatic
shcat_
prefix for Shopify tokens
📸 Screenshot Opportunity: Show the browser's Developer Tools > Application > Cookies section, highlighting the secure authentication cookies with their httpOnly and secure flags.
Token Lifecycle Management
Why token lifecycle management is important: Tokens expire for security reasons, but users shouldn't be logged out unexpectedly. Proper lifecycle management ensures seamless user experience by automatically refreshing tokens before they expire, while maintaining security by not keeping tokens longer than necessary.
// Check if token is expired (with 5-minute buffer)
const isExpired = isTokenExpired(tokens.expiresAt);
// Get valid access token (auto-refresh if needed)
const accessToken = await getValidAccessToken();
// Update access token after refresh
await updateAccessToken(newAccessToken, expiresIn);
🔍 Session Persistence Troubleshooting: A common issue we encountered was users appearing logged in during the session but showing as unauthenticated after page refresh. This typically happens when the initial session loading fails:
The Problem: Auth context doesn't properly load session on app initialization
// ❌ Missing credentials can cause session loading to fail
const response = await fetch("/api/auth/session", {
method: "GET",
// Missing credentials: "include"
});
The Solution: Ensure cookies are included in session requests
// ✅ Include credentials for proper cookie handling
const response = await fetch("/api/auth/session", {
method: "GET",
credentials: "include", // Essential for cookie-based sessions
});
Why this happens: HTTP-only cookies require the credentials: "include"
option to be sent with requests. Without this, the session endpoint can't access the authentication cookies, causing the user to appear logged out even though their session is valid.
Quick debugging check: If users are logged out after refresh, inspect the session request in Network tab and verify that cookies are being sent with the request.
Authentication Flow Implementation
Why understanding the full flow is crucial: Authentication isn't just about the initial login - it's about managing the entire user journey from login to logout, including error handling and token refresh. This comprehensive flow ensures users have a smooth experience while maintaining security throughout their session.
1. Login Flow
The login journey: When a user clicks "Login," they're redirected to Shopify's secure authentication server. After successful authentication, they're redirected back to your app with an authorization code. This code is then exchanged for access tokens that allow your app to access their account data.
File: components/auth/auth-context.tsx
const startLogin = useCallback(async (): Promise<void> => {
try {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
// Generate PKCE and security parameters
const verifier = generateCodeVerifier();
const state = generateState();
const nonce = generateNonce();
// Store PKCE verifier temporarily (browser only)
sessionStorage.setItem("oauth_code_verifier", verifier);
sessionStorage.setItem("oauth_state", state);
sessionStorage.setItem("oauth_nonce", nonce);
// Build authorization URL and redirect
const authUrl = await buildAuthorizationUrl(verifier, state, nonce);
router.push(authUrl);
} catch (error) {
setAuthState((prev) => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : "Failed to start login",
}));
}
}, [router]);
📸 Screenshot Opportunity: Show the Shopify authentication page that users see during login, then capture the callback URL in the browser's address bar showing the authorization code parameter.
2. OAuth Callback Handler
What happens during the callback: After users authenticate with Shopify, they're redirected back to your app with an authorization code. This callback handler validates the response, exchanges the code for tokens, and establishes the user session. This is where the magic happens - transforming a temporary code into permanent access to the customer's account.
File: app/api/auth/callback/route.ts
export async function POST(request: NextRequest) {
try {
const { code, verifier } = await request.json();
if (!code || !verifier) {
return NextResponse.json(
{ message: "Missing code or verifier" },
{ status: 400 }
);
}
// Exchange code for tokens
const tokenResponse = await exchangeCodeForTokens(code, verifier);
// Store tokens in secure cookies
await setTokens(
tokenResponse.access_token,
tokenResponse.refresh_token,
tokenResponse.id_token,
tokenResponse.expires_in
);
// Get customer data
const formattedToken = formatAccessToken(tokenResponse.access_token);
const customer = await getCustomer(formattedToken);
return NextResponse.json({
customer,
success: true,
});
} catch (error) {
console.error("OAuth callback failed:", error);
return NextResponse.json(
{ message: "Authentication failed" },
{ status: 400 }
);
}
}
🔧 Common Token Format Issues: During development, we encountered several token-related issues that can be confusing to debug:
Issue 1: Missing Token Prefix
// ❌ Problem - Using raw OAuth token
const customer = await getCustomer(tokenResponse.access_token);
// ✅ Solution - Format token with required prefix
const formattedToken = formatAccessToken(tokenResponse.access_token);
const customer = await getCustomer(formattedToken);
Issue 2: Incorrect Authorization Header
// ❌ Problem - Standard OAuth Bearer format
headers: {
Authorization: `Bearer ${token}`;
}
// ✅ Solution - Shopify Customer Account API format
headers: {
Authorization: token; // Direct token, no "Bearer" prefix
}
Why these issues occur: The Customer Account API uses a unique authentication format that differs from standard OAuth implementations. The token must be prefixed with shcat_
and sent directly in the Authorization header without the "Bearer" prefix.
📸 Screenshot Opportunity: Show the Network tab during the OAuth callback, highlighting the POST request to
/api/auth/callback
with the authorization code and the successful response with customer data. Also capture any token format errors in the console.
3. Logout Implementation
Why proper logout is essential: Logout isn't just about clearing local state - it's about completely terminating the user's session both in your app and with Shopify. This involves token revocation, cookie clearing, and redirecting users to Shopify's logout endpoint to ensure they're fully logged out of their Shopify account too.
File: components/auth/auth-context.tsx
const logout = useCallback(async (): Promise<void> => {
try {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
// Call logout API endpoint to revoke tokens server-side
const response = await fetch("/api/auth/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) {
const { logoutUrl } = await response.json();
// Clear client state
setAuthState({
customer: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
// Redirect to Shopify logout if URL provided
if (logoutUrl) {
window.location.href = logoutUrl;
}
}
} catch (error) {
setAuthState((prev) => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : "Failed to logout",
}));
}
}, []);
📸 Screenshot Opportunity: Show the logout flow in action - capture the user clicking logout, then show the redirect to Shopify's logout page, followed by the return to your site in a logged-out state.
Customer Data Management
Why GraphQL for customer data: The Customer Account API uses GraphQL, which allows you to request exactly the data you need in a single request. This is particularly important for customer data where you might need profile information, addresses, and order history - GraphQL lets you fetch all of this efficiently while respecting privacy boundaries.
1. GraphQL Queries
Building efficient queries: These queries are designed to fetch customer data with minimal network requests while respecting Shopify's rate limits and the customer's privacy settings.
File: lib/shopify/queries/customer.ts
# Get customer profile
query getCustomer {
customer {
id
displayName
firstName
lastName
emailAddress {
emailAddress
}
phoneNumber {
phoneNumber
}
defaultAddress {
id
firstName
lastName
address1
address2
city
province
country
zip
}
}
}
# Get customer orders
query getCustomerOrders($first: Int = 10) {
customer {
orders(first: $first) {
nodes {
id
name
processedAt
totalPrice {
amount
currencyCode
}
}
}
}
}
Pagination considerations: The Customer Account API uses cursor-based pagination similar to the Storefront API. If you're implementing order history with large order counts, the pagination strategies from our Shopify Headless Storefront Cursor Pagination guide apply here as well - just replace product nodes with order nodes.
📸 Screenshot Opportunity: Show the GraphQL Playground or your IDE with the customer queries, highlighting the structured data that gets returned for customer profile and orders.
2. Customer Data Fetching
How data fetching works: The fetching functions abstract away the complexity of token management and API calls, providing a clean interface for your components to request customer data. They handle token validation, automatic refresh, and error management transparently.
File: lib/shopify/fetchers/customer-account/customer.ts
export async function getCustomer(accessToken?: string): Promise<Customer> {
let token = accessToken;
if (!token) {
const { getTokens } = await import("lib/auth/session");
const tokens = await getTokens();
if (!tokens) throw new Error("No access token available");
token = tokens.accessToken;
}
const res = await customerAccountApiRequest<ShopifyCustomerOperation>({
accessToken: token,
query: getCustomerQuery,
});
return res.body.data.customer;
}
🚨 GraphQL Schema Gotchas: One frustrating issue we encountered was using fields that don't exist in the Customer Account API. This commonly happens when copying queries from Storefront API examples:
Fields that DON'T exist in Customer Account API:
// ❌ These fields will cause GraphQL errors:
customer {
createdAt // Not available
updatedAt // Not available
numberOfOrders // Not available
addresses {
phone // Not available in CustomerAddress
}
}
Fields that DO exist:
// ✅ Use these supported fields:
customer {
id
displayName
firstName
lastName
emailAddress {
emailAddress
}
phoneNumber {
phoneNumber
}
defaultAddress {
id
firstName
lastName
address1
address2
city
province
country
zip
}
}
Debugging tip: If you encounter "Field doesn't exist" errors, check the Customer Account API GraphQL schema for the exact field names and structure.
📸 Screenshot Opportunity: Show the Network tab during a customer data fetch, highlighting the GraphQL request with the customer token and the returned customer data structure. Also capture any GraphQL field errors in the console.
Order History Implementation
Why order history is crucial: Order history is often the primary reason customers create accounts. A well-implemented order history gives customers confidence in your store and reduces support requests. It needs to be fast, accurate, and present information in a user-friendly way.
Order Display Component
Creating user-friendly order displays: The order component transforms raw GraphQL data into a readable, clickable interface. It handles edge cases like empty order lists and provides clear navigation to detailed order views.
Performance optimization: For customers with extensive order histories, you'll want to implement the pagination patterns detailed in our cursor pagination guide. The same GraphQL cursor techniques that work for product listings apply to order history, ensuring smooth performance even with hundreds of orders.
File: components/account/orders-tab.tsx
export default function OrdersTab({ orders, error }: OrdersTabProps) {
if (error) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800">{error}</p>
</div>
);
}
if (orders.length === 0) {
return (
<div className="text-center py-8">
<p className="text-gray-500">
Sie haben noch keine Bestellungen aufgegeben.
</p>
</div>
);
}
return (
<div className="space-y-4">
{orders.map((order) => (
<Link
key={order.id}
href={`/account/orders/${extractOrderId(order.id)}`}
className="block"
>
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="font-semibold">Bestellung #{order.name}</h3>
<p className="text-sm text-gray-500">
{new Date(order.processedAt).toLocaleDateString("de-DE")}
</p>
</div>
<div className="text-right">
<p className="font-semibold">
{order.totalPrice.amount} {order.totalPrice.currencyCode}
</p>
<p className="text-sm text-blue-600 mt-1">Details anzeigen →</p>
</div>
</div>
</div>
</Link>
))}
</div>
);
}
📸 Screenshot Opportunity: Show the final order history component in action - display the list of orders with order numbers, dates, and prices, demonstrating how the raw data is transformed into a user-friendly interface.
API Routes Structure
Why API routes are the backbone: API routes handle all server-side authentication logic, from OAuth callbacks to session management. They're the bridge between your React components and Shopify's APIs, handling security, token management, and data transformation in a secure server environment.
Authentication Endpoints
Understanding the endpoint structure: Each endpoint has a specific purpose in the authentication flow. Understanding when and how to use each endpoint is crucial for implementing robust authentication.
app/api/auth/
├── callback/route.ts # OAuth callback handler
├── session/route.ts # Session validation
├── logout/route.ts # Logout and token revocation
├── refresh/route.ts # Token refresh
└── clear-cookies/route.ts # Clear auth cookies
📸 Screenshot Opportunity: Show your file structure in VS Code with the
app/api/auth/
directory expanded, highlighting all the authentication endpoints and their purpose.
Key API Route Implementations
Critical implementations explained: These are the core implementations that make authentication work. Each handles a specific part of the authentication lifecycle and includes proper error handling and security measures.
Session Validation: app/api/auth/session/route.ts
export async function GET() {
try {
const customer = await getCustomer();
return NextResponse.json({ customer, authenticated: true });
} catch (error) {
return NextResponse.json({ authenticated: false }, { status: 401 });
}
}
Token Refresh: app/api/auth/refresh/route.ts
export async function POST() {
try {
const tokens = await getTokens();
if (!tokens) {
return NextResponse.json({ error: "No tokens found" }, { status: 401 });
}
const newTokens = await refreshAccessToken(tokens.refreshToken);
await updateAccessToken(newTokens.access_token, newTokens.expires_in);
const customer = await getCustomer();
return NextResponse.json({ customer, success: true });
} catch (error) {
await clearTokens();
return NextResponse.json(
{ error: "Token refresh failed" },
{ status: 401 }
);
}
}
📸 Screenshot Opportunity: Show the API routes in action through the Network tab, demonstrating the session validation and token refresh calls with their responses.
Component Architecture
Why component architecture matters: A well-structured component architecture makes your authentication system maintainable and reusable. The context provider pattern ensures authentication state is consistently available throughout your app while keeping components focused on their specific responsibilities.
Authentication Context Provider
Centralized authentication state: The authentication context provides a single source of truth for user authentication state across your entire application. It handles complex state management so your components can focus on rendering UI.
File: components/auth/auth-context.tsx
The authentication context provides application-wide auth state management:
interface AuthState {
customer: Customer | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
type AuthContextType = AuthState & {
startLogin: () => Promise<void>;
handleCallback: (code: string, state: string) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
};
Key Features:
- Automatic Token Monitoring: Checks token expiration every 5 minutes
- State Management: Centralized authentication state
- Error Handling: Comprehensive error management
- Session Persistence: Maintains auth state across page refreshes
📸 Screenshot Opportunity: Show the React Developer Tools with the AuthContext highlighted, demonstrating how the authentication state flows through your component tree.
Account Management Components
Organized account functionality: These components handle different aspects of the customer account experience, from viewing orders to managing profile information. Each component is focused on a specific user need while sharing common authentication state.
components/account/
├── account-server-layout.tsx # Server-side layout wrapper
├── client-tabs-wrapper.tsx # Client-side tab navigation
├── orders-tab.tsx # Order history display
├── profile-tab.tsx # Customer profile management
└── skeletons.tsx # Loading states
📸 Screenshot Opportunity: Show your account management interface with the different tabs (orders, profile) and loading states, demonstrating how the components work together to create a cohesive user experience.
Security Considerations
Why security is paramount: Customer account data is sensitive and regulated. A security breach can destroy customer trust and result in legal consequences. These security measures aren't optional - they're essential for protecting your customers and your business.
1. Token Security
Protecting the keys to the kingdom: Access tokens are like keys to your customers' accounts. Proper token security ensures that even if other parts of your application are compromised, customer data remains protected.
- HTTP-Only Cookies: Prevents XSS attacks
- Secure Transmission: HTTPS-only in production
- Token Prefixing: Automatic
shcat_
prefix for Shopify compatibility - Expiration Management: 5-minute buffer for token expiration
2. CSRF Protection
Preventing malicious requests: CSRF (Cross-Site Request Forgery) attacks can trick users into performing actions they didn't intend. These protections ensure that requests to your authentication endpoints are legitimate and come from your application.
- State Parameter: Validates OAuth callback authenticity
- SameSite Cookies: Prevents cross-site request forgery
- Nonce Validation: Prevents replay attacks
3. Data Protection
Safeguarding sensitive information: Customer data must be protected at every layer of your application. These measures ensure that sensitive information never leaks and that access is properly controlled.
- Server-Only Operations: Admin API calls restricted to server-side
- Environment Variables: Sensitive data in environment variables
- Token Revocation: Proper logout with token invalidation
📸 Screenshot Opportunity: Show your browser's Security tab or SSL certificate information, demonstrating the HTTPS connection and secure cookie settings that protect customer data.
Error Handling
Why robust error handling is crucial: Authentication errors can be confusing and frustrating for users. Good error handling provides clear feedback, maintains security, and helps users recover from problems. It's the difference between a user successfully logging in and abandoning your site.
Authentication Errors
Handling authentication failures gracefully: Authentication can fail for many reasons - expired tokens, network issues, or invalid credentials. Your error handling should guide users to resolution while maintaining security.
try {
const customer = await getCustomer();
} catch (error) {
if (error.message.includes("401")) {
// Token expired, attempt refresh
await refreshToken();
} else {
// Other authentication errors
setAuthState({
customer: null,
isAuthenticated: false,
isLoading: false,
error: "Authentication failed",
});
}
}
GraphQL Error Handling
Managing API communication errors: GraphQL errors can provide detailed information about what went wrong, but they need to be handled carefully to avoid exposing sensitive information to users while still providing helpful feedback.
const res = await customerAccountApiRequest({
accessToken: token,
query: getCustomerQuery,
});
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
📸 Screenshot Opportunity: Show error states in your UI - capture authentication errors, network failures, and loading states to demonstrate how your error handling provides clear user feedback.
File Structure Overview
Understanding the project organization: A well-organized file structure makes your authentication system maintainable and helps new developers understand how everything fits together. This structure separates concerns while keeping related files close together.
project/
├── app/
│ ├── api/auth/ # Authentication API routes
│ └── auth/callback/ # OAuth callback page
├── components/
│ ├── auth/ # Authentication components
│ │ ├── auth-context.tsx # Global auth state
│ │ ├── login-button.tsx # Login trigger
│ │ └── account-dropdown.tsx
│ └── account/ # Account management
│ ├── orders-tab.tsx # Order history
│ └── profile-tab.tsx # Profile management
├── lib/
│ ├── auth/
│ │ └── session.ts # Session management
│ └── shopify/
│ ├── oauth-client.ts # Client-side OAuth
│ ├── oauth-server.ts # Server-side OAuth
│ ├── queries/customer.ts # GraphQL queries
│ └── fetchers/customer-account/
│ └── customer.ts # Customer data fetching
└── docs/
└── customer-account-api-implementation-guide.md
📸 Screenshot Opportunity: Show your complete project structure in VS Code with key authentication files highlighted, demonstrating how the architecture translates to actual file organization.
Testing and Validation
Why testing your authentication is critical: Authentication bugs can lock users out of their accounts or create security vulnerabilities. Proper testing ensures your authentication flow works reliably across different scenarios and edge cases.
Environment Validation
Catching configuration issues early: Environment validation prevents mysterious authentication failures by ensuring all required credentials are properly configured before your application starts.
// Validate OAuth configuration
export function validateServerOAuthConfig(): void {
const requiredEnvVars = [
"NEXT_PUBLIC_SHOPIFY_SHOP_ID",
"NEXT_PUBLIC_SHOPIFY_CUSTOMER_API_CLIENT_ID",
"NEXT_PUBLIC_BASE_URL",
];
const missing = requiredEnvVars.filter((envVar) => !process.env[envVar]);
if (missing.length > 0) {
throw new Error(
`Missing required server environment variables: ${missing.join(", ")}`
);
}
}
Development Commands
Essential commands for development: These commands help you maintain code quality, generate types, and test your authentication implementation throughout development.
# Start development server
pnpm dev
# Build for production
pnpm build
# Format code
pnpm prettier
# Check formatting
pnpm prettier:check
# Generate GraphQL types
pnpm codegen
📸 Screenshot Opportunity: Show your terminal running these commands, particularly the output from
pnpm codegen
showing GraphQL types being generated and any lint/format checks passing.
Common Issues and Solutions
Learning from real-world implementations: These are the exact issues we encountered during our Customer Account API implementation, along with the solutions that actually work. Each problem includes the symptoms, root causes, and step-by-step fixes.
1. "Invalid token, missing prefix shcat_" Error
Most Critical Issue: This error can have multiple layered causes that mask each other, making it particularly frustrating to debug.
Symptom: Error appears in console during OAuth callback, authentication seems to fail randomly
Root Causes & Solutions:
A) Missing OAuth Scope (Critical)
// ❌ Broken - Missing customer-account-api:full scope
const scopes = "openid email";
// ✅ Fixed - Complete scope required
const scopes = "openid email customer-account-api:full";
File: lib/shopify/oauth-client.ts
Why Critical: Without this scope, Shopify rejects ALL requests regardless of token format
B) Wrong Authorization Header Format
// ❌ Broken - Standard OAuth Bearer format
headers: {
Authorization: `Bearer ${token}`;
}
// ✅ Fixed - Shopify Customer Account API format
headers: {
Authorization: token; // Direct token, no "Bearer" prefix
}
File: lib/shopify/index.ts
in customerAccountApiRequest
C) Token Missing Prefix
// ❌ Broken - Raw OAuth token
const customer = await getCustomer(tokenResponse.access_token);
// ✅ Fixed - Formatted token with prefix
const formattedToken = formatAccessToken(tokenResponse.access_token);
const customer = await getCustomer(formattedToken);
File: app/api/auth/callback/route.ts
2. GraphQL Field Errors After Successful Authentication
Symptom: Authentication succeeds but GraphQL queries fail with "Field doesn't exist" errors
Root Cause: Using Storefront API fields that don't exist in Customer Account API
Unsupported Fields:
// ❌ These fields don't exist in Customer Account API:
customer {
createdAt // Not available
updatedAt // Not available
numberOfOrders // Not available
addresses {
phone // Not available in CustomerAddress
}
}
Solution: Use only supported fields from the Customer Account API schema
3. Authentication Context Not Updating
Symptom: OAuth completes successfully but isAuthenticated
remains false
Root Cause: Callback page doesn't properly trigger auth context update
Solution: Ensure callback properly calls handleCallback
:
// In app/auth/callback/page.tsx
await handleCallback(code, state); // Must update auth context
4. Session Not Persisting After Page Refresh
Symptom: User appears logged in but becomes unauthenticated after browser refresh
Root Cause: Missing credentials: "include"
in session requests
Solution: Include credentials in all session-related requests:
const response = await fetch("/api/auth/session", {
method: "GET",
credentials: "include", // Essential for cookie-based sessions
});
5. NGROK Development Setup Issues
Symptom: OAuth redirects fail in development, BASE_URL constantly changes
Root Cause: NGROK generates new URLs on each restart
Solution:
- Set up NGROK with a consistent URL (paid feature) OR
- Update
.env.local
with new NGROK URL after each restart - Update Shopify app redirect URI to match
6. Environment Configuration Errors
Symptom: Various authentication failures, often with misleading error messages
Root Cause: Missing or incorrect environment variables
Required Variables:
# Public variables NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=your-store.myshopify.com NEXT_PUBLIC_SHOPIFY_SHOP_ID=123456789 NEXT_PUBLIC_SHOPIFY_CUSTOMER_API_CLIENT_ID=uuid-client-id NEXT_PUBLIC_BASE_URL=https://your-domain.com # Private variables SHOPIFY_ADMIN_API_ACCESS_TOKEN=shpat_xxx SHOPIFY_API_KEY=xxx SHOPIFY_API_SECRET_KEY=xxx
Verification: Ensure redirect URI in Shopify Partner Dashboard matches NEXT_PUBLIC_BASE_URL/auth/callback
Conclusion
What you've accomplished: You've now implemented a complete, secure, and production-ready customer authentication system using Shopify's Customer Account API. This implementation provides enterprise-grade security while maintaining full control over your user experience.
This implementation provides a complete, secure, and scalable customer authentication system using Shopify's Customer Account API. The architecture supports:
- Full OAuth 2.0 Flow: With PKCE and security best practices
- Secure Session Management: HTTP-only cookies with automatic refresh
- Complete Customer Experience: Login, logout, profile management, and order history
- Production-Ready: Proper error handling, security measures, and performance optimizations
The system offloads authentication complexity to Shopify while maintaining full control over the user experience in your headless application.
Your next steps: With this foundation in place, you can extend the system with additional features like address management, order tracking, wishlist functionality, and more. The architecture you've built is designed to scale with your business needs while maintaining security and performance.
Building a complete headless store: This Customer Account API implementation is one piece of a complete headless e-commerce solution. For the full picture, explore our other deep dives on implementing efficient product counts for category pages and mastering cursor pagination for smooth product browsing. Together, these guides provide the foundation for a high-performance, scalable headless Shopify store.
📸 Screenshot Opportunity: Show your final, fully functional customer account system in action - the login flow, authenticated dashboard, order history, and logout process working seamlessly together.