> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bigcommerce.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bigcommerce.com/_mcp/server.

# Lab - Build Customer Registration

Plan: Composable Developer

Lesson 25 of 27 · 45 min

## **Introduction**

The final essential workflow to implement in your basic Next.js storefront is customer login. While you'll track a customer's authenticated state within your own application, BigCommerce will be used for the authentication itself.

### In this lab, you will:

* Add a customer login page.
* Use a customer's email and password to authenticate them with BigCommerce.
* Track a customer's ID in a secure JWT and include it as context in all relevant GraphQL requests.

### Prerequisites

This exercise builds on the basic Next.js application begun in previous labs and has the same technical requirements.

Customer account registration will not be part of this lab, so you will need a pre-existing customer in your store.

To observe the full functionality of your customer session logic, your store should contain a **customer group** matching the following:

* Is NOT the default customer group. (The point of this customer group is to demonstrate differences from an anonymous user.)
* Has custom pricing. (Edit the customer group and set a Storewide Discount.)
* Has restricted category access. (Edit the customer group, un-check "Customers in this group can see products in all categories across all channels" and select a limited sub-set of your headless channel's top-level categories.)

Then make sure your store contains a **customer** matching the following:

* Has "Origin channel" set to your headless channel
* Has "Customer group" set to the customer group above
* Has a password set

## **Setup**

This exercise continues where you left off previously in your basic Next.js project. Simply start the dev server from the project root if it's not currently running:

```bash showLineNumbers={false}
pnpm run dev
```

If you need a fresh start, you can follow the instructions below to set up a new project complete with previous exercise code.

1. In your terminal, **run** the following, replacing the path to the working directory with your own path:

```bash showLineNumbers={false}
corepack enable pnpm && pnpm dlx create-next-app@latest -e https://github.com/bigcommerce-edu/lab-nextjs-storefront/tree/e-cust-start /path/to/working/directory
```

2. **Copy** `.env.example` as `.env.local` and **modify** `.env.local` with configuration details for your store.
3. **Start** the dev server process:

```bash showLineNumbers={false}
cd /path/to/working/directory
pnpm run dev
```

4. **Browse** to the local URL printed in the `pnpm` output and **verify** your catalog pages and cart load successfully.

### Exercise Reference Copy

You may choose to download or clone the completed version of this lab in a separate location to serve as a reference for the final state.

[Customer Registration Lab](https://github.com/bigcommerce-edu/lab-nextjs-storefront/tree/e-cust-complete)

## **Configure JWT Support**

You'll be encoding an authenticated customer's information in a JWT to securely track their session. The library `jsonwebtoken` is pre-installed in the codebase and is used to manage JWTs. This library requires a secret value in order to sign JWTs.

1. **Run** the following in your terminal to generate a random secret that will be used to sign JWTs.

```bash showLineNumbers={false}
openssl rand -hex 32
```

2. **Add** the secret you generated above to your `.env.local` file.

```text showLineNumbers={false}
JWT_SECRET="{your secret}"
```

## Create the Registration Page

1. **Open** the file `app/register/page.tsx`, which defines the register page component. **Update** the component as follows.

```js {4-7} showLineNumbers={false}
export default async function RegisterPage() {
  return (
    <>
      <PageHeading>Register</PageHeading>
      <div className="w-1/3">
        <RegisterForm />
      </div>
    </>
  );
}
```

We're not doing much in this page component. The reason is that basically the entire contents of the page are contained within a form, and as you've previously seen with the Add to Cart button, interacction requires a client component!

`RegisterForm` is currently empty. Let's fill it in next.

2. **Open** the file `components/account/register-form.tsx` and **modify** the component as follows.

```js {3-12,15-59} showLineNumbers={false}
const RegisterForm = () => {

  const [email, setEmail] = useState("");
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [errorMsg, setErrorMsg] = useState<string | null>(null);

  const submitRegister = async () => {

  }

  return (
    <form onSubmit={(e) => {e.preventDefault(); submitRegister()}}>

      {errorMsg && (
        <div className="bg-rose-400 p-2 font-bold text-sm text-center">
          {errorMsg}
        </div>
      )}

      <div className="my-8 p-4">
        <label className="font-bold text-lg block">Email</label>
        <input className="w-full border border-neutral-300 p-2
          hover:border-neutral-700"
          type="text" value={email} onChange={(e) => setEmail(e.target.value)} />
      </div>
      <div className="my-8 p-4">
        <label className="font-bold text-lg block">First Name</label>
        <input className="w-full border border-neutral-300 p-2
          hover:border-neutral-700"
          type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      </div>
      <div className="my-8 p-4">
        <label className="font-bold text-lg block">Last Name</label>
        <input className="w-full border border-neutral-300 p-2
          hover:border-neutral-700"
          type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} />
      </div>
      <div className="my-8 p-4">
        <label className="font-bold text-lg block">Password</label>
        <input className="w-full border border-neutral-300 p-2
          hover:border-neutral-700"
          type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      </div>
      <div className="my-8 p-4 text-center">
      <button disabled={loading} className="p-2 rounded-md text-lg w-44 cursor-pointer
        bg-neutral-700 text-white hover:bg-neutral-500 disabled:bg-neutral-500">
        {loading ? (
          <span>...</span>
        ) : (
          <span>
            Register
          </span>
        )}
      </button>
      </div>
    </form>
  );
}
```

We're using some simple state here to capture the form values, as well as tracking a "loading" state when the form is submitted.

3. **Fill in** the `submitRegister` function with the request logic.

```js {5-19} showLineNumbers={false}
const RegisterForm = () => {
  ...

  const submitRegister = async () => {
    setLoading(true);
    setErrorMsg(null);

    const registerResp = await registerCustomer({
      email,
      firstName,
      lastName,
      password,
    });

    setLoading(false);

    if (!registerResp.success) {
      setErrorMsg(registerResp.error ?? "An unexpected error occurred");
    }
  }

  return ...
}
```

`registerCustomer` is another server function, which doesn't currently accept the right parameters.

4. **Open** the file `components/account/_actions/register-customer.ts` and **update** the function to accept the proper parameters and return a generic error for the time being.

```js {2-5,7-10,13-15} showLineNumbers={false}
export const registerCustomer = async ({
  email,
  firstName,
  lastName,
  password,
}: {
  email: string,
  firstName: string,
  lastName: string,
  password: string,
}) => {

  return Promise.resolve(
    { success: false, error: "Registration not implemented." }
  );

};
```

5. Since there's not yet a navigation link to the registration page, **browse** directly to `http://localhost:3000/register` (or the correct port for your own local server). You can try submitting the form for a successful HTTP response, but you'll currently receive the "Registration. is not implemented" error.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-01-pre...e-cust-01-post?diff=split)

## Add the Registration Logic

1. **Open** the file `components/account/_actions/register-customer.ts` and **add** the GraphQL and type definitions related to the registration mutation.

```js {3-61} showLineNumbers={false}
import ...

const registerQuery = `
mutation Register(
  $email: String!,
  $firstName: String!,
  $lastName: String!,
  $password: String!
) {
  customer {
    registerCustomer(
      input: {
        email: $email,
        firstName: $firstName,
        lastName: $lastName,
        password: $password
      }
    ) {
      customer {
        entityId
      }
      errors {
        ... on EmailAlreadyInUseError {
          message
        }
        ... on AccountCreationDisabledError {
          message
        }
        ... on CustomerRegistrationError {
          message
        }
        ... on ValidationError {
          message
        }
      }
    }
  }
}
`;

interface RegisterVars {
  email: string;
  firstName: string;
  lastName: string;
  password: string;
}

interface RegisterResp {
  data: {
    customer: {
      registerCustomer: {
        customer: {
          entityId: number;
        };
        errors?: {
          message: string;
        }[]
      }
    }
  }
}

/**
 * Register a new customer
 */
export const registerCustomer = async ({
  ...
```

2. **Update** the `registerCustomer` function to perform the mutation.

```js {7-32} showLineNumbers={false}
export const registerCustomer = async ({
  ...
}: {
  ...
}) => {

  try {
    const customerResp = await bcGqlFetch<RegisterResp, RegisterVars>(
      registerQuery,
      {
        email,
        firstName,
        lastName,
        password,
      }
    );

    const customer = customerResp.data.customer.registerCustomer.customer;
    const errors = customerResp.data.customer.registerCustomer.errors;
    if (!customer) {
      let errorMsg = "Customer registration failed";
      if (errors) {
        errorMsg = errors.map(error => error.message).join("; ")
      }
      return { success: false, error: errorMsg };
    }
  } catch(err) {
    const error = (err instanceof Error) ? err.message : String(err);
    return { success: false, error };
  }

  redirect("/login");

};
```

3. **Browse** once again to `http://localhost:3000/register` (or the correct port for your own local server).
4. **Test** the register form with an email address that doesn't match any existing customer account. A successful registration should result in being redirected to `/login` (which is currently a blank page).
5. **Verify** in your store control panel that the new customer exists.
6. **Browse** to `/register` again and try out a registration with the same email address you used before. **Verify** that you receive appropriate error feedback.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-02-pre...e-cust-02-post?diff=split)

