Build an e-commerce application

This guide shows how you can use Fauna and Next.js to build a simplified e-commerce application.

The application is a marketplace where vendors can create their own stores to add and sell products. Customers can use the marketplace to buy products from different vendors.

You can use the application as a starting point for your own Fauna-powered applications.

The source code for the application is available on GitHub. You can find the live demo here.

Prerequisites

  • Basic knowledge of JavaScript and React

  • Basic knowledge of Fauna

Learning objectives

  • Compose Fauna queries with Fauna Query Language (FQL)

  • Use Fauna in a full-stack application

Data models

This application has the following data models:

  • User: Represents a user of the application

  • Shop: Represents a store created by a user

  • Product: Represents a product added to a store

Fauna is document-relational; it combines the flexibility and familiarity of JSON documents with the relationships and querying power of a traditional relational database.

You have the flexibility to store unstructured data while also getting the structured querying power of traditional SQL databases. You can define data relationships that are one-to-one, one-to-many, and many-to-many in Fauna.

Data model relationships

A user can have many shops. This is a one-to-many relationship.

One-to-many relationship

A shop can have many products. This is a one-to-many relationship.

One-to-many relationship

A shop can have many orders, and a user can order from multiple shops at the same time. There is a many-to-many relationship between shops and users.

Another collection called OrderDetail is used to handle this relationship.

Create a database

To start, create a database in Fauna.

  1. Log in to the Fauna dashboard.

  2. Click Create Database to create a database.

  3. Enter a database Name and enter the other database properties.

  4. Click Create.

Create collections

Next, use the web shell to create collections for your database.

  1. Navigate to Fauna web shell.

  2. To create new collections, write the following FQL code into the web shell and run it.

    Collection.create({ name: "Order" })
    Collection.create({ name: "OrderDetails" })
    Collection.create({ name: "Product" })
    Collection.create({ name: "Shop" })
    Collection.create({ name: "User" })
    Collection.create({ name: "UserProfile" })

    It creates six collections: Order, OrderDetails, Product, Shop, User, and UserProfile. Note the newly created collections in the Explorer window.

Create your data relationships

In this section, you generate data and data relationships in Fauna using FQL. You create one-to-many, many-to-many, and one-to-one relationships in Fauna using FQL queries for the e-commerce application.

One-to-many

Create a new user from the Fauna web shell. Run the following code to create a new user:

User.create({
  "name": "test",
  "email": "test@example.com",
})
{
  id: "360355394276556881",
  coll: User,
  ts: Time("2024-04-01T13:32:06.830Z"),
  name: "test",
  email: "test@example.com"
}

The id and ts are auto generated and may differ per document. Copy the generated id value for the next steps.

To establish a one-to-many relationship between a user and a shop, create a new shop and link it to the user. Use the id value of the user you created in the previous step. Run the following code:

let user = User.byId("<userId>")

Shop.create({
 name: "Sam's Electric Swags",
 description: "Some descrption of the shop",
 countryLocation: "USA",
 owner: user 
})

Shop.create({
 name: "Sam's T shirt store",
 description: "Get cool t shirts here",
 countryLocation: "USA",
 owner: user 
})

To query data in a one-to-many relationship, run the following:

let user = User.byId("<userId>")
Shop.where(.owner == user)

To get a particular shop’s owner, run the following query:

let shop = Shop.byId("<shopId>")

shop {
 owner
}

Many-to-many

In the sample app, a user can buy from many shops and a shop sells to many users. Shop and User collections have a many-to-many relationship through the OrderDetails collection. It works like a join table in SQL. The following code snippet demonstrates how you define it in FQL:

First create couple new products to add to the shop.

Product.create({
    name: 'Really cool t-shirt',
    price: 19.99,
    description: 'This is a product'
})

Product.create({
    name: 'Awesome hat',
    price: 5.99,
    description: 'This is a product'
})

Use the id value of the products you created in the previous step to add them to the shop.

let user = User.byId("<userId>")
let shop = Shop.byId("<shopId>")

OrderDetails.create({
  vendor: shop,
  customer: user,
  products: [
    Product.byId("<productId1>"),
    Product.byId("<productId2>"),
  ]
})

let shop2 = Shop.byId("<shopId>")

