oritizes session integrity, extension compatibility, and environment isolation. Below is a production-ready implementation path.
Step 1: Environment Isolation and Dependency Audit
Never enable the new customer accounts on a live store without a parallel validation environment. Spin up a development store, mirror your production app stack, and run a compatibility matrix against every plugin that touches customer state. Loyalty, subscription, and B2B pricing apps must be verified against Shopify's extension documentation. Disable or replace incompatible integrations before proceeding.
Step 2: Theme Navigation and Routing Updates
Legacy themes often hardcoded account links to /account or relied on Liquid conditionals that no longer resolve correctly. Update your header navigation to point to the new hosted account URL. Verify that login, logout, and redirect callbacks align with Shopify's OAuth endpoints.
// navigation-utils.ts
export const getAccountRoute = (isLoggedIn: boolean): string => {
if (isLoggedIn) {
return '/account'; // Hosted account page
}
return '/account/login'; // Hosted login redirect
};
export const handleLogout = async (shopDomain: string): Promise<void> => {
const logoutUrl = `https://${shopDomain}/account/logout`;
window.location.href = logoutUrl;
};
Step 3: Headless Session Management with PKCE
For decoupled storefronts, the Customer Account API requires a proper OAuth 2.0 flow with PKCE. Never store client secrets in frontend code. Generate a code verifier, derive the challenge, and exchange the authorization code for an access token.
// auth-manager.ts
import { createHash } from 'crypto';
export class CustomerAuthManager {
private readonly shopDomain: string;
private readonly clientId: string;
private readonly redirectUri: string;
constructor(config: { shopDomain: string; clientId: string; redirectUri: string }) {
this.shopDomain = config.shopDomain;
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
}
private generateCodeVerifier(): string {
return crypto.randomUUID().replace(/-/g, '');
}
private async generateCodeChallenge(verifier: string): Promise<string> {
const hash = createHash('sha256').update(verifier).digest('base64url');
return hash;
}
public async initiateLogin(): Promise<string> {
const verifier = this.generateCodeVerifier();
const challenge = await this.generateCodeChallenge(verifier);
sessionStorage.setItem('pkce_verifier', verifier);
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
code_challenge: challenge,
code_challenge_method: 'S256',
scope: 'openid email',
});
return `https://${this.shopDomain}/account/oauth/authorize?${params.toString()}`;
}
public async exchangeCodeForToken(code: string): Promise<string> {
const verifier = sessionStorage.getItem('pkce_verifier');
if (!verifier) throw new Error('PKCE verifier missing');
const response = await fetch(`https://${this.shopDomain}/account/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: this.clientId,
code,
grant_type: 'authorization_code',
redirect_uri: this.redirectUri,
code_verifier: verifier,
}),
});
if (!response.ok) throw new Error('Token exchange failed');
const data = await response.json();
return data.access_token;
}
}
Step 4: UI Extension Deployment
Shopify's new account pages are decoupled from theme rendering. Custom blocks must be built using the @shopify/ui-extensions-react framework. Extensions run in a sandboxed environment and communicate via GraphQL queries scoped to the customer account.
// LoyaltyPointsExtension.tsx
import { render, BlockExtension } from '@shopify/ui-extensions-react';
import { useApi, Text, View } from '@shopify/ui-extensions-react/account';
render('CustomerAccount::Block::LoyaltySummary', () => <LoyaltyBlock />);
function LoyaltyBlock() {
const { query, data } = useApi();
const { customer } = data;
const points = customer?.metafields?.find(
(m: any) => m.namespace === 'loyalty' && m.key === 'points'
)?.value || '0';
return (
<BlockExtension>
<View padding="large">
<Text size="medium">Available Loyalty Points: {points}</Text>
</View>
</BlockExtension>
);
}
Architecture Rationale
- OAuth 2.0 with PKCE: Eliminates authorization code interception attacks. The verifier/challenge pair ensures that even if the redirect is intercepted, the token cannot be exchanged without the original verifier.
- UI Extensions over Liquid Overrides: Shopify's hosted account pages are no longer rendered through theme templates. Extensions provide a controlled, versioned interface that survives platform updates without breaking custom markup.
- Headless Decoupling for Complex Stores: B2B pricing, multi-tier loyalty, or custom onboarding flows require state management that exceeds extension capabilities. A Next.js or Hydrogen frontend paired with the Customer Account API provides full routing control while maintaining session continuity through standardized OAuth tokens.
Pitfall Guide
1. The Inline Modal Illusion
Explanation: Developers attempt to embed the hosted login page inside an iframe or modal to preserve zero-redirect UX. Shopify's CSP headers and session cookie policies block cross-origin embedding, causing silent authentication failures.
Fix: Accept the redirect flow or rebuild the experience using UI extensions. If inline UX is non-negotiable, migrate to a fully headless storefront where you control the entire authentication surface.
2. Multipass Token Mapping
Explanation: Teams try to adapt legacy Multipass tokens to the new OAuth flow, assuming the payload structure is compatible. Multipass is deprecated and the Customer Account API expects standard OAuth grants.
Fix: Replace Multipass entirely. Implement the PKCE flow for server-to-server authentication and use the returned access token to query the Customer Account API.
3. App Dependency Neglect
Explanation: Loyalty, subscription, and wishlist apps often inject Liquid or rely on legacy session cookies. Enabling new accounts without auditing these plugins causes silent data loss or broken post-login redirects.
Fix: Maintain a compatibility matrix. Test each app in a development environment. Disable incompatible plugins during migration and schedule vendor updates before production rollout.
4. PKCE Verifier Leakage
Explanation: Storing the code verifier in localStorage or exposing it in client-side logs violates OAuth security practices and enables token hijacking.
Fix: Use sessionStorage for the verifier, clear it immediately after token exchange, and enforce HTTPS-only cookies for session state. Never log verifier values in production.
5. Mobile Viewport Degradation
Explanation: The hosted login and account pages render responsively, but custom branding assets often break on iOS Safari or Android Chrome when viewport meta tags are misconfigured in the parent theme.
Fix: Test the redirect flow on physical devices. Ensure your theme's <meta name="viewport"> aligns with Shopify's hosted page expectations. Use Shopify's branding settings to upload optimized header images and brand colors.
6. Over-Engineering Standard Stores
Explanation: Teams build custom headless authentication layers for stores that only need basic account management. This introduces unnecessary infrastructure costs and maintenance debt.
Fix: Reserve headless architecture for B2B, multi-currency, or complex loyalty requirements. Standard DTC stores should leverage hosted pages + UI extensions.
7. Skipping Session Boundary Testing
Explanation: Developers verify login works but fail to test token persistence across storefront β checkout β account page transitions. Legacy session bugs resurface when boundaries aren't validated.
Fix: Create a test matrix covering guest checkout, authenticated checkout, account updates, and logout. Verify that the access token refreshes correctly and that checkout never prompts for re-authentication.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Standard DTC store with basic account needs | Hosted pages + UI extensions | Minimal infrastructure, native session handling, fast deployment | Low (theme updates only) |
| B2B store with tiered pricing & custom onboarding | Headless Next.js/Hydrogen + Customer Account API | Full routing control, complex state management, custom auth surfaces | High (frontend infra, dev time) |
| Store relying heavily on legacy Multipass SSO | OAuth 2.0 PKCE replacement + app vendor coordination | Multipass is deprecated; requires protocol migration and plugin updates | Medium (migration labor, potential app fees) |
Configuration Template
# shopify.ui.extension.toml
name = "loyalty-summary"
type = "customer_account_block"
runtime_context = "customer_account"
[settings]
target = "CustomerAccount::Block::LoyaltySummary"
extension_points = ["account.order.details", "account.profile"]
// env-config.ts
export const AUTH_CONFIG = {
shopDomain: process.env.SHOPIFY_SHOP_DOMAIN!,
clientId: process.env.SHOPIFY_CLIENT_ID!,
redirectUri: process.env.NEXT_PUBLIC_AUTH_CALLBACK!,
scopes: ['openid', 'email', 'profile'],
tokenRefreshThreshold: 300, // seconds before expiry
};
Quick Start Guide
- Navigate to Shopify Admin β Settings β Customer accounts and toggle to New customer accounts on a development store.
- Run a dependency audit across all loyalty, subscription, and wishlist plugins. Disable incompatible apps temporarily.
- Update your theme's navigation links to
/account/login and /account, then verify redirect behavior.
- Deploy a basic UI extension using
@shopify/ui-extensions-react to validate sandboxed rendering and GraphQL scoping.
- Execute a full customer journey test: OTP login, social login, order history access, address update, and logout. Confirm session persistence across checkout transitions.