## Create the Login Page

The focus of the next sections will be to provide a login form, perform authentication with BigCommerce, and store a JWT in a cookie to represent the customer's session.

1. **Open** the file `app/login/page.tsx`, which defines the login page component. **Update** the component as follows.

```js {4-8} showLineNumbers={false}
export default function LoginPage() {
  return (
    <>
      <PageHeading>Log In</PageHeading>
      <div className="w-1/3">
        <LoginForm />
      </div>
    </>
  );
}
```

Similar to the registration page, most of what's going on here is delegated to a client form component.

2. **Open** the file `components/account/login-form.tsx` and **modify** the component as follows.

```js {3-10,13-45} showLineNumbers={false}
const LoginForm = () => {

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [errorMsg, setErrorMsg] = useState<string | null>(null);

  const submitLogin = async () => {

  }

  return (
    <form onSubmit={(e) => {e.preventDefault(); submitLogin()}}
      className="">
      {errorMsg && (
        <div className="bg-rose-400 p-2 font-bold text-sm text-center">
          {errorMsg}
        </div>
      )}

      <div className="my-8 p-4">
        <label className="font-bold text-lg block">Email</label>
        <input className="w-full border border-neutral-300 p-2
          hover:border-neutral-700"
          type="text" value={email} onChange={(e) => setEmail(e.target.value)} />
      </div>
      <div className="my-8 p-4">
        <label className="font-bold text-lg block">Password</label>
        <input className="w-full border border-neutral-300 p-2
          hover:border-neutral-700"
          type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      </div>
      <div className="my-8 p-4 text-center">
      <button disabled={loading} className="p-2 rounded-md text-lg w-44 cursor-pointer
        bg-neutral-700 text-white hover:bg-neutral-500 disabled:bg-neutral-500">
        {loading ? (
          <span>...</span>
        ) : (
          <span>
            Log In
          </span>
        )}
      </button>
      </div>
    </form>
  );
};
```