OrderDetails.create({
  vendor: shop2,
  customer: user,
  products: [
    Product.byId("<productId1>"),
    Product.byId("<productId2>"),
  ]
})

To find all the shops a user has shopped from, you can run the following FQL query:

let user = User.byId("<userId>")

let details = OrderDetails.all().where(.customer == user)

details {
  vendor
}

One-to-one

The User and UserProfile collections have a one-to-one relationship. A user can have only one profile. This is an example of a one-to-one relationship. The following code snippet demonstrates how you define it in FQL:

let user = User.create({
  name: "Jon Luc Picard",
  email: "picard1@example.com"
})

UserProfile.create({
  address: "123 California Ave",
  user: user
})

Fauna indexes

Fauna uses indexes to efficiently and quickly retrieve and manipulate data based on various criteria defined by you. Fauna indexes help organize and optimize data queries in a Fauna database.

Run the following code in shell to create a byEmail index on the User collection.

This index helps you query users by email more efficiently.

User.definition.update({
  indexes: {
    byEmail: {
      terms: [{ field: "email" }]
    }
  }
})

To verify run the following query.

User.byEmail("test@example.com")

You can convert one of the previous queries to use an index. To find all shops that belongs to a particular user you can create an index.

Shop.definition.update({
  indexes: {
    byShopOwner: {
      terms: [{ "field": "owner" }]
    }
  }
})

You can run the following query to get shops by owner reference.

let owner = User.byId("<userId>")
Shop.byShopOwner(owner)

Using indexes in Fauna is a good practice to optimize your queries. The .all() and .where() functions go through all the documents in the collection to find the result. This can be slow if the collection has a large number of documents. Defining an index on collections and using it in your queries can help you optimize your queries.

User-defined functions

User-defined functions (UDFs) in Fauna are functions that you create and store in your Fauna database.

UDFs allow you to encapsulate logic and business rules in a centralized location, making it easier to manage and reuse across your applications. You can turn the product search logic from the previous section into a UDF by running the following FQL code.

Function.create({
  name: "SearchProduct",
  role: "server",
  body: "(priceMin, priceMax, searchTerm) => {
    Product.all().where(
     (.price > priceMin && .price < priceMax ) && 
     (.name != null && .name.toLowerCase().includes(searchTerm))
    )
  }"
})

Then, you can call the function as follows.

SearchProduct(10, 300, "shirt")

User authentication and ABAC

Fauna provides ABAC (Attribute-Based Access Control) to secure your data. This section demonstrates how to implement basic user authentication and ABAC in Fauna.

Securing application with ABAC

Instead of using the server key in your application you use a public key with limited privileges. The public key assumes a role that has privileges to do only read operations in all the Collection. Every other operation is restricted for this key.

Following diagram gives you a visualization of this.

ABAC

Using the shell create a new Role called UnAuthenticatedRole. Next create a new key for the this role. Run the following FQL code to create the Role and the key.

Role.create({
  name: "UnAuthenticatedRole",
  privileges: [
    {
      resource: "User",
      actions: {
        read: true
      }
    },
    {
      resource: "Product",
      actions: {
        read: true
      }
    },
    {
      resource: "Shop",
      actions: {
        read: true
      }
    }
  ]
})

let unAuthPubliKey = Key.create({ role: "UnAuthenticatedRole" })
unAuthPubliKey
{
  "id": "358243630271430723",
  "coll": "Key",
  "ts": "2023-03-04T05:09:59.240Z",
  "secret": "fnAE-LyvBWAAQ_rdkz_DbDwSpgHt1pE5rRtweu4y",
  "role": "UnAuthenticatedRole"
}

The returned secret is your application’s public key (it assumes all the permissions granted to the UnAuthenticatedRole user role). This is the key you use to register and log-in users in your application. This key has read-only permission to all your collections. It can not be used to create, update, or delete any document in any collection.

The unauthenticated users need a way to signup and login into your application. You create two user defined functions Signup and Login for this.

User signup

Create a new UDF called Signup to allow users to register in your application.

Signup

Creating the signup function:

Run the following FQL code to create the Signup function.

The FQL function body is written like a standard JavaScript arrow function. When you call the function from your application you provide username, email and password as parameters; you are able to define any other parameters you may need here as well. In the function body you have the Credentials.create built in Fauna function, which securely encrypts and hides the password.

