TNS
VOXPOP
As a JavaScript developer, what non-React tools do you use most often?
Angular
0%
Astro
0%
Svelte
0%
Vue.js
0%
Other
0%
I only use React
0%
I don't use JavaScript
0%
CI/CD / Programming Languages

Build a Real-Time Bidding System With Next.js and Stream

Stream’s React Chat SDK with the JavaScript client will help us build a bidding app with support for rich messages, image uploads, videos and more.
Apr 16th, 2025 2:00pm by
Featued image for: Build a Real-Time Bidding System With Next.js and Stream
Image from Lemberg Vector studio on Shutterstock.

Real-time applications are becoming more vital than ever in today’s digital world, providing users with instant updates and interactive experiences. Technologies like web sockets and reactive streams enable this type of interaction. They can:

  • Show the presence of users and typing indicators (for instance, when an online user is typing or was last seen).
  • Read receipts and delivery status.
  • Push notifications to users of new messages.
  • Add multimedia support and rich messaging (voice notes, file sharing, etc.).
  • Thread and group conversations.

Creating real-time applications can be tricky. Users expect a high level of interactivity in their applications.

This tutorial will show how to build a real-time bidding web application with Next.js and the React Chat SDK. This application will use Shadcn for a customizable and responsive UI, Stream React components for real-time updates and messaging, Next.js server-side data fetching for fast and secure auction data rendering and Next.js API routes for seamless bid processing. It will support real-time messaging with dedicated chat channels for different products, automatic posting of bids in the chats to keep all users up to date and a smooth, scalable experience for bidders.

Check out a deployed version of the bidding application here: https://stream-bidding-site.vercel.app/

Stream’s React Chat SDK includes the JavaScript client, which will help us build a fully functional bidding application with support for rich messages, reactions, threads, image uploads, videos and all the above features. The StreamChat client abstracts API calls into functions and handles state and real-time updates.

Prerequisites

Before you begin, ensure you have the following:

Project Setup

We’ve set up a starter code to keep this guide concise and user-friendly. It includes static data, letting you instantly view all products available for bidding without hassle. For the streaming features, we’ll set up Stream React SDKs and the Stream Chat API, so we won’t need to build from scratch. We’ve also integrated Shadcn UI components, delivering a polished interface to explore. With this setup, you’ll hit the ground running.

Start by cloning the starter branch of our repository and installing dependencies:


The starter template is already set up with the required dependencies. When you run pnpm install, it will install:

  • The necessary UI components from Shadcn
  • stream-chat and stream-chat-react,​ which we’ll use to set up Stream’s Chat API and connect our users to the stream

Project Structure

Our project is organized in the following structure:

├── app

│ ├── api # API routes for handling auctions and stream tokens

│ ├── auction # Auction pages for each product

├── components # UI components

├── lib # Utility functions and data

├── public # Static assets

└── types # Type definitions

Step 1: Setting up Stream

Sign up for Stream and create an application. Retrieve your API key and secret from the dashboard.

Stream dashboard

Create a .env.development.local file and add the variables:

Step 2: Implementing User Authentication

This API route app/api/stream-tone/route.ts is responsible for generating a Stream Chat authentication token for a user who wants to participate in a bidding auction. It ensures the user exists, creates a messaging channel if necessary, and adds the user to the auction. Here’s how it works:

We retrieve the apiKey and apiSecret from environment variables. If either is missing, we return an error response:


Next, we parse the incoming request body to extract the userId and productId. If productId is not provided, it defaults to "product-1".


To ensure it’s successful, we check that a valid userId is provided. If not, we return a 400 error indicating the missing or invalid userId.


To retrieve a product, use the provided productId to look it up in the PRODUCTS object. If the product associated with the productId is found, return its details. If no matching product exists in the PRODUCTS object, return a 404 error to indicate that the product could not be found.


To set up the StreamChat functionality, initialize an instance of the StreamChat client by providing the apiKey and apiSecret as parameters. This instance will enable interaction with the StreamChat service using the specified credentials.

const serverClient = StreamChat.getInstance(apiKey, apiSecret);

To manage user data in the chat system, ensure the user is updated by adding or updating their information in the chat service. Use the provided userId to identify the user and assign them the user role during this process. This step guarantees that the user’s details are either created if they don’t exist or updated if they already do.