3. **Fill in** the `submitLogin` function with the request logic.

```js {5-17} showLineNumbers={false}
const LoginForm = () => {
  ...

  const submitLogin = async () => {
    setLoading(true);
    setErrorMsg(null);

    const loginResp = await loginCustomer({
      email,
      password,
    });

    setLoading(false);

    if (!loginResp.success) {
      setErrorMsg(loginResp.error ?? "An unexpected error occurred");
    }
  }

  return ...
};
```

Once again, we're relying on a server function that must be modified to accept the right parameters.

4. **Open** the file `components/account/_actions/login-customer.ts` and **modify** the function as follows.

```js {2-3,5-6,9-11} showLineNumbers={false}
export const loginCustomer = async ({
  email,
  password,
}: {
  email: string,
  password: string,
}) => {

  return Promise.resolve(
    { success: false, error: 'Login not implemented' }
  );

};
```

5. Since there's not yet a navigation link to the login page, **browse** directly to `http://localhost:3000/login` (or the correct port for your own local server). You can try submitting the form for a successful HTTP response. You'll currently see the "not implemented" message.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-03-pre...e-cust-03-post?diff=split)

## Add the Login Logic

1. **Open** the file `components/account/_actions/login-customer.ts` and **add** the GraphQL and type definitions related to the login mutation.

```js {3-38} showLineNumbers={false}
import ...

const loginQuery = `
mutation Login(
  $email: String!,
  $password: String!
) {
  login(
    email: $email,
    password: $password
  ) {
    customer {
      entityId
    }
    customerAccessToken {
        value
    }
  }
}
`;

interface LoginVars {
  email: string;
  password: string;
}

interface LoginResp {
  data: {
    login: {
      customer: {
        entityId: number;
      }
      customerAccessToken: {
        value: string;
      }
    }
  }
}

/**
 * Perform login
 */
export const loginCustomer = async ({
  ...
```

2. **Update** the `loginCustomer` function to perform the mutation.

