DEV Community

Cover image for Build a Real-Time Voting System with Strapi & Instant DB: Part 1
Strapi for Strapi

Posted on • Edited on • Originally published at strapi.io

Build a Real-Time Voting System with Strapi & Instant DB: Part 1

Introduction

Applications with real-time features are a vital part of today's digital life.
Many everyday apps and services have integrated real-time features. Examples include instant messaging applications like Telegram which allows you to send and receive messages in real-time, and Google Meet, which enables you to make video calls with other people in real time.
These real-time features are very engaging and provide a great user experience.

In this tutorial, we'll explore how to build a real-time voting app with Strapi 5, Instant DB, and Next.js.
Strapi CMS will handle the backend, managing users, polls, and results, Instant DB will manage the real-time data updates and Next.js will be used to build the user interface for the application.

For reference purposes, here's the outline of this blog series:

  • Part 1: Integrate Instant DB and Strapi for Creating Votes.
  • Part 2: Connecting the Next.js frontend with Strapi and Instant DB Client

Overview

In this tutorial, we'll cover the basics of Strapi as a Headless CMS, what that means, and how we can set up our own Strapi instance. We'll also cover the basics of Instant DB and how we can set that up for real-time data updates. The frontend framework of choice, Next.js will also be briefly discussed and then set up.
We'll then explore how all these technologies will be used to build a real-time voting application.

Goals

At the end of this tutorial:

  • We will have created a working voting system where users can sign in
  • Users can vote on a poll that updates in real-time.
  • We will learn how to quickly start a Strapi instance.
  • Customize the Strapi backend
  • Integrate Strapi CMS with an external service, Instant DB.
  • Connect Instant DB with a Next.js frontend application that will be served to users.

Here is what we are building:

Demo of voting system with Strapi and Instant DB.gif

Introduction to the technologies

Let's talk about the technologies we'll be using:

Strapi

Strapi is a popular headless CMS that allows us to customize the APIs it provides depending on the needs of our project and can be consumed by any frontend framework of our choice.

Instant DB

A real-time database service that makes it easy to store, manage, and sync data across multiple clients instantly. It provides real-time syncing of data across clients with minimal configuration, making it perfect for use cases like live voting, collaborative editing, and real-time messaging.

Next.js

This is our front-end framework of choice for this tutorial. It is a React-based framework with powerful features, including file-based routing, built-in CSS support, and API routes.

Prerequisites

Before we begin, make sure you have the following ready:

  1. Basic JavaScript and React Knowledge: You should be comfortable working with JavaScript and React, as we'll be using React via Next.js for the frontend.
  2. Node.js and npm: Install Node.js and npm to run Strapi and Next.js.
  3. Instant DB Account: Sign up for Instant DB and get your app ID for real-time updates.
  4. A Code Editor: Use any text editor, but Visual Studio Code is recommended.

Step 1: Setting up Strapi 5

We'll start by creating the backend for our project using Strapi 5.
Create a central folder that will hold both the backend and frontend projects. Create a folder called voting.

mkdir voting
Enter fullscreen mode Exit fullscreen mode

Then, we move into the folder:

cd voting
Enter fullscreen mode Exit fullscreen mode

Create the Strapi project by running any of the commands below

# yarn
yarn create strapi-app votes-api --quickstart
# npx
npx create-strapi@latest votes-api
# pnpm
pnpm create strapi votes-api
Enter fullscreen mode Exit fullscreen mode

This command will take us through a few prompts:

Need to install the following packages:
create-strapi@5.0.0
Ok to proceed? (y) y


 Strapi   v5.0.0 🚀 Let's create your new project


We can't find any auth credentials in your Strapi config.

Create a free account on Strapi Cloud and benefit from:

- ✦ Blazing-fast ✦ deployment for your projects
- ✦ Exclusive ✦ access to resources to make your project successful
- An ✦ Awesome ✦ community and full enjoyment of Strapi's ecosystem

