In this tutorial, we are going to migrate from the default customer account API authentication in Hydrogen. Which is the non-customizable, email-code login (like below).

If you are reading this, you likely made a new Hydrogen project after August 2023. Hydrogen projects created after this date are pre-configured with the customer account API as the default user authentication method.
This article is going to teach you how to abandon this form of authentication, and implement the storefront API for authentication instead.
Notice Before Reading
Depending on your knowledge level of JWT (access token) authentication, and the Shopify APIs, I would highly recommend reading and understand the FULL article, not just skimming.
This is not really too difficult, but will require a good understanding of Shopify’s authentication. If you attempt to implement authentication and do it wrongly or without full oversight, you risk exposing user data to malicious attacks.
Therefore, it is very important to understand the user authentication process very well before tampering with it.
Authentication Methods in Shopify
There are 2 primary methods for authentication in headless Shopify stores:
The Customer Account API
The Storefront API
Customer Account API
This is the authentication method we are abandoning
Newer Hydrogen builds come with the Customer Account API preconfigured as the default authentication method. You can tell if your Hydrogen project is using this, by looking for these methods in your account/login and account/logout routes within the loader functions.
context.customerAccount.login();
context.customerAccount.logout();
context.customerAccount.authorize();
context.customerAccount.handleAuthStatus();
If you are reading this blog, you likely made a Hydrogen build after August 2023, and this was the pre-configured authentication method in your hydrogen build (pictured under article title).
Even though we are here to abandon this as our authentication method, there are some benefits to using the customer account API on Hydrogen:
| Pros | Cons |
| Effortless setup | Forced to use email code auth |
| Preconfigured auth form | Difficult to test queries |
| Ensured Security out of box | Unable to customize authentication form |
| Not really “headless” |
Storefront API Authentication
This is the authentication method we want to switch to
The Storefront API is the original method of authentication for headless storefronts. The Storefront API allows customers to sign in via email & password, and allows developers full control over the authentication process.
This also allows us to make a completely custom login form and keep all of the user authentication within your headless site.
| Pros | Cons |
| Better Documentation and Support | Longer Setup Process |
| Completely Headless | Less Beginner-friendly |
| Ability to use custom auth form | Abstracts security responsibility to developers |
| Easy to test endpoints via GraphiQL |
Example Storefront API Authentication Queries
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customer {
id
email
firstName
}
customerUserErrors {
field
message
}
}
}
mutation customerAccessTokenCreate($email: String!, $password: String!) {
customerAccessTokenCreate(input: { email: $email, password: $password }) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
query customer($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
firstName
lastName
email
orders(first: 10) {
edges {
node {
orderNumber
totalPrice {
amount
}
}
}
}
}
}
mutation customerAccessTokenRenew($customerAccessToken: String!) {
customerAccessTokenRenew(customerAccessToken: $customerAccessToken) {
customerAccessToken {
accessToken
expiresAt
}
userErrors {
field
message
}
}
}
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
userErrors {
field
message
}
}
}
These methods will be used later in this article, as we switch our Hydrogen application from using the Customer Account API to using the Storefront API for customer authentication (Which will allow us to make our own custom login form on our site).
For testing purposes, it is smart to build the form on the frontend first. This way, we can test the endpoints we are changing within our application. Keep in mind, the storefront API is email/password authentication.
import { useState, useEffect } from 'react';
import { useFetcher } from '@remix-run/react';
import { json, redirect } from '@shopify/remix-oxygen';
export async function loader({ context }) {
const customerAccessToken = await context.session.get('customerAccessToken');
if (!customerAccessToken) {
return json({ isLoggedIn: false });
}
const { customer } = await context.storefront.query(
`query CustomerDetails($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
}
}`,
{
variables: { customerAccessToken },
});
if (customer) {
return redirect('/account/orders');
} else {
return null;
}
}
export default function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const fetcher = useFetcher();
const handleSubmit = (event) => {
event.preventDefault();
if(email && password) {
const formData = new FormData();
formData.append('email', email);
formData.append('password', password);
fetcher.submit(formData, { method: 'post', action: '/account/login' });
}
};
useEffect(() => {
if (fetcher.data) {
console.log('Response from action:', fetcher.data);
}
}, [fetcher.data]);
return (
<div className='bg-white'>
<form
onSubmit={(event) => handleSubmit(event)}
className='max-w-sm mx-auto flex flex-col gap-3 py-10 text-sizzleBlue'
>
<h3 className='text-4xl text-center font-thin mb-2'>Login</h3>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder='Email'
className='rounded-md border-neutral-300'
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='Password'
className='rounded-md border-neutral-300'
required
/>
<button
type="submit"
className='self-center mt-2 px-10 py-2 bg-sizzleOrange text-white font-thin text-lg rounded-full'
>
Log In
</button>
</form>
</div>
);
}
Later you can add creating new accounts, styling etc
Switching From Customer Account API to Storefront API
There are 4 primary components that need to be updated, when switching from the Customer Account API to the Storefront API:
Login
Authorization
Logout
Fetching Customer Details
And you will need to create:
User Creation functionality
Custom Login / Signup form
Any other instances of customerAccount methods will need to be replaced with their storefront API equivalents
Login Component
Your account/login route will go from this:
import {json} from '@shopify/remix-oxygen';
export async function loader({ request, context }) {
return context.customerAccount.login();
}
to this:
import {json} from '@shopify/remix-oxygen';
import {redirect} from '@shopify/remix-oxygen';
export async function action({ request, context }) {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
const { customerAccessTokenCreate } = await context.storefront.mutate(`
mutation customerAccessTokenCreate($email: String!, $password: String!) {
customerAccessTokenCreate(input: { email: $email, password: $password }) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`, { variables: { email, password } });
if (customerAccessTokenCreate.customerUserErrors.length > 0) {
return json({ error: customerAccessTokenCreate.customerUserErrors }, { status: 400 });
}
const accessToken = customerAccessTokenCreate.customerAccessToken.accessToken;
console.log("Access Token: ", accessToken);
await context.session.set('customerAccessToken', accessToken);
return redirect('/account/orders', {
headers: {
'Set-Cookie': await context.session.commit(),
},
});
}
Authorization Component
In my storefront API setup, I no longer utilize the /account/authorization route given by the customer Account API:
export async function loader({context}) {
return context.customerAccount.authorize();
}
Instead, I store access tokens in the session. Sessions should already be set up in your Hydrogen application (in lib/session.js ).
Session methods:
context.session.get('key'); gets the value of a value stored in the session
context.session.set('key', value); stores a new value in the session
context.session.unset('key'); deletes a session value
context.session.commit(); saves all of the changes that were made to the session during the component (typically is towards bottom of server component
How to check if a user is logged in, and redirect accordingly
This code snippet is used to check if a user is logged in, and redirect accordingly. Put this in any page you only want signed-in users to view. Orders page, account page etc.
This effectively replaces the authorization component. You could abstract this to a utility function and just reference it access multiple components.
const customerAccessToken = await context.session.get('customerAccessToken');
const { customer } = await context.storefront.query(
`query CustomerDetails($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
}
}`,
{
variables: { customerAccessToken },
});
if (customer) {
return redirect('/account/orders');
} else {
return null;
}
Logout Component
For my logout code, in route /account/logout, it goes from:
import {redirect} from '@shopify/remix-oxygen';
export async function loader() {
return redirect('/');
}
export async function action({context}) {
return context.customerAccount.logout();
}
to:
import { redirect } from '@shopify/remix-oxygen';
const CUSTOMER_ACCESS_TOKEN_DELETE_MUTATION = `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
userErrors {
field
message
}
}
}
`;
export async function loader() {
return redirect('/');
}
export async function action({ context }) {
const customerAccessToken = await context.session.get('customerAccessToken');
if (customerAccessToken) {
try {
const { data, errors } = await context.storefront.mutate({
mutation: CUSTOMER_ACCESS_TOKEN_DELETE_MUTATION,
variables: { customerAccessToken },
});
if (errors?.length || !data.customerAccessTokenDelete.deletedAccessToken) {
console.error('Error invalidating customer access token:', errors || data.customerAccessTokenDelete.userErrors);
}
} catch (error) {
console.error('Error while invalidating access token:', error);
}
}
context.session.unset('customerAccessToken');
return redirect('/', {
headers: {
'Set-Cookie': await context.session.commit(),
},
});
}
Fetching Customer Details
Change your /account route from this:
export async function loader({context}) {
const {data, errors} = await context.customerAccount.query(
CUSTOMER_DETAILS_QUERY,
);
if (errors?.length || !data?.customer) {
throw new Error('Customer not found');
}
return json(
{customer: data.customer},
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Set-Cookie': await context.session.commit(),
},
},
);
}
to this:
export async function loader({ context }) {
const customerAccessToken = await context.session.get('customerAccessToken');
console.log("Customer Access Token from session: ", customerAccessToken);
if (!customerAccessToken) {
throw new Error('Customer not authenticated');
}
const { customer } = await context.storefront.query(CUSTOMER_DETAILS_QUERY, {
variables: { customerAccessToken },
});
if (!customer) {
throw new Error('Customer not found');
}
return json(
{ customer },
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Set-Cookie': await context.session.commit(),
},
}
);
}
const CUSTOMER_DETAILS_QUERY = `
query CustomerDetails($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
firstName
lastName
email
acceptsMarketing
createdAt
phone
defaultAddress {
address1
address2
city
province
country
zip
}
}
}
`;
Notice I also changed the graphQL query, since we are now using the Storefront API.
Conclusion
I hope you enjoyed this tutorial. And most importantly, I hope you learned more about the storefront API and the customer account API. Understanding these 2 APIs in Hydrogen will allow you to make this migration seamlessly and specific to your application.
Don’t forget, your application may be different than mine. The main idea here is switching your customer account API methods to storefront API queries that can accomplish the same task.
Make sure that you have gotten rid of any context.customerAccount references. When context.customerAccount is referenced, your application will automatically send a GET request to account/login if not logged in via customer account API.