```js {9-51} showLineNumbers={false}
export const loginCustomer = async ({
  email,
  password,
}: {
  email: string,
  password: string,
}) => {

  const { JWT_SECRET } = process.env;
  if (!JWT_SECRET) {
    return { success: false, error: "Could not perform login" };
  }

  try {
    const customerResp = await bcGqlFetch<LoginResp, LoginVars>(
      loginQuery,
      {
        email,
        password,
      }
    );

    const customer = customerResp.data.login.customer;
    const token = customerResp.data.login.customerAccessToken.value;
    if (!customer || !token) {
      return { success: false, error: "Customer login failed"};
    }

    const customerJwt = jwt.sign({
      sub: JSON.stringify({
        entityId: customer.entityId,
        token,
      }),
    }, JWT_SECRET);

    const cookieStore = await cookies();
    const secure = await isSecure();
    const cookieName = getCookieName({ name: "customer", secure });

    cookieStore.set({
      name: cookieName,
      value: customerJwt,
      httpOnly: true,
      secure,
    });
  } catch(err) {
    const error = (err instanceof Error) ? err.message : String(err);
    return { success: false, error };
  }

  redirect("/");

};
```

3. **Browse** once again to `http://localhost:3000/login` (or the correct port for your own local server).
4. **Test** the login form with an incorrect email or password to observe the resulting error message.
5. **Test** the login form with credentials for an existing customer. While there won't yet be any visible feedback, you can use your browser dev tools to observe the `customer` cookie being set.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-04-pre...e-cust-04-post?diff=split)

## Add Header Links

Your next step will be to add contextual login/logout links in the store header, which will require awareness of a customer's logged-in status.

1. **Open** the file `lib/getCurrentCustomer.ts`. The helper function here will be used by any component that needs to retrieve the details tracked in the "customer" cookie. **Fill in** the function as follows.

```js {3-27} showLineNumbers={false}
export const getCurrentCustomer = async () => {

  const cookieStore = await cookies();
  const secure = await isSecure();
  const cookieName = getCookieName({ name: "customer", secure });

  const customerJwt = cookieStore.get(cookieName)?.value;
  if (!customerJwt) {
    return null;
  }

  const JWT_SECRET = process.env.JWT_SECRET ?? '';

  let customerResult;
  try {
    const customerClaim = jwt.verify(customerJwt, JWT_SECRET);
    const customer = JSON.parse(customerClaim.sub?.toString() ?? '');
    customerResult = {
      entityId: parseInt(customer.entityId ?? ''),
      token: customer.token ?? '',
    }
  } catch (err) {
    customerResult = null;
    cookieStore.delete(cookieName);
  }

  return customerResult;

};
```

2. **Open** the file `components/account-links/index.tsx`. **Update** the component as follows.

```js {3,7-15} showLineNumbers={false}
const AccountLinks = async () => {

  const currentCustomer = await getCurrentCustomer();

  return (
    <>
      {currentCustomer?.entityId ? (
        <LogoutButton />
      ) : (
        <>
          <Link href="/register" className="mx-4 font-bold hover:underline">Register</Link>
          |
          <Link href="/login" className="mx-4 font-bold hover:underline">Log in</Link>
        </>
      )}

    </>
  );
};
```

Notice that while "Register" and "Log in" are simple links, while a separate component is included for logging out. The logout link, rather than simply linking the user to a destination, is a dynamic interaction, and so it calls for yet another client component. Let's fill out that component's initial content now.

3. **Open** the file `components/account-links/logout-button.tsx` and **modify** the component as follows.

```js {3} showLineNumbers={false}
const LogoutButton = () => {

  const [loading, setLoading] = useState(false);

  return (
    <button
      disabled={loading}
      className="mx-4 font-bold cursor-pointer hover:underline"
    >
      {loading ? (
        <span>...</span>
      ) : (
        <span>Log out</span>
      )}
    </button>
  );
};
```

This button doesn't do anything yet but already has a simple loading state ready to go.

4. **Modify** `components/header/index.tsx` to place `AccountLinks`.

```js {10} showLineNumbers={false}
const Header = async () => {
  ...

  return (
    <header ...>
      <div ...>
        ...
        <div className="flex">

          <AccountLinks />

          <MiniCart />
        </div>
      </div>
      ...
    </header>
  )
}
```

5. **Browse** to your local site and observe the new account link.

If you retry a valid login on the login page, you should be able to observe the links in the header immediately update accordingly.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-05-pre...e-cust-05-post?diff=split)

## Add Logout Logic

1. **Open** the file `components/account-links/logout-button.tsx` and **modify** the component to implement a submit event calling a server function.