Start your 14-day free trial now!


? Please log in or sign up. Skip
? Do you want to use the default database (sqlite) ?
Yes
? Start with an example structure & data? No
? Start with Typescript? Yes
? Install dependencies with npm? Yes
? Initialize a git repository? Yes

 Strapi   Creating a new application at /Users/miracleio/Documents/writing/strapi/building-a-real-time-voting-system-with-strapi-v5-and-instantdb/votes-api

   deps   Installing dependencies with npm


added 1458 packages, and audited 1459 packages in 3m

181 packages are looking for funding
  run `npm fund` for details

15 vulnerabilities (11 moderate, 4 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

       ✓  Dependencies installed

    git   Initializing git repository.

       ✓  Initialized a git repository.

 Strapi   Your application was created!
          Available commands in your project:

          Start Strapi in watch mode. (Changes in Strapi project files will trigger a server restart)
          npm run develop

          Start Strapi without watch mode.
          npm run start

          Build Strapi admin panel.
          npm run build

          Deploy Strapi project.
          npm run deploy

          Display all available commands.
          npm run strapi

          To get started run

          cd voting/voting-api
          npm run develop
Enter fullscreen mode Exit fullscreen mode

Now, we can start our server with

cd voting-api
npm run develop
Enter fullscreen mode Exit fullscreen mode

This starts the Strapi server at http://localhost:1337 and builds the admin dashboard.

strapi dashboard.jpeg

Here, we'll enter our details to create a new admin user.

Strapi admin.jpeg

Now that we've set up our Strapi Admin, we can create our content types.

Creating the Poll Collection Content Type

Navigate to the Content Type Builder by clicking the Content-Type Builder icon on the menu bar at the right of the admin dashboard.

In the Content-Type Builder page, click on the + Create new collection type option.
First, in the Configurations, enter the display name for our new collection type: poll, the singular and plural API ID will automatically be generated. Click on Continue to proceed.

create collection.png

Next, we start creating fields for our new collection type.

create collection fields.png

The fields we'll be creating are:

  • Question:
    • Type: Text (Short Text)
    • Name: question

Click on Add another field to proceed.

add another field.png

  • User:
    • Type: Relation (Many-to-One)
    • Name: user
    • Reference: User (from users-permissions)

Click on Finish to proceed.

user and polls relation.png

With that, we should have something like this as our Poll Collection Type structure:

Poll collection and fields.png

Click on Save and the server will restart due to changes our configuration made in the codebase.

Creating the Option Collection Content Type

Click on the + Create new collection type option again and enter the configuration for the Option collection.

  • Display Name: option

create option collection.png

Next, we can create fields for our new collection type:

  • Value:
    • Type: Text (Short Text)
    • Name: value
  • Poll:
    • Type: Relation (Many-To-One)
    • Name: poll
    • Reference: Poll

The set up for the relation field should look something like this:

poll and options relation.png

With that, we can click on Finish. Then click on Save to save the collection type and restart the server.

Creating the Vote Collection Content Type

Click on the + Create new collection type option again and enter the configuration for the Vote collection.

  • Display Name: vote

create vote collection.png

Next, we can create fields for our Vote collection type:

  • Option:
    • Type: Relation (Many-To-One)
    • Name: option
    • Reference: Option
  • Poll:
    • Type: Relation (Many-To-One)
    • Name: poll
    • Reference: Poll
  • User:
    • Type: Relation (Many-To-One)
    • Name: user
    • Reference: User (from user-permissions)

With that, we should have something like this as our collection content structure:

vote collection and fields.png

Click on Save to save the Vote collection type and restart the server.

Allowing API access for Authenticated users

To make authenticated API calls, we'll need to edit the Authenticated role on the Users & Permissions plugn.

Navigate to Settings > Roles > Authenticated and in the Permissions, enable all permissions for Option, Poll, and Vote.

Allowing API access for Authenticated users.png

Click on Save to save changes.

Step 2: Customizing the Strapi Backend

In the following steps, we'll be leveraging Strapi's flexible and customizable structure to create custom middleware and customized routes that suit our needs.

Adding User fields to the Poll data at entry creation

By default, Strapi does not add the user relation when creating a new poll entry via the API. We can add the relation by connecting the two entities, Poll and User at Poll creation. You can learn more about Managing relations with API requests from the docs.

First, we'll need to create a custom middleware - on-poll-create for the Poll API. Run the following command in your terminal:

npx strapi generate
Enter fullscreen mode Exit fullscreen mode

Provide values for the accompanying prompts as follows:

npx strapi generate
? Strapi Generators middleware - Generate a middleware for an API
? Middleware name on-poll-create
? Where do you want to add this middleware? Add middleware to an existing API
? Which API is this for? poll
✔  ++ /api/poll/middlewares/on-poll-create.ts
Enter fullscreen mode Exit fullscreen mode

Now, in the newly created ./src/api/poll/middlewares/on-poll-create.ts, enter the following:

// ./src/api/poll/middlewares/on-poll-create.ts
/**
 * `on-poll-create` middleware
 * This middleware executes logic when a new poll is created.
 */
import type { Core } from "@strapi/strapi";
// Export the middleware function, accepting config and Strapi instance
export default (config, { strapi }: { strapi: Core.Strapi }) => {
  // Return an asynchronous function that takes in context (ctx) and next middleware
  return async (ctx, next) => {
    // Log a message indicating we are in the on-poll-create middleware
    strapi.log.info("In on-poll-create middleware.");
    // Retrieve the current user from the context's state
    const user = ctx.state.user;
    // Proceed to the next middleware or controller
    await next();
    try {
      // Update the user's document in the database to connect the new poll
      await strapi.documents("plugin::users-permissions.user").update({
        documentId: user.documentId, // Use the user's document ID
        data: {
          polls: {
            connect: [ctx.response.body.data.documentId], // Connect the new poll's ID
          } as any, // Type assertion to any for compatibility
        },
      });
    } catch (error) {
      // Log the error to the console
      console.log(error);
      // Set the response status to 400 (Bad Request)
      ctx.response.status = 400;
      // Provide a response body with error details
      ctx.response.body = {
        statusCode: 400,
        error: "Bad Request",
        message: "An error occurred while connecting the poll to the user.",
      };
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

From the code above, we define a middleware function that is going to be triggered upon the creation of a new poll.

It retrieves the current user from ctx.state.user, ensuring that we have the relevant user context. After calling await next(), which allows the request to proceed to the subsequent middleware or controller, it attempts to update the user's document in the database using strapi.documents("plugin::users-permissions.user").update.

This function connects the new poll's ID (accessible via ctx.response.body.data.documentId) to the user's record by adding it to the polls array. If an error occurs during this update process, it catches the error, logs it, sets the response status to 400 (Bad Request), and returns a detailed error message in the response body.

To add this middleware to our Polls route, open ./src/api/poll/routes/poll.ts and enter the following:

// ./src/api/poll/routes/poll.ts
/**
 * poll router
 * This file defines the routing for the poll API, setting up routes and middlewares.
 */
import { factories } from "@strapi/strapi";
// Export the core router for the "poll" API using Strapi's factories
export default factories.createCoreRouter("api::poll.poll", {
  config: {
    // Configuration options for the router
    create: {
      // Attach the `on-poll-create` middleware to the create route
      middlewares: ["api::poll.on-poll-create"],
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Here, we’re utilizing Strapi’s factories.createCoreRouter function to create a core router specifically for the “poll” API. In the configuration, we’ve attached the on-poll-create middleware to the create route, which will attach the user relation when a new poll is created.

Let's see it in action:

Adding User fields to the Poll data at entry creation.png

Here's the command line equivalent, make sure to add the Authoriaztion header:

curl  -X POST \
  'http://localhost:1337/api/polls?populate=*' \
  --header 'Accept: */*' \
  --header 'User-Agent: Thunder Client (https://www.thunderclient.com)' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNzI3NjA2ODAxLCJleHAiOjE3MzAxOTg4MDF9.UudUAIcX8dMyqX-pqfRQKweQoDjqBavkjdLNYsYdQ0A' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNzI3NjA2ODAxLCJleHAiOjE3MzAxOTg4MDF9.UudUAIcX8dMyqX-pqfRQKweQoDjqBavkjdLNYsYdQ0A' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "data": {
    "question": "Can the avengers beat the Gaurdians?"
  }
}'
Enter fullscreen mode Exit fullscreen mode

Here's the response:

{
  "data": {
    "id": 2,
    "documentId": "to5k2s0211nbrsn0vchvszaj",
    "question": "Can the avengers beat the Gaurdians?",
    "createdAt": "2024-09-29T12:06:07.605Z",
    "updatedAt": "2024-09-29T12:06:07.605Z",
    "publishedAt": "2024-09-29T12:06:07.617Z",
    "locale": null,
    "options": [],
    "votes": [],
    "localizations": []
  },
  "meta": {}
}

Enter fullscreen mode Exit fullscreen mode

Notice, that despite adding the populate=* query parameter, the user relation field is not included in this response. We'll fix this later, but for now, If we check it out in the Strapi Admin, we should see the user who created the poll:

who created the poll.png

Nice.

Adding User fields to the Vote data at entry creation

Similarly, we'll create a custom on-vote-create middleware for the Vote API as well:

npx strapi generate
Enter fullscreen mode Exit fullscreen mode

Provide values for the accompanying prompts:

npx strapi generate
? Strapi Generators middleware - Generate a middleware for an API
? Middleware name on-vote-create
? Where do you want to add this middleware? Add middleware to an existing API
? Which API is this for? vote
✔  ++ /api/vote/middlewares/on-vote-create.ts
Enter fullscreen mode Exit fullscreen mode

In the newly generated ./src/api/vote/middlewares/on-vote-create.ts file, enter the following:

// ./src/api/vote/middlewares/on-vote-create.ts
/**
 * `on-vote-create` middleware
 * This middleware executes logic when a new vote is created.
 */
import type { Core } from "@strapi/strapi";
export default (config, { strapi }: { strapi: Core.Strapi }) => {
  // Add your own logic here.
  return async (ctx, next) => {
    strapi.log.info("In on-vote-create middleware.");
    // Retrieve the current user from the context's state
    const user = ctx.state.user;

    // Proceed to the next middleware or controller
    await next();

    // Retrieve the document ID of the new vote from the response body
    const voteDocumentId = ctx.response.body.data.documentId;

    try {
      // Update the user's document in the database to connect the new vote
      await strapi.documents("plugin::users-permissions.user").update({
        documentId: user.documentId, // Use the user's document ID
        data: {
          votes: {
            connect: [voteDocumentId], // Connect the new vote's ID
          } as any, // Type assertion to any for compatibility
        },
      });
    } catch (error) {
      // Log the error to the console
      console.log(error);
      // Set the response status to 400 (Bad Request)
      ctx.response.status = 400;
      // Provide a response body with error details
      ctx.response.body = {
        statusCode: 400,
        error: "Bad Request",
        message: "An error occurred while connecting the vote to the user.",
      };
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

Here, we're also connecting the newly created vote to the user using the vote's documentId.

For this to work, we have to add it to the Votes route configuration - ./src/api/vote/routes/vote.ts:

// ./src/api/vote/routes/vote.ts
/**
 * vote router
 */
import { factories } from "@strapi/strapi";
export default factories.createCoreRouter("api::vote.vote", {
  config: {
    create: {
      // add the `on-vote-create` middleware to the `create` action
      middlewares: ["api::vote.on-vote-create"],
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Awesome.

Restricting User vote to only one per Poll

To ensure that a user can only vote on a Poll once, we have to check if a vote with the poll relation already exists. We can do this by adding the following code to our custom on-vote-create middleware - ./src/api/vote/middlewares/on-vote-create.ts:

// ./src/api/vote/middlewares/on-vote-create.ts
/**
 * `on-vote-create` middleware
 * This middleware executes logic when a new vote is created.
 */
import type { Core } from "@strapi/strapi";
export default (config, { strapi }: { strapi: Core.Strapi }) => {
  // Add your own logic here.
  return async (ctx, next) => {
    strapi.log.info("In on-vote-create middleware.");
    // Retrieve the current user from the context's state
    const user = ctx.state.user;

    // check if vote document with option and poll already exists
    const vote = await strapi.documents("api::vote.vote").findFirst({
      filters: {
        // Filter by the user's ID and the poll's document ID
        user: {
          id: user.id,
        },
        poll: {
          documentId: ctx.request.body.data.poll,
        } as any,
      },
      populate: ["option", "poll"],
    });
    // If the user has already voted on this poll, return a 400 (Bad Request) response
    if (vote) {
      ctx.response.status = 400;
      ctx.response.body = {
        statusCode: 400,
        error: "Bad Request",
        message: "You have already voted on this poll.",
      };
      return;
    }

    // Proceed to the next middleware or controller
    await next();

    // ...
  };
};
Enter fullscreen mode Exit fullscreen mode

Here, we're using the Document Service API - strapi.documents to find an existing vote document created by the currently authenticated user and is connected to the specified poll. If a document is found, then the user has already submitted a vote for that poll and we return an error response.

Let's see it in action.

First, we create a new vote:

create a new vote.png

Here's the command line equivalent, make sure to add the Authoriaztion header:

curl  -X POST \
  'http://localhost:1337/api/votes?populate=*' \
  --header 'Accept: */*' \
  --header 'User-Agent: Thunder Client (https://www.thunderclient.com)' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNzI3NjI4OTI1LCJleHAiOjE3MzAyMjA5MjV9.OkbbTHqIPuYqbQ0Z2rb9qbOfwowWdoyIqbY-W0V9-MU' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "data": {
    "poll": "ew73j3xcm3elucznhzjcgwl2",
    "option": "czoitcywjm996eh27epzl7mh"
  }
}'
Enter fullscreen mode Exit fullscreen mode

As you can see, this was successful. Now, if we try to run the request again, we get this:

Restricting User vote to only one per Poll.png

Awesome!

Returning User Relation Data in Poll Response

As we mentioned before, Strapi doesn't return the user relation data by defualt when we try populating the fields using the populate=* query parameter.
We have a few methods we can try to show this user relation data, let's quickly go over them.

  1. Enabling Authenticated Access to User Permissions Plugin We'll have to navigate to Settings > Roles (Under Users & Permissions Plugin) > Public.

Here, we can scroll down to Users-permissions and enable count, find and findOne

enable count, find and findOne in User permissions in Strapi.jpeg

This is by far the easiest solution but we can still explore a few more custom solutions.

  1. Populating Creator fields

We can try following this REST API guide on how to populate creator fields but that seems to only work for entries created via the Strapi Admin dashboard.

  1. Customizing the Poll controller That being said, our next solution could be to customize the default Poll controller at ./src/api/poll/controllers/poll.ts:
// ./src/api/poll/controllers/poll.ts
/**
 * poll controller
 * This controller extends the default functionality for the poll API.
 */
import { factories } from "@strapi/strapi";
// Export the customized poll controller
export default factories.createCoreController(
  "api::poll.poll", // Define the controller for the "poll" API
  ({ strapi }) => ({
    // Custom find method that retrieves multiple polls with additional user and vote data
    async find(ctx) {
      // Call the base "find" method from the core controller
      const response = await super.find(ctx);
      // For each poll in the response data, fetch related documents with detailed user and vote information
      await Promise.all(
        response.data.map(async (poll) => {
          // Retrieve the poll document with populated fields for votes and user data
          const pollDocument = await strapi
            .documents("api::poll.poll") // Access the "poll" collection
            .findOne({
              documentId: poll.documentId, // Find by the poll's document ID
              populate: {
                votes: {
                  populate: {
                    option: {
                      fields: ["id", "value"], // Include "id" and "value" for each option
                    },
                    user: {
                      fields: ["id", "username", "email"], // Include "id", "username", and "email" for each user
                    },
                  },
                },
                user: {
                  fields: ["id", "username", "email"], // Include poll creator's "id", "username", and "email"
                },
              },
            });
          // Add the user details to the poll response
          poll.user = {
            id: pollDocument.user.id,
            documentId: pollDocument.user.documentId,
            username: pollDocument.user.username,
            email: pollDocument.user.email,
          };
          // Assign the populated votes data to the poll
          poll.votes = pollDocument.votes;
        })
      );
      // Return the modified response with additional data
      return response;
    },
    // Custom findOne method to retrieve a single poll by its ID with populated user and vote information
    async findOne(ctx) {
      // Call the base "findOne" method from the core controller
      const response = await super.findOne(ctx);
      // Fetch the poll document and its associated user and vote data
      const pollDocument = await strapi.documents("api::poll.poll").findOne({
        documentId: response.data.documentId, // Use the poll's document ID to find it
        populate: {
          votes: {
            populate: {
              option: {
                fields: ["id", "value"], // Include vote option details
              },
              user: {
                fields: ["id", "username", "email"], // Include user details for each vote
              },
            },
          },
          user: {
            fields: ["id", "username", "email"], // Include poll creator's details
          },
        },
      });
      // Attach the user details to the response
      response.data.user = {
        id: pollDocument.user.id,
        documentId: pollDocument.user.documentId,
        username: pollDocument.user.username,
        email: pollDocument.user.email,
      };
      // Attach the votes to the response
      response.data.votes = pollDocument.votes;
      // Return the modified response with user and vote information
      return response;
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

Here, we have a custom controller for the "Poll" API, built using the createCoreController factory method.

We override two methods: find and findOne, which are responsible for fetching multiple polls and a single poll, respectively. Both methods extend the default functionality by using super , then we fetch additional details about the poll creator (user) and associated votes.

The find method iterates over all retrieved polls, retrieves user and vote data from the database, and populates this information into the poll objects. Similarly, the findOne method fetches detailed information about a specific poll's creator and its votes. By doing so, we have more comprehensive information available on the front end, such as user IDs, emails, and vote details.

In the next section, we'll dive into how we can integrate Instant DB into our API by customizing the Strapi backend.

Step 3: Setting up Instant DB

To get started with Instant DB, create a new account if you don't have one already at https://www.instantdb.com/dash.

login to instantdb.png

You can go through the onboarding process to create your app:

name your app in instantdb.png

On the home page of the dashboard, you should see the App ID at the top:

get instantdb app id .png

To obtain your Admin secret key, navigate to the admin page by clicking on Admin at the side navigation:

get instantdb admin secret.png

Step 4: Integrating Instant DB into Strapi

First, we have to install the InstantDB Admin SDK. Run the following command in your terminal:

npm i @instantdb/admin
Enter fullscreen mode Exit fullscreen mode

Go to your Instant DB Dashboard, create an account if you do not have one already, obtain your app ID as well as your admin token, and place it in your .env file:

# .env
# ...
INSTANT_APP_ID=123456
INSTANT_ADMIN_TOKEN=123456
Enter fullscreen mode Exit fullscreen mode

Next, in the src/index.ts file, modify the code to this:

// ./src/index.ts
// import type { Core } from '@strapi/strapi';
import { init } from "@instantdb/admin";
type InstantDBSchema = {
  votes: {
    user: {
      documentId: string;
      username: string;
      email: string;
    };
    poll: {
      question: string;
      documentId: string;
    };
    option: {
      value: string;
      documentId: string;
    };
    createdAt: string;
  };
};
export const db = init<InstantDBSchema>({
  appId: process.env.INSTANT_APP_ID,
  adminToken: process.env.INSTANT_ADMIN_TOKEN,
});
export default {
  /**
   * An asynchronous register function that runs before
   * your application is initialized.
   *
   * This gives you an opportunity to extend code.
   */
  register(/* { strapi }: { strapi: Core.Strapi } */) {},
  /**
   * An asynchronous bootstrap function that runs before
   * your application gets started.
   *
   * This gives you an opportunity to set up your data model,
   * run jobs, or perform some special logic.
   */
  bootstrap(/* { strapi }: { strapi: Core.Strapi } */) {},
};
Enter fullscreen mode Exit fullscreen mode

From the code block above, we are integrating Instant DB into our project.

First, the Instant DB Admin SDK is imported, and the voting schema - InstantDBSchema is defined to manage real-time voting data (e.g., users, polls, and options). The database is initialized using appId and adminToken from our .env file, establishing a secure connection to Instant DB.

The register and bootstrap functions are placeholders for any additional logic when the Strapi app initializes or starts.

Authenticating Instant DB Users

The admin SDK allows us to “impersonate users", that is make queries on behalf of our users from the backend.

To do this, we'll have to create Instant DB tokens for every user who signs up or logs in through the Strapi API. To do this, we'll have to customize the Users & Permissions Plugin Controllers on the backend.

Create a new file - ./src/extensions/users-permissions/strapi-server.ts and enter the following:

// ./src/extensions/users-permissions/strapi-server.ts
// Import the initialized Instant DB instance (db) to interact with the database
import { db } from "../..";
// Export a function that modifies the default plugin (users-permissions)
module.exports = (plugin) => {
  // Store references to the original register and callback controllers for later use
  const register = plugin.controllers.auth.register;
  const callback = plugin.controllers.auth.callback;
  // Override the default register method to include InstantDB token creation
  plugin.controllers.auth.register = async (ctx) => {
    // Extract the user's email from the registration request body
    const { email } = ctx.request.body;
    // Create an authentication token for the user in InstantDB using their email
    const token = await db.auth.createToken(email);
    // Call the original register function to handle the default registration process
    await register(ctx);
    // Access the response body after registration
    const body = ctx.response.body;
    // Add the InstantDB token to the response body
    body.instantdbToken = token;
  };
  // Override the default callback method (used for login) to include InstantDB token creation
  plugin.controllers.auth.callback = async (ctx) => {
    // Extract the identifier (usually the email or username) from the login request body
    const { identifier } = ctx.request.body;
    // Create an authentication token for the user in InstantDB using the identifier
    const token = await db.auth.createToken(identifier);
    // Call the original callback function to handle the default login process
    await callback(ctx);
    // Access the response body after login
    const body = ctx.response.body;
    // Add the InstantDB token to the response body
    body.instantdbToken = token;
  };
  // Return the modified plugin object with the updated controller methods
  return plugin;
};
Enter fullscreen mode Exit fullscreen mode

In this code block, we enhance our Strapi application by integrating Instant DB for user authentication.

First, we override the default register function with our implementation that extracts the user's email from Strapi Context - ctx.request.body.

Next, we create a token using db.auth.createToken(email) to generate an authentication token for the user.

After calling the original register(ctx) function to ensure the default registration process runs, we add our Instant DB token to the response body with body.instantdbToken = token.
Similarly, we modify the callback function for login by extracting the identifier, creating the token with db.auth.createToken(identifier), and adding it to the response.

This allows us to seamlessly integrate Instant DB’s auth into our authentication workflow.

Now, if we send a register or login request to our server, we should get an additional instantdbToken field.

To register, send a POST request to http://localhost:1337/api/auth/local/register with the following body:

{
  "username": "james",
  "email": "james@gmail.com",
  "password": "Pass1234"
}
Enter fullscreen mode Exit fullscreen mode

With that, we should have something like this:

Authenticating InstantDB Users.png

Here's the response data:

{
  "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNzI3NjA2ODAxLCJleHAiOjE3MzAxOTg4MDF9.UudUAIcX8dMyqX-pqfRQKweQoDjqBavkjdLNYsYdQ0A",
  "user": {
    "id": 2,
    "documentId": "s0z5llgt1cuitio2bv2u7ryz",
    "username": "james",
    "email": "james@gmail.com",
    "provider": "local",
    "confirmed": true,
    "blocked": false,
    "createdAt": "2024-09-29T10:46:41.898Z",
    "updatedAt": "2024-09-29T10:46:41.898Z",
    "publishedAt": "2024-09-29T10:46:41.899Z",
    "locale": null
  },
  "instantdbToken": "659f0759-c2bf-47ca-8818-380d6f2e241b"
}
Enter fullscreen mode Exit fullscreen mode

Notice the additional instantdbToken property in the JSON response, we can use that to make authenticated Instant DB queries and writes in our frontend.

To login, send a POST request to http://localhost:1337/api/auth/local with the following body:

{
  "identifier": "james@gmail.com",
  "password": "Pass1234"
}
Enter fullscreen mode Exit fullscreen mode

With that, we should have something like this:

login user and instantdb token.png

You can also see the token here as well.

Creating Vote records on Instant DB

To create votes on Instant DB when the user submits a vote on the Strapi API, we'll use the db.asUser()and transact methods. In the ./src/api/vote/middlewares/on-vote-create.ts file, add the following:

// ./src/api/vote/middlewares/on-vote-create.ts
/**
 * `on-vote-create` middleware
 * This middleware executes logic when a new vote is created.
 */
import type { Core } from "@strapi/strapi";
import { db } from "../../..";
import { id, tx } from "@instantdb/admin";
export default (config, { strapi }: { strapi: Core.Strapi }) => {
  // Add your own logic here.
  return async (ctx, next) => {
    strapi.log.info("In on-vote-create middleware.");
    // ...

    // Proceed to the next middleware or controller
    await next();

    // Retrieve the document ID of the new vote from the response body
    const voteDocumentId = ctx.response.body.data.documentId;
    // Retrieve the document data from the response body
    const document = ctx.response.body.data;
    // Create a new record in the InstantDB database for the vote
    const res = await db
      .asUser({
        // Use the user's information to create the vote record
        email: user.email,
      })
      .transact(
        tx.votes[id()].update({
          // Use the user's information to create the vote record
          user: {
            documentId: user.documentId,
            username: user.username,
            email: user.email,
          },
          // Use the poll and option information from the vote document
          poll: {
            documentId: document.poll,documentId,
            question: document.poll.question,
          },
          // Use the option information from the vote document
          option: {
            documentId: document.option.documentId,
            value: document.option.value,
          },
          // Use the creation timestamp from the vote document
          createdAt: document.createdAt,
        })
      );
    console.log("🟢🟢🟢🟢 ~ instantDB record created", res);

    // ...
  };
};
Enter fullscreen mode Exit fullscreen mode

Here, we connect to Instant DB using the db.asUser() method, ensuring the vote is created in the database under the user’s identity. The transact function is used to update the votes collection on Instant DB, saving the vote’s details such as the user’s document ID, poll question, option value, and creation timestamp.

Now, if we send a request to create a vote, we should see something like this in our terminal:

instantdb record in the command line.png

Great. Now, if we check our Instant DB dashboard, we should see the records we've created so far:

all instant db records.png

Conclusion

So far, we've been able to set up our Strapi backend by creating our collection types and creating and registering custom middleware to extend Strapi 5 functionality to fit our needs.
We’ve also been able to set up Instant DB admin for creating real-time vote entries on behalf of authenticated users.

Next, we'll create the front end for our project where we'll be able to create polls, vote, and see the changes in real-time.

Resources

Top comments (0)