Function.create({
  name: "Signup",
  role: "server",
  body: "(username, email, password) => {
    let user = User.create({ name: username, email: email })
    Credentials.create({ document: user, password: password })
  }"
})
{
  "name": "Signup",
  "coll": "Function",
  "ts": "2023-03-05T18:31:16.835Z",
  "body": "(username, email, password) => {\n    let user = User.create({ name: username, email: email })\n    Credentials.create({ document: user, password: password })\n  }",
  "role": "server"
}

You can call the function in the shell to test it.

In your application you provide the key for the UnAuthenticatedRole role. The UnAuthenticatedRole role should have the Call privilege to the Signup function. Remember that the UnAuthenticatedRole role should have read-only access to all collections.

Apply these privileges to the UnAuthenticatedRole role by running the following Fauna code.

User login

Create a new UDF called Login to allow users to log in to your application. Similar to the Signup function, the Login function takes email and password as parameters.

User.definition?.update({
  indexes: {
    byEmail: {
      terms: [{ field: "email" }]
    }
  }
})

Function.create({
  name: "Login",
  role: "server",
  body: "(email, password) => {
    let user = User.byEmail(email).first()
    Credentials.byDocument(user)?.login(password)
  }" 
})
{
  "name": "Login",
  "coll": "Function",
  "ts": "2023-03-05T19:02:35.535Z",
  "body": "(email, password) => {\n    let user = User.byEmail(email).first()\n    Credentials.byDocument(user).login(password)\n  }",
  "role": "server"
}

If email and password match the data saved in the database then Fauna issues a temporary key to the application. This key assumes the AuthenticatedRole role.

The following diagram demonstrates this flow:

Login

Run the following FQL code to create the AuthenticatedRole role.

Role.create({
  name: "AuthenticatedRole",
  membership: {
    resource: "User"
  },
  privileges: [
    {
      resource: "User",
      actions: {
        read: true,
        create: true,
        write: true,
        delete: true
      }
    },
    {
      resource: "Shop",
      actions: {
        read: true,
        create: true,
        write: true,
        delete: true
      }
    },
    {
      resource: "Product",
      actions: {
        read: true,
        create: true,
        write: true,
        delete: true
      }
    }
  ]
})

Run the Login function in the shell to test it.

{
  "id": "358257703275987011", //this id is unique and will be different for you
  "coll": "Token",
  "ts": "2023-03-04T08:53:40.310Z",
  "secret": "...",
  "document": {
    "id": "358250589763665987",
    "coll": "User",
    "ts": "2023-03-04T07:00:36.380Z",
    "name": "JonLuc",
    "email": "picard@tng.com"
  }
}

The Login function returns a temporary key that you can use to access the database resources. This key has the permissions defined in the AuthenticatedRole.

The UnAuthenticatedRole role should have the Call privilege to the Login and Signup function and read privileges to User and Shop collections. Run the following FQL code to apply the privileges to the UnAuthenticatedRole role.

Run the app

To run the app:

  1. Clone the repository from GitHub.

  2. Create a new .env file in the root of your application and add the following environment variables. Generate a new secret key for your unauthenticated role by running the following Fauna code in the shell:

    Key.create({
      role: "UnAuthenticatedRole"
    })
    {
      id: "394568774441762893",
      coll: Key,
      ts: Time("2024-04-08T04:02:36.250Z"),
      secret: "fnAFeco1uBAATZPOuclEAJ11MMVT_EDxGXy1vyb4",
      role: "UnAuthenticatedRole"
    }

    Copy the secret key and add it to your .env file. Also include the NEXT_PUBLIC_FAUNA_ENDPOINT environment variable.

    NEXT_PUBLIC_FAUNA_SECRET="<Secret for UnAuthenticated Role>"
    NEXT_PUBLIC_FAUNA_ENDPOINT="https://db.fauna.com/"

Note that the NEXT_PUBLIC_FAUNA_SECRET is the key of your UnAuthenticatedRole.

  1. Run the following commands in your terminal:

    cd fauna-shop-app
    npm install
    npm run dev

When the application is running, you can access it at http://localhost:3000. Go ahead and create a new user, add a shop, and add products to the shop. When you create a new user in the application you can see the user in the Fauna dashboard.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!