```js {4-8,13} showLineNumbers={false}
const LogoutButton = () => {
  const [loading, setLoading] = useState(false);

  const submitLogout = async () => {
    setLoading(true);
    await logout();
    setLoading(false);
  }

  return (
    <button
      ...
      onClick={submitLogout}

    >
      ...
    </button>
  );
}
```

2. **Open** the file `components/account-links/_actions/logout.ts` and **add** the appropriate query and response types.

```js {3-17} showLineNumbers={false}
import ...

const logoutQuery = `
mutation Logout {
  logout {
    result
  }
}
`;

interface LogoutResp {
  data: {
    logout: {
      result: string;
    }
  }
}

/**
 * Perform logout
 */
export const logout = async () => {
```

3. **Update** `logout` to perform the appropriate mutation and destroy the `customer` cookie.

```js {3-16} showLineNumbers={false}
export const logout = async () => {

  const currentCustomer = await getCurrentCustomer();

  if (currentCustomer?.token) {
    await bcGqlFetch<LogoutResp>(
      logoutQuery,
      currentCustomer?.token
    );
  }

  const cookieStore = await cookies();
  const secure = await isSecure();
  const cookieName = getCookieName({ name: "customer", secure });

  cookieStore.delete(cookieName);

};
```

4. **Browse** to your local site and make sure you're logged in with a valid customer account. **Try out** the "Log Out" link in the header.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-06-pre...e-cust-06-post?diff=split)

## Send Customer Token in GraphQL

Your storefront will now handle authenticating and tracking a customer, but this has little actual effect except to then give the customer the chance to log out again. Let's put the customer context to use in one of the most important ways: Passing this context in the storefront's various GraphQL queries, which will affect the data returned.

Support for a `customerToken` is already built into the `bcGqlFetch` function, which passes this parameter in the `X-Bc-Customer-Access-Token` header. Each of the catalog queries you've built also supports a `customerToken` parameter. All that's needed is to update the contexts where these queries are used to obtain and pass in the current customer's token.

In a truly complete storefront, cart-related queries and mutations should be affected as well, but we're not tackling the extra complexity (such as reassigning an existing guest cart to a logged-in customer) in this exercise.

1. **Modify** `components/header/index.tsx` to get the current customer using the helper function you created and pass the token into the GraphQL function.

```js {3-7} showLineNumbers={false}
const Header = async () => {

  const currentCustomer = await getCurrentCustomer();

  const { settings, navCategories } = await getHeaderSettings({
    customerToken: currentCustomer?.token,
  });

  ...

  return ...
}
```

2. **Modify** `app/category/[...catPath]/page.tsx` to perform the same customer context logic.

```js {8,20} showLineNumbers={false}
export default async function CategoryPage({
  ...
}: {
  ...
}) {
  ...

  const currentCustomer = await getCurrentCustomer();

  let category;
  try {
    category = await getCategoryWithProducts({
      path,
      mainImgSize,
      thumbnailSize,
      page: {
        ...
      },

      customerToken: currentCustomer?.token,

    });
  } catch(err) {
    ...
  }

  ...

  return ...
}
```

3. **Modify** `app/product/[...productPath]/page.tsx` to perform the same customer context logic.

```js {8,16} showLineNumbers={false}
export default async function ProductPage({
  ...
}: {
  ...
}) {
  ...

  const currentCustomer = await getCurrentCustomer();

  let product;
  try {
    product = await getProduct({
      path,
      imgSize,

      customerToken: currentCustomer?.token,

    });
  } catch (err) {
    ...
  }

  ...

  return ...
}
```

4. **Choose** a customer belonging to a group with custom pricing and without access to some top-level categories. If you don't have such a customer, register a new customer and make the appropriate change to their customer group.
5. **Try out** both a logged-in and logged-out state on your storefront. Make sure when logging in to use the customer account subject to price and catalog differences. You should be able to observe the differences in the contents of the main nav menu and the pricing on catalog pages.

If you try adding a product to the cart while logged in as a customer with pricing discounts, you will NOT see the same price difference in the cart. This is because the cart queries and mutations, including the initial `createCart` request, still lack the customer context, so cart pricing is not affected by it.

[Example Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-07-pre...e-cust-07-post?diff=split)

## Full Lab Code

[Full Lab Code](https://github.com/bigcommerce-edu/lab-nextjs-storefront/compare/e-cust-start...e-cust-complete?diff=split)

## Taking It Further

Provide the token from the current customer for cart requests as well, and verify that a logged-in customer session is maintained when redirecting to checkout.