To set up a bidding channel for an auction, define the channelId using the format auction-${productId}, where productId uniquely identifies the product being auctioned. Then, attempt to create a channel for bidding with this channelId. Include a try block to handle the creation process, catching any exceptions if the channel already exists to avoid errors and ensure smooth execution.


To include the user in the bidding process, add them as a member of the newly created auction channel. Use their userId to register them within the channel, ensuring they have access to participate in the auction activities.

await channel.addMembers([userId]);

Finally, we calculate an expiration time (seven days from now) and generate a token for the user to authenticate with the chat service.


The generated token is logged with its expiration time, and the response includes the token and product data.


If an error occurs at any step, we catch it, log the details and return a 500 error with the error message.

Step 3: Fetching Products

This API route retrieves product details from the PRODUCTS data set. It can return either a single product (by id) or a list of all products.

We will use the URL constructor to extract the id query parameter from the request URL.

const productId = new URL(req.url).searchParams.get("id");

When a productId is provided, search for the corresponding product within the PRODUCTS data using the given identifier. If the product is located, proceed with the retrieved information. If no product matches the provided productId, return a 404 error to indicate that the product could not be found.


If no productId is provided, retrieve all products from the PRODUCTS object by converting it into an array using Object.values(). This will return the complete list of products for further processing or display.

return NextResponse.json(Object.values(PRODUCTS));

If any errors occur during the process (invalid URL, database errors), we catch them and return a 500 error with a message.

Step 4: Implementing Real-Time Bidding

In the API route app/api/finalize-auction/route.ts, finalize an auction by performing two key actions: Send a message to the auction channel to notify participants and update the channel’s status to reflect the auction’s completion.

Begin by extracting the Stream API key and secret from the environment variables. Before proceeding, verify that the NEXT_PUBLIC_STREAM_KEY and STREAM_API_SECRET are available, ensuring the StreamChat client can be properly initialized for these operations.


To enable chat functionality for the auction, create an instance of the StreamChat client by initializing it with the API credentials. Use the Stream API key and secret extracted from the environment variables to set up the client, allowing interaction with the chat service for subsequent operations.

const serverClient = StreamChat.getInstance(apiKey, apiSecret);

To process the auction finalization, extract the productId, winner and amount from the request body. Verify that all these required fields are present in the request to ensure the necessary information is available to complete the operation successfully.


To interact with the auction’s chat, retrieve the associated chat channel using the previously defined channelId (auction-${productId}). Use the StreamChat client instance to access this channel for further actions, such as sending messages or updating its status.

const channel = serverClient.channel("messaging", auction-${productId});

Conclude the auction process by sending a message via the chat channel to announce the result. Use the StreamChat client and the retrieved channel to broadcast the outcome, including details such as the winner and amount, informing all channel members of the finalized auction.


Finalize the auction’s status by updating the channel metadata to mark it as completed. Modify the channel’s data using the StreamChat client, setting an appropriate field (status: 'completed') to reflect that the auction has concluded.


To confirm the auction process’s completion, return a successful response to the requester. Use an appropriate HTTP status code (e.g., 200 OK) and a message or data indicating the auction has been finalized.

return NextResponse.json({ success: true, message: "Auction finalized successfully" });

To handle potential issues during the auction finalization, wrap the process in a try-catch block. If any errors occur, catch them and return an error response with an appropriate HTTP status code (500 for server errors) and a message detailing the issue, ensuring the requester is informed of the failure.

Step 5: All Products Interface

On this page, we create an async server component page.tsx at the root of our layout that fetches products during server-side rendering.

It uses the modern Next.js data fetching pattern with a dedicated getProducts function.

The imported client components will manage our UI states, search functionality and refresh actions.

This component also contains a dedicated loading skeleton and Suspense for better loading state management.

> 💡

We have already created the ProductListClient.tsx and ProductsPageSkeleton.tsx files in the starter template within our components directory. You just need to import and use them here.


At this point, run the development server, go to http://localhost:3000 in a browser, and you should see the list of products:

List of products

Step 6: Bidding Page Interface

This page will act as our data-fetching layer for the bidding page of a single product. It will retrieve the product information on the server and delegate the task of rendering the UI to the client component.

In the snippet, the page is accessed via a dynamic route from the previous page that displayed all products. The productId extracted from the URL parameters is used in the getProductById function that we already set up to fetch a single product’s data.

In the next section, we’ll implement the ClientBiddingPage, which holds the client logic of Stream API and all the UI components for our bidding chat interface.

