Building An E-Commerce Store With NextJS
Tags: webdev, javascript, tutorial, programming
Published: 2024-06-04 18:52:25

In this tutorial, you'll learn how to build an e-commerce store where customers can purchase products and make payments via Stripe. After a successful payment, an email notification is sent to the customer, and an in-app notification to the Admin user. The Admin user can also create and delete products within the application.

To build this application, we'll use the following tools:

- [Appwrite](https://appwrite.io/) - for authenticating users, as well as saving and retrieving product details.
- [Next.js](https://nextjs.org/) - for creating the application’s user interface and backend.
- [Novu](https://docs.novu.co/getting-started/introduction)  - for sending email and in-app notifications.
- [React Email](https://react.email/docs/introduction) - for creating email templates.
- [Stripe](https://docs.stripe.com/) - for integrating a payment checkout to the application.

![app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t02iyysrqjfqw8imuqxn.png)

---

## Building the application interface with Next.js

The application pages are divided into two parts based on the roles assigned to the users. Customers can access the Home page and sign in to the application before making payments. Admin users can access all pages, including a sign-in page and a dashboard page where they can add and remove products.

Now, let's build the application.

![https://media1.giphy.com/media/iopxsZtW2QVRs4poEC/giphy.gif?cid=7941fdc6aot3qt7vvq4voh5c1iagyusdpuga713m8ljqcqmd&ep=v1_gifs_search&rid=giphy.gif&ct=g](https://media1.giphy.com/media/iopxsZtW2QVRs4poEC/giphy.gif?cid=7941fdc6aot3qt7vvq4voh5c1iagyusdpuga713m8ljqcqmd&ep=v1_gifs_search&rid=giphy.gif&ct=g)

Create a new Next.js Typescript project by running the code snippet below:

```bash
npx create-next-app novu-store
```

Next, install [React Icons](https://react-icons.github.io/react-icons) and the [Headless UI](https://headlessui.com/) package. React Icons allows us to use various icons within the application, while Headless UI provides easy-to-use modern UI components.

```bash
npm install react-icons @headlessui/react
```

Copy this code snippet from the [GitHub repository](https://github.com/dha-stix/ecom-store-with-nextjs-appwrite-novu-and-stripe/blob/main/src/app/page.tsx) into the `app/page.tsx` file. It renders a list of products on the screen and allows users to select items in a cart, similar to the image below.


![1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dj69givzhqfapgsg12rk.gif)


Create a login route that enables users to sign using their GitHub account. Copy the code snippet below into the `app/login/page.tsx` file.

```tsx
//👉🏻 create a login folder containing a page.tsx file
export default function Home() {
	const handleGoogleSignIn = async () => {};

	return (
		<main className='w-full min-h-screen flex flex-col items-center justify-center'>
			<h2 className='font-semibold text-3xl mb-2'>Customer Sign in</h2>
			<p className='mb-4 text-sm text-red-500'>
				You need to sign in before you can make a purchase
			</p>
			<button
				className='p-4 border-[2px] border-gray-500 rounded-md hover:bg-black hover:text-white w-2/3'
				onClick={() => handleGoogleSignIn()}
			>
				Sign in with GitHub
			</button>
		</main>
	);
}
```


![Customer Sign In](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3nh2rowpfg4hgksj5diy.png)



When users click the Sign in button, it redirects them to the GitHub authentication page and prompts them to sign in to the application. You'll learn how to do this with [Appwrite](https://appwrite.io/) shortly.

Next, let's create the admin pages. Add an `admin` folder containing a `login` and `dashboard` route within the `app` folder.

```bash
cd app
mkdir admin && cd admin
mkdir dashboard login
```

Add a `page.tsx` file within the `dashboard` and `login` folders, and copy the code snippet below into the `login/page.tsx` file.

```tsx
"use client";
import Link from "next/link";
import { useState } from "react";

export default function Login() {
	const [email, setEmail] = useState<string>("");
	const [password, setPassword] = useState<string>("");

	const handleLogin = async (e: React.FormEvent) => {
		e.preventDefault();
		console.log({ email, password });
	};

	return (
		<main className='w-full min-h-screen flex flex-col items-center justify-center'>
			<h2 className='font-semibold text-3xl mb-4'> Admin Sign in</h2>
			<form className='w-2/3' onSubmit={handleLogin}>
				<label htmlFor='email' className='block'>
					Email
				</label>
				<input
					type='email'
					id='email'
					className='w-full px-4 py-3 border border-gray-400 rounded-sm mb-4'
					required
					value={email}
					placeholder='admin@admin.com'
					onChange={(e) => setEmail(e.target.value)}
				/>

				<label htmlFor='password' className='block'>
					Password
				</label>
				<input
					type='password'
					id='password'
					className='w-full px-4 py-3 border border-gray-400 rounded-sm mb-4'
					required
					value={password}
					placeholder='admin123'
					onChange={(e) => setPassword(e.target.value)}
				/>
				<button className='p-4 text-lg mb-3 bg-blue-600 text-white w-full rounded-md'>
					Sign in
				</button>
				<p className='text-sm text-center'>
					Not an Admin?{" "}
					<Link href='/login' className='text-blue-500'>
						Sign in as a Customer
					</Link>
				</p>
			</form>
		</main>
	);
}
```

The code snippet above renders a form that accepts the Admin's email and password, validates the credentials, and then logs the user into the application.


![Admin Sign In](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjd9wsi63t96d5cls9om.png)



The Admin dashboard page renders the available products and allows the Admin user to add and delete products from the application. Copy [this code snippet](https://github.com/dha-stix/ecom-store-with-nextjs-appwrite-novu-and-stripe/blob/main/src/app/admin/dashboard/page.tsx) into the `dashboard/page.tsx` file to create the user interface.


![2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1gd1uq1eq6n76fesjxu.gif)



Congratulations! You've built the application interface. In the upcoming sections, you'll learn how to connect the application to an Appwrite backend and send data between the client and the server.

---

## How to add Appwrite to a Next.js application

Appwrite is an open-source backend service that enables you to create secure and scalable software applications. It offers features such as multiple authentication methods, a secure database, file storage, cloud messaging, and more, which are essential for building full-stack applications.

In this section, you'll learn how to set up an Appwrite project, including features such as authentication, database, and file storage.

First, visit [Appwrite Cloud](https://cloud.appwrite.io/register), and create an account and organization for your projects.

Next, create a new project and select your preferred region for hosting the project.


![Appwrite 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/as6302olk60oklfo70x5.png)



Select `Web` as the platform SDK for the application.


![Appwrite 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bb5ae82i9fyoyrowsy96.png)



Follow the steps displayed on the screen. Since you're currently building in development mode, you can use the wildcard (`*`) as your hostname and change it to your domain name after deploying the application.

![Appwrite 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y5ccs0hzgs9ujf5lzh83.png)

Install the Appwrite client SDK within your Next.js project.

```bash
npm install appwrite
```

Finally, create an `appwrite.ts` file within your Next.js app folder and copy the code snippet below into the file to initialize Appwrite.

```tsx
import { Client, Account, Databases, Storage } from "appwrite";

const client = new Client();

client
	.setEndpoint("https://cloud.appwrite.io/v1")
	.setProject(<YOUR_PROJECT_ID>);

export const account = new Account(client);

export const db = new Databases(client);

export const storage = new Storage(client);
```

### Setting up GitHub Authentication with Appwrite

Here, you'll learn how to set up GitHub and Email/Password authentication with Appwrite. Email/Password authentication is already configured by default, so let's focus on setting up GitHub authentication.

Before we proceed, you need to create a [GitHub OAuth application](https://github.com/settings/developers) using your GitHub account. Appwrite will require the client ID and secrets to set up GitHub authentication.


![GitHub 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9znk1yr7tffus7soitq2.png)


Enable Appwrite's GitHub authentication method by selecting `Auth` from the sidebar menu and navigating to the `Settings` tab.


![Appwrite 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43uo6nho1bz9su14zsno.png)


Copy your GitHub client ID and secret into the Appwrite's GitHub OAuth settings.

![Screenshot 2024-05-24 at 19.45.36.png](Building%20An%20E-Commerce%20Store%20With%20NextJS%20684b2e35ac5f4575930e9fbb8c958741/Screenshot_2024-05-24_at_19.45.36.png)

Finally, ensure you copy the URI generated by Appwrite into your GitHub app settings.


![GitHub 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g75q5r5hc6l5pi09k88m.png)


### Setting up Appwrite Database

Select Databases from the sidebar menu and create a new database. You can name it `novu store`.


![Appwrite 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y7kn1llmu7olqirfcrpa.png)


Next, create a `products` collection. It will contain the lists of products within the application.


![Appwrite 6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7p8laty6z37x0q1g6az4.png)


Add name, price, and image attributes to the collection.


![Appwrite 7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nzom3ptlz8t1rh9dtt1k.png)


Under the Settings tab, update the permissions to allow every user to perform CRUD operations. However, you can change this after deploying the application to ensure that only authenticated users can perform various actions.


![Appwrite 8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/37cqr8s0crtcttocjagk.png)



Finally, copy your project, database, and collection IDs into an **`.env.local`** file. This keeps your credentials safe and allows you to reference each value from its environment variables.

```
NEXT_PUBLIC_PROJECT_ID=<YOUR_PROJECT_ID>
NEXT_PUBLIC_DB_ID=<YOUR_DATABASE_ID>
NEXT_PUBLIC_PRODUCTS_COLLECTION_ID=<YOUR_DB_COLLECTION_ID>
```

### Setting up Appwrite Storage

Select `Storage` from the sidebar menu and create a new bucket that will hold all the product images.


![Appwrite 9](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b84t9mk3k0wrkgiy4uca.png)


Under the `Settings` tab, update the Permissions to allow any user for now.


![Appwrite 10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zi3iozkaera7fohkwanm.png)


Set the acceptable file formats. Since we are uploading images, you can select the **`.jpg`** and **`.png`** file formats.


![Appwrite 11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zzxqpoq5dcpokkvdcsce.png)


Finally, copy your bucket ID into `.env.local` file.

```
NEXT_PUBLIC_BUCKET_ID=<YOUR_BUCKET_ID>
```

Congratulations! You've successfully configured Appwrite. We can now start interacting with its various features.

---

## How to perform CRUD operations with Appwrite

In this section, you'll learn how to create, retrieve, and delete products from Appwrite. Users need to be able to view existing products before making a purchase, while Admin users should have the permission to add and delete products from the application.

First, create a **`utils.ts`** file within the Next.js **`app`** folder. This file will contain all Appwrite database interactions, which you can then import into the necessary pages.

```bash
cd app
touch utils.ts
```

### Saving products to Appwrite

Recall that the `products` collection has three attributes: name, image, and price. Therefore, when adding products to the database, you need to first upload the product's image, retrieve its URL and ID from the response, and then upload the URL as the product's image attribute, using the image's storage ID for the product data.

Here is the code snippet that explains this:

```tsx
import { db, storage } from "@/app/appwrite";
import { ID } from "appwrite";

export const createProduct = async (
	productTitle: string,
	productPrice: number,
	productImage: any
) => {
	try {
		//👇🏻 upload the image
		const response = await storage.createFile(
			process.env.NEXT_PUBLIC_BUCKET_ID!,
			ID.unique(),
			productImage
		);
		//👇🏻 get the image's URL
		const file_url = `https://cloud.appwrite.io/v1/storage/buckets/${process.env.NEXT_PUBLIC_BUCKET_ID}/files/${response.$id}/view?project=${process.env.NEXT_PUBLIC_PROJECT_ID}&mode=admin`;

		//👇🏻 add the product to the database
		await db.createDocument(
			process.env.NEXT_PUBLIC_DB_ID!,
			process.env.NEXT_PUBLIC_PRODUCTS_COLLECTION_ID!,
			response.$id, //👉🏻 use the image's ID
			{
				name: productTitle,
				price: productPrice,
				image: file_url,
			}
		);
		alert("Product created successfully");
	} catch (err) {
		console.error(err);
	}
};
```

The code snippet above uploads the image to Appwrite's cloud storage and retrieves the exact image URL using the bucket ID, image ID, and project ID. Once the image is successfully uploaded, its ID is used in the product's data to enable easy retrieval and reference.

### Retrieving products from Appwrite

To fetch the products from Appwrite, you can execute the function below within the React **`useEffect`** hook when the page loads.

```tsx
export const fetchProducts = async () => {
	try {
		const products = await db.listDocuments(
			process.env.NEXT_PUBLIC_DB_ID!,
			process.env.NEXT_PUBLIC_PRODUCTS_COLLECTION_ID!
		);
		if (products.documents) {
			return products.documents;
		}
	} catch (err) {
		console.error(err);
	}
};
```

The `fetchProducts` function returns all the data within the `products` collection.

### Deleting products from Appwrite

Admin users can also delete a product via its ID. The **`deleteProduct`** function accepts the product's ID as a parameter and deletes the selected product from the database, including its image, since they use the same ID attribute.

```tsx
export const deleteProduct = async (id: string) => {
	try {
		await db.deleteDocument(
			process.env.NEXT_PUBLIC_DB_ID!,
			process.env.NEXT_PUBLIC_PRODUCTS_COLLECTION_ID!,
			id
		);
		await storage.deleteFile(process.env.NEXT_PUBLIC_BUCKET_ID!, id);

		alert("Product deleted successfully");
	} catch (err) {
		console.error(err);
	}
};
```

---

## How to authenticate users with Appwrite

In the previous sections, we've configured the GitHub authentication method. Here, you'll learn how to handle user sign-ins into the application.

To enable customers to sign into the application using their GitHub account, execute the function below when they click the `Sign in` button. The function redirects the user to GitHub, where they can authorize or grant permission to the application and then sign into the application:

```tsx

import { account } from "../appwrite";
import { OAuthProvider } from "appwrite";

const handleGoogleSignIn = async () => {
	try {
		account.createOAuth2Session(
			OAuthProvider.Github,
			"http://localhost:3000",
			"http://localhost:3000/login"
		);
	} catch (err) {
		console.error(err);
	}
};
```

Admin users can sign into the application using an email and password. Appwrite validates the credentials before granting access to the application's dashboard.

```tsx
import { account } from "@/app/appwrite";

const handleLogin = async (e: React.FormEvent) => {
	e.preventDefault();
	try {
		await account.createEmailPasswordSession(email, password);
		alert(`Welcome back 🎉`);
		router.push("/admin/dashboard");
	} catch (err) {
		console.error(err);
		alert("Invalid credentials ❌");
	}
};
```

Appwrite also allows you to fetch the current user's data. For instance, if only authenticated users can make payments, you can do this by running the code snippet below. It retrieves the current user's data or returns null if the user is not logged in.

```tsx
import { account } from "@/app/appwrite";

useEffect(() => {
	const checkAuthStatus = async () => {
		try {
			const request = await account.get();
			setUser(request);
		} catch (err) {
			console.log(err);
		}
	};
	checkAuthStatus();
}, []);
```

---

## How to add Stripe payment checkout to Next.js

In this section, you'll learn how to implement a Stripe payment checkout in the application. Stripe is a popular online payment processing platform that enables you to create products and integrate both one-time and recurring payment methods into your application.

First, you need to [create a Stripe account](https://dashboard.stripe.com/login). You can use a test mode account for this tutorial.


![Stripe 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nibs7bxb09i167mxm918.png)


Click on `Developers` from the top menu and copy your secret key from the API keys menu.


![Stripe 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/up8757knbquc3k0577ps.png)


Paste your Stripe secret key into the `.env.local` file.

```bash
STRIPE_SECRET_KEY=<your_secret_key>
```

Install the [Stripe Node.js SDK](https://docs.stripe.com/libraries).

```bash
npm install stripe
```

Next, create an `api` folder within the Next.js `app` folder. The `api` folder will contain all the API routes and endpoints for the application.

```bash
cd app
mkdir api
```

Create a `checkout` endpoint by adding a `checkout` folder within the `api` folder.

```bash
cd api
mkdir checkout && cd checkout
touch route.ts
```

Copy the code snippet below into the `route.ts` file.

```tsx
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: NextRequest) {
	//👇🏻 accepts the customer's cart
	const cart = await req.json();

	try {
		//👇🏻 creates a checkout session
		const session = await stripe.checkout.sessions.create({
			payment_method_types: ["card"],
			line_items: cart.map((product: Product) => ({
				price_data: {
					currency: "usd",
					product_data: {
						name: product.name,
					},
					unit_amount: product.price * 100,
				},
				quantity: 1,
			})),
			mode: "payment",
			cancel_url: `http://localhost:3000/?canceled=true`,
			success_url: `http://localhost:3000?success=true&session_id={CHECKOUT_SESSION_ID}`,
		});
		//👇🏻 return the session URL
		return NextResponse.json({ session: session.url }, { status: 200 });
	} catch (err) {
		return NextResponse.json({ err }, { status: 500 });
	}
}
```

The code snippet above creates a checkout endpoint that accepts POST requests. It creates a checkout session for the customer and returns the session URL.

The **`cancel_url`** and **`success_url`** determine where to redirect the user after completing or canceling a payment.

Finally, you can send a customer's cart to the `/checkout` endpoint when a user decides to make payment for products by running the code snippet below:

```tsx
const processPayment = async (cart: Product[]) => {
	try {
		if (user !== null) {
			//👇🏻 saves cart to local storage
			localStorage.setItem("cart", JSON.stringify(cart));
			//👇🏻 sends cart to /checkout route
			const request = await fetch("/api/checkout", {
				method: "POST",
				body: JSON.stringify(cart),
				headers: { "Content-Type": "application/json" },
			});
			//👇🏻 retrieves the session URL
			const { session } = await request.json();
			//👇🏻 redirects the user to the checkout page
			window.location.assign(session);
		} else {
			//👇🏻 redirects unauthenticated users
			router.push("/login");
		}
	} catch (err) {
		console.error(err);
	}
};
```

The code snippet above saves the cart to the browser's local storage and sends it to the API endpoint, then retrieves the response (session URL) from the backend server and redirects the user to the Stripe checkout page.


![Stripe 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5hokf2qyqyey3kwsg9x.gif)



---

## Sending in-app and email notifications with Novu

[Novu](https://github.com/novuhq/novu) is the first notification infrastructure that provides a unified API for sending notifications through multiple channels, including In-App, Push, Email, SMS, and Chat.

In this section, you'll learn how to add Novu to your application to enable you to send email and in-app messages.

First, install the required Novu packages:

```bash
npm install @novu/node @novu/echo @novu/notification-center
```

When users make a purchase, they will receive a payment confirmation email, and the admin user also receives an in-app notification.

To do this, you need to [create an account on Novu](https://web.novu.co/auth/login) and set up a primary email provider. We'll use [Resend](https://resend.com/docs/introduction) for this tutorial.

After creating an account on Novu, create a [Resend account](https://resend.com/docs/introduction), and select `API Keys` from the sidebar menu on your dashboard to create one.


![Resend 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jhehx7s45x180zpir1ti.png)


Next, return to your Novu dashboard, select `Integrations Store` from the sidebar menu, and add Resend as an email provider. You'll need to paste your Resend API key and email address into the required fields.


![Novu 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f03vb6nftyi8g790vg7m.png)


Select **Settings** from the sidebar menu and copy your `Novu API`key and `App ID` into a **`.env.local`** file as shown below. Also, copy your `subscriber ID` into its field - you can get this from the `Subscribers` section.

```bash
NOVU_API_KEY=<YOUR_API_FOR_NEXT_SERVER>
NEXT_PUBLIC_NOVU_API_KEY=<YOUR_API_FOR_NEXT_CLIENT>

NEXT_PUBLIC_NOVU_APP_ID=<YOUR_API_ID>

NOVU_SUBSCRIBER_ID=<YOUR_API_FOR_NEXT_SERVER>
NEXT_PUBLIC_NOVU_SUBSCRIBER_ID=<YOUR_API_FOR_CLIENT>
```


![Novu 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/voeofvvtv88pex9rpr1s.png)


Finally, add the Novu notification bell to the Admin dashboard to enable admin users to receive notifications within the application.

```tsx
import {
	NovuProvider,
	PopoverNotificationCenter,
	NotificationBell,
} from "@novu/notification-center";

export default function AdminNav() {
	return (
		<NovuProvider
			subscriberId={process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID!}
			applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
		>
			<PopoverNotificationCenter colorScheme='light'>
				{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
			</PopoverNotificationCenter>
		</NovuProvider>
	);
}
```


![Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m62ft87ue9orse2yww9z.png)



---

## How to create notification workflows with Novu Echo

[Novu](https://docs.novu.co/echo/quickstart) offers a code-first workflow engine that enables you to create notification workflows within your codebase. It allows you to integrate email, SMS, and chat template and content generators, such as [React Email](https://react.email/docs/introduction) and [MJML](https://mjml.io/), into Novu to create advanced and powerful notifications.

In this section, you'll learn how to create notification workflows within your application, use email notification templates with Novu, and send in-app and email notifications with Novu.

Install  [React Email](https://react.email/docs/introduction) by running the following command:

```bash
npm install react-email @react-email/components -E
```

Include the following script in your package.json file. The `--dir` flag gives React Email access to the email templates located within the project. In this case, the email templates are located in the `src/emails` folder.

```json
{
	"scripts": {
		"email": "email dev --dir src/emails"
	}
}
```

Next, create an `emails` folder containing an `email.tsx` within the Next.js `app` folder and copy the code snippet below into the file:

```tsx
import {
	Body,
	Column,
	Container,
	Head,
	Heading,
	Hr,
	Html,
	Link,
	Preview,
	Section,
	Text,
	Row,
	render,
} from "@react-email/components";
import * as React from "react";

const EmailTemplate = ({
	message,
	subject,
	name,
}: {
	message: string;
	subject: string;
	name: string;
}) => (
	<Html>
		<Head />
		<Preview>{subject}</Preview>
		<Body style={main}>
			<Container style={container}>
				<Section style={header}>
					<Row>
						<Column style={headerContent}>
							<Heading style={headerContentTitle}>{subject}</Heading>
						</Column>
					</Row>
				</Section>

				<Section style={content}>
					<Text style={paragraph}>Hey {name},</Text>
					<Text style={paragraph}>{message}</Text>
				</Section>
			</Container>

			<Section style={footer}>
				<Text style={footerText}>
					You&apos;re receiving this email because your subscribed to Newsletter
					App
				</Text>

				<Hr style={footerDivider} />
				<Text style={footerAddress}>
					<strong>Novu Store</strong>, &copy;{" "}
					<Link href='https://novu.co'>Novu</Link>
				</Text>
			</Section>
		</Body>
	</Html>
);

export function renderEmail(inputs: {
	message: string;
	subject: string;
	name: string;
}) {
	return render(<EmailTemplate {...inputs} />);
}

const main = {
	backgroundColor: "#f3f3f5",
	fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
};

const headerContent = { padding: "20px 30px 15px" };

const headerContentTitle = {
	color: "#fff",
	fontSize: "27px",
	fontWeight: "bold",
	lineHeight: "27px",
};

const paragraph = {
	fontSize: "15px",
	lineHeight: "21px",
	color: "#3c3f44",
};

const divider = {
	margin: "30px 0",
};

const container = {
	width: "680px",
	maxWidth: "100%",
	margin: "0 auto",
	backgroundColor: "#ffffff",
};

const footer = {
	width: "680px",
	maxWidth: "100%",
	margin: "32px auto 0 auto",
	padding: "0 30px",
};

const content = {
	padding: "30px 30px 40px 30px",
};

const header = {
	borderRadius: "5px 5px 0 0",
	display: "flex",
	flexDireciont: "column",
	backgroundColor: "#2b2d6e",
};

const footerDivider = {
	...divider,
	borderColor: "#d6d8db",
};

const footerText = {
	fontSize: "12px",
	lineHeight: "15px",
	color: "#9199a1",
	margin: "0",
};

const footerLink = {
	display: "inline-block",
	color: "#9199a1",
	textDecoration: "underline",
	fontSize: "12px",
	marginRight: "10px",
	marginBottom: "0",
	marginTop: "8px",
};

const footerAddress = {
	margin: "4px 0",
	fontSize: "12px",
	lineHeight: "15px",
	color: "#9199a1",
};
```

The code snippet above creates an customizable email template using React Email. You can find more [easy-to-edit inspirations or templates](https://demo.react.email/preview/notifications/vercel-invite-user). The component also accepts a message, subject, and name as props, and fills them into the elements.

Finally, you can run `npm run email` in your terminal to preview the template.

Next, let's integrate the email template to Novu Echo. First, close the React Email server, and run the code snippet below. It opens the [Novu Dev Studio](https://docs.novu.co/echo/concepts/studio) in your browser.

```bash
npx novu-labs@latest echo
```

Create an `echo` folder containing a `client.ts` file within the Next.js app folder and copy this code snippet into the file.

```tsx
import { Echo } from "@novu/echo";
import { renderEmail } from "@/app/emails/email";

interface EchoProps {
	step: any;
	payload: {
		subject: string;
		message: string;
		name: string;
		totalAmount: string;
	};
}
export const echo = new Echo({
	apiKey: process.env.NEXT_PUBLIC_NOVU_API_KEY!,
	devModeBypassAuthentication: process.env.NODE_ENV === "development",
});

echo.workflow(
	"novu-store",
	async ({ step, payload }: EchoProps) => {
		//👇🏻 in-app notification step
		await step.inApp("notify-admin", async () => {
			return {
				body: `${payload.name} just made a new purchase of ${payload.totalAmount} 🎉`,
			};
		});
		//👇🏻 email notification step
		await step.email(
			"email-customer",
			async () => {
				return {
					subject: `${payload ? payload?.subject : "No Subject"}`,
					body: renderEmail(payload),
				};
			},
			{
				inputSchema: {
					type: "object",
					properties: {},
				},
			}
		);
	},
	{
		payloadSchema: {
			type: "object",
			properties: {
				message: {
					type: "string",
					default: "Congratulations! Your purchase was successful! 🎉",
				},
				subject: { type: "string", default: "Message from Novu Store" },
				name: { type: "string", default: "User" },
				totalAmount: { type: "string", default: "0" },
			},
			required: ["message", "subject", "name", "totalAmount"],
			additionalProperties: false,
		},
	}
);
```

The code snippet defines a Novu notification workflow named `novu-store`, which accepts a payload containing the email subject, message, the customer's name and the total amount. 

The workflow has two steps: in-app and email notification. The in-app notification sends a message to the Admin using the notification bell and the email sends a message to the customer’s email.

Next, you need to create an API route for Novu Echo. Within the `api` folder, create an `email` folder containing a `route.ts` file and copy the provided code snippet below into the file.

```tsx
import { serve } from "@novu/echo/next";
import { echo } from "@/app/echo/client";

export const { GET, POST, PUT } = serve({ client: echo });
```

Run `npx novu-labs@latest echo` in your terminal. It will automatically open the Novu Dev Studio where you can preview your workflow and Sync it with the Cloud.


![Novu 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ed2sl38m7zrlgjoj4a6y.gif)


The `Sync to Cloud` button triggers a pop-up that provides instructions on how to push your workflow to the Novu Cloud.


![Novu 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ch8ba7y9klyudmmv9jz.png)


To proceed, run the following code snippet in your terminal. This will generate a unique URL representing a local tunnel between your development environment and the cloud environment.

```bash
npx localtunnel --port 3000
```

Copy the generated link along with your Echo API endpoint into the Echo Endpoint field, click the `Create Diff` button, and deploy the changes.

```bash
https://<LOCAL_TUNNEL_URL>/<ECHO_API_ENDPOINT (/api/email)> 
```

Congratulations! You've just created a Novu workflow from your codebase.


![Novu 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6bdugs6g15y1e7xeixux.png)


Finally, let's create the endpoint that sends the email and in-app notifications when a user makes a payment. Create an `api/send` route and copy the code snippet below into the file:

```tsx
import { NextRequest, NextResponse } from "next/server";
import { Novu } from "@novu/node";

const novu = new Novu(process.env.NOVU_API_KEY!);

export async function POST(req: NextRequest) {
	const { email, name, totalAmount } = await req.json();

	const { data } = await novu.trigger("novu-store", {
		to: {
			subscriberId: process.env.NOVU_SUBSCRIBER_ID!,
			email,
			firstName: name,
		},
		payload: {
			name,
			totalAmount,
			subject: `Purchase Notification from Novu Store`,
			message: `Your purchase of ${totalAmount} was successful! 🎉`,
		},
	});
	console.log(data.data);

	return NextResponse.json(
		{
			message: "Purchase Completed!",
			data: { novu: data.data },
			success: true,
		},
		{ status: 200 }
	);
}
```

The endpoint accepts the customer's email, name, and total amount paid, and triggers the Novu notification workflow to send the required notifications after a payment is successful.

---

## Conclusion

So far, you've learned how to do the following: 

- Implement multiple authentication methods, store, and retrieve data and files from Appwrite.
- Create email templates with React Email, and send in-app and email notifications with Novu.

If you are looking forward to sending notifications within your applications, Novu is your best choice. With Novu, you can add multiple notification channels to your applications, including chat, SMS, email, push, and in-app notifications.

The source code for this tutorial is available here:
[https://github.com/novuhq/ecom-store-with-nextjs-appwrite-novu-and-stripe](https://github.com/novuhq/ecom-store-with-nextjs-appwrite-novu-and-stripe)

Thank you for reading!