Server-side benefits: Fetching data on the server reduces client-side load, improves SEO and allows direct access to backend resources.

Step 7: Client Bidding Page

This section contains the real-time auction logic implemented using StreamChat. This particular component will allow users to:

  • Join the auction rooms for a specific product.
  • View product details and auction status.
  • Place bids in real time.
  • Chat with other participants.
  • Track time remaining and auction results.

Managing Bidding State

It’s important to track the connection status with StreamChat, the current auction state, such as bids and remaining time, user interface states and the user’s identity.

Initial Setup on Component Mount

To set up a function that will generate a random user ID, since we have not set up an authentication system in this demo, we’ll need to have this done when the component mounts.

Based on previous conversations, this effect will also set the initial bid amount and check whether the auction has ended.

The Auction Timer

Set up a timer that updates every second. This will ensure that it calculates the remaining time until an auction ends and automatically declares a winner when time runs out.

Connecting to Stream Chat

To connect to a StreamChat, we’ll need to fetch a token from our app/api/stream-token/route.ts and initialize the Stream Chat Client.

This function, which a button triggers, will set up connection monitoring for automatic reconnection and join an auction channel once connected.

Joining the Auction Channel

This function will create a chat channel unique to the product and load the message history to find the current highest bid. It will also register us as real-time listeners for new bids and auction status updates and parse bid information from regex messages.

Placing Bids

This bidding method will validate a bid amount by checking if it’s a number higher than the current bid. It will also prevent us from placing bids on ended auctions and will send the bid as a formatted message to the channel. It also updates the local state to reflect the new bid.

Auction Finalization

Since the aim of an auction is to sell to the highest bidder, this section will include the metadata about the winning bid and call our final-auction API to record the auction result.

StreamChat’s Chat Interface

app/components/ChatInterface.tsx

We have a separate reusable component designed to simplify handling complex real-time communication behind the scenes. It’s a thin wrapper around Stream Chat’s React components that adds auction-specific logic, like disabling chat after the auction ends.

This is the structure of the props that we pass to it:

  • client: The Stream Chat client instance that handles the connection
  • channel: The specific auction chat channel
  • isJoining and isConnecting: Status flags for UI feedback
  • handleConnect: Function to connect the user to the chat
  • isAuctionEnded: Flag to disable chat input when the auction ends

It uses the Stream Chat React SDK components that will handle all the complex real-time messaging functionality:

Rendering the UI

In the return statement, you’ll just need to add the UI layout we made. We have separated concerns into modular components that do the following:

  • ProductDetails: This component shows the details of a particular auction item, and we just parse the data from this component on the server page we made.
  • AuctionStatus: It shows the status of the component from when it started, the countdown timer, the user and the winner.
  • BiddingInterface: This contains the input field for entering our bid, and the button lets us establish a connection to a stream chat.
  • ChatInterface: This provides a real-time chat feature for the auction. These handle all the complex real-time messaging functionality.


Finally, run the development server, go to http://localhost:3000 in a browser, and you should see the list of products. Then click a single product and place a bid:

Step 8: Deploying to Vercel From the Terminal

This is a straightforward step-by-step guide for deploying to Vercel using the command line. It includes setting the environment variables straight from our CLI.

Install the Vercel CLI

npm install -g vercel

Log In to Vercel

vercel login

At the root of our bidding project, just prompt:

vercel

And you will get a prompt with a few questions to set up your project:

Adding Environment Variables

vercel env add

Extending the Application

This application can be further improved by adding new features to make it a real-time, sophisticated application:

  • Add user authentication and replace the random IDs we used with proper user accounts.
  • Integrate payment gateways for automatic checkout.
  • Add notifications to notify users of bidding events and the auction end.
  • Add admin controls for sellers to monitor and manage auctions.
  • Add visualizations of bid history over time.

Conclusion

In this article, you’ve learned how to build a bidding application and set up Next.js API routes that handle server communications. This familiarity has made it easy for us to build a complex bidding application without independently setting up complex real-time messaging functionalities. We’ve also seen that Stream can power live bid feeds, notify users of outbids or even enable chat between bidders, enhancing engagement.

This stack leverages each tool’s strengths to create a robust, user-friendly experience in a bidding app where speed, reliability and real-time interaction are non-negotiable.

Group Created with Sketch.
TNS owner Insight Partners is an investor in: Real.
TNS DAILY NEWSLETTER Receive a free roundup of the most recent TNS articles in your inbox each day.