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:
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.
At the end of this tutorial:
Here is what we are building:
Let's talk about the technologies we'll be using:
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.
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.
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.
Before we begin, make sure you have the following ready:
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
Then, we move into the folder:
cd voting
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
This command will take us through a few prompts:
1Need to install the following packages:
2create-strapi@5.0.0
3Ok to proceed? (y) y
4
5
6 Strapi v5.0.0 🚀 Let's create your new project
7
8
9We can't find any auth credentials in your Strapi config.
10
11Create a free account on Strapi Cloud and benefit from:
12
13- ✦ Blazing-fast ✦ deployment for your projects
14- ✦ Exclusive ✦ access to resources to make your project successful
15- An ✦ Awesome ✦ community and full enjoyment of Strapi's ecosystem
16
17Start your 14-day free trial now!
18
19
20? Please log in or sign up. Skip
21? Do you want to use the default database (sqlite) ?
22Yes
23? Start with an example structure & data? No
24? Start with Typescript? Yes
25? Install dependencies with npm? Yes
26? Initialize a git repository? Yes
27
28 Strapi Creating a new application at /Users/miracleio/Documents/writing/strapi/building-a-real-time-voting-system-with-strapi-v5-and-instantdb/votes-api
29
30 deps Installing dependencies with npm
31
32
33added 1458 packages, and audited 1459 packages in 3m
34
35181 packages are looking for funding
36 run `npm fund` for details
37
3815 vulnerabilities (11 moderate, 4 high)
39
40To address issues that do not require attention, run:
41 npm audit fix
42
43To address all issues (including breaking changes), run:
44 npm audit fix --force
45
46Run `npm audit` for details.
47
48 ✓ Dependencies installed
49
50 git Initializing git repository.
51
52 ✓ Initialized a git repository.
53
54 Strapi Your application was created!
55 Available commands in your project:
56
57 Start Strapi in watch mode. (Changes in Strapi project files will trigger a server restart)
58 npm run develop
59
60 Start Strapi without watch mode.
61 npm run start
62
63 Build Strapi admin panel.
64 npm run build
65
66 Deploy Strapi project.
67 npm run deploy
68
69 Display all available commands.
70 npm run strapi
71
72 To get started run
73
74 cd voting/voting-api
75 npm run develop
Now, we can start our server with
cd voting-api
npm run develop
This starts the Strapi server at http://localhost:1337
and builds the admin dashboard.
Here, we'll enter our details to create a new admin user.
Now that we've set up our Strapi Admin, we can create our content types.
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.
Next, we start creating fields for our new collection type.
The fields we'll be creating are:
question
Click on Add another field to proceed.
user
Click on Finish to proceed.
With that, we should have something like this as our Poll Collection Type structure:
Click on Save and the server will restart due to changes our configuration made in the codebase.
Click on the + Create new collection type option again and enter the configuration for the Option collection.
option
Next, we can create fields for our new collection type:
value
poll
The set up for the relation field should look something like this:
With that, we can click on Finish. Then click on Save to save the collection type and restart the server.
Click on the + Create new collection type option again and enter the configuration for the Vote collection.
vote
Next, we can create fields for our Vote collection type:
option
poll
user
With that, we should have something like this as our collection content structure:
Click on Save to save the Vote collection type and restart the server.
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.
Click on Save to save changes.
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.
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
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
Now, in the newly created ./src/api/poll/middlewares/on-poll-create.ts
, enter the following:
1// ./src/api/poll/middlewares/on-poll-create.ts
2/**
3 * `on-poll-create` middleware
4 * This middleware executes logic when a new poll is created.
5 */
6import type { Core } from "@strapi/strapi";
7// Export the middleware function, accepting config and Strapi instance
8export default (config, { strapi }: { strapi: Core.Strapi }) => {
9 // Return an asynchronous function that takes in context (ctx) and next middleware
10 return async (ctx, next) => {
11 // Log a message indicating we are in the on-poll-create middleware
12 strapi.log.info("In on-poll-create middleware.");
13 // Retrieve the current user from the context's state
14 const user = ctx.state.user;
15 // Proceed to the next middleware or controller
16 await next();
17 try {
18 // Update the user's document in the database to connect the new poll
19 await strapi.documents("plugin::users-permissions.user").update({
20 documentId: user.documentId, // Use the user's document ID
21 data: {
22 polls: {
23 connect: [ctx.response.body.data.documentId], // Connect the new poll's ID
24 } as any, // Type assertion to any for compatibility
25 },
26 });
27 } catch (error) {
28 // Log the error to the console
29 console.log(error);
30 // Set the response status to 400 (Bad Request)
31 ctx.response.status = 400;
32 // Provide a response body with error details
33 ctx.response.body = {
34 statusCode: 400,
35 error: "Bad Request",
36 message: "An error occurred while connecting the poll to the user.",
37 };
38 }
39 };
40};
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"],
},
},
});
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:
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?"
}
}'
Here's the response:
1{
2 "data": {
3 "id": 2,
4 "documentId": "to5k2s0211nbrsn0vchvszaj",
5 "question": "Can the avengers beat the Gaurdians?",
6 "createdAt": "2024-09-29T12:06:07.605Z",
7 "updatedAt": "2024-09-29T12:06:07.605Z",
8 "publishedAt": "2024-09-29T12:06:07.617Z",
9 "locale": null,
10 "options": [],
11 "votes": [],
12 "localizations": []
13 },
14 "meta": {}
15}
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:
Nice.
Similarly, we'll create a custom on-vote-create
middleware for the Vote API as well:
npx strapi generate
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
In the newly generated ./src/api/vote/middlewares/on-vote-create.ts
file, enter the following:
1// ./src/api/vote/middlewares/on-vote-create.ts
2/**
3 * `on-vote-create` middleware
4 * This middleware executes logic when a new vote is created.
5 */
6import type { Core } from "@strapi/strapi";
7export default (config, { strapi }: { strapi: Core.Strapi }) => {
8 // Add your own logic here.
9 return async (ctx, next) => {
10 strapi.log.info("In on-vote-create middleware.");
11 // Retrieve the current user from the context's state
12 const user = ctx.state.user;
13
14 // Proceed to the next middleware or controller
15 await next();
16
17 // Retrieve the document ID of the new vote from the response body
18 const voteDocumentId = ctx.response.body.data.documentId;
19
20 try {
21 // Update the user's document in the database to connect the new vote
22 await strapi.documents("plugin::users-permissions.user").update({
23 documentId: user.documentId, // Use the user's document ID
24 data: {
25 votes: {
26 connect: [voteDocumentId], // Connect the new vote's ID
27 } as any, // Type assertion to any for compatibility
28 },
29 });
30 } catch (error) {
31 // Log the error to the console
32 console.log(error);
33 // Set the response status to 400 (Bad Request)
34 ctx.response.status = 400;
35 // Provide a response body with error details
36 ctx.response.body = {
37 statusCode: 400,
38 error: "Bad Request",
39 message: "An error occurred while connecting the vote to the user.",
40 };
41 }
42 };
43};
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
:
1// ./src/api/vote/routes/vote.ts
2/**
3 * vote router
4 */
5import { factories } from "@strapi/strapi";
6export default factories.createCoreRouter("api::vote.vote", {
7 config: {
8 create: {
9 // add the `on-vote-create` middleware to the `create` action
10 middlewares: ["api::vote.on-vote-create"],
11 },
12 },
13});
Awesome.
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
:
1// ./src/api/vote/middlewares/on-vote-create.ts
2/**
3 * `on-vote-create` middleware
4 * This middleware executes logic when a new vote is created.
5 */
6import type { Core } from "@strapi/strapi";
7export default (config, { strapi }: { strapi: Core.Strapi }) => {
8 // Add your own logic here.
9 return async (ctx, next) => {
10 strapi.log.info("In on-vote-create middleware.");
11 // Retrieve the current user from the context's state
12 const user = ctx.state.user;
13
14 // check if vote document with option and poll already exists
15 const vote = await strapi.documents("api::vote.vote").findFirst({
16 filters: {
17 // Filter by the user's ID and the poll's document ID
18 user: {
19 id: user.id,
20 },
21 poll: {
22 documentId: ctx.request.body.data.poll,
23 } as any,
24 },
25 populate: ["option", "poll"],
26 });
27 // If the user has already voted on this poll, return a 400 (Bad Request) response
28 if (vote) {
29 ctx.response.status = 400;
30 ctx.response.body = {
31 statusCode: 400,
32 error: "Bad Request",
33 message: "You have already voted on this poll.",
34 };
35 return;
36 }
37
38 // Proceed to the next middleware or controller
39 await next();
40
41 // ...
42 };
43};
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:
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"
}
}'
As you can see, this was successful. Now, if we try to run the request again, we get this:
Awesome!
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.
Here, we can scroll down to Users-permissions and enable count, find and findOne
This is by far the easiest solution but we can still explore a few more custom solutions.
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.
./src/api/poll/controllers/poll.ts
:1// ./src/api/poll/controllers/poll.ts
2/**
3 * poll controller
4 * This controller extends the default functionality for the poll API.
5 */
6import { factories } from "@strapi/strapi";
7// Export the customized poll controller
8export default factories.createCoreController(
9 "api::poll.poll", // Define the controller for the "poll" API
10 ({ strapi }) => ({
11 // Custom find method that retrieves multiple polls with additional user and vote data
12 async find(ctx) {
13 // Call the base "find" method from the core controller
14 const response = await super.find(ctx);
15 // For each poll in the response data, fetch related documents with detailed user and vote information
16 await Promise.all(
17 response.data.map(async (poll) => {
18 // Retrieve the poll document with populated fields for votes and user data
19 const pollDocument = await strapi
20 .documents("api::poll.poll") // Access the "poll" collection
21 .findOne({
22 documentId: poll.documentId, // Find by the poll's document ID
23 populate: {
24 votes: {
25 populate: {
26 option: {
27 fields: ["id", "value"], // Include "id" and "value" for each option
28 },
29 user: {
30 fields: ["id", "username", "email"], // Include "id", "username", and "email" for each user
31 },
32 },
33 },
34 user: {
35 fields: ["id", "username", "email"], // Include poll creator's "id", "username", and "email"
36 },
37 },
38 });
39 // Add the user details to the poll response
40 poll.user = {
41 id: pollDocument.user.id,
42 documentId: pollDocument.user.documentId,
43 username: pollDocument.user.username,
44 email: pollDocument.user.email,
45 };
46 // Assign the populated votes data to the poll
47 poll.votes = pollDocument.votes;
48 })
49 );
50 // Return the modified response with additional data
51 return response;
52 },
53 // Custom findOne method to retrieve a single poll by its ID with populated user and vote information
54 async findOne(ctx) {
55 // Call the base "findOne" method from the core controller
56 const response = await super.findOne(ctx);
57 // Fetch the poll document and its associated user and vote data
58 const pollDocument = await strapi.documents("api::poll.poll").findOne({
59 documentId: response.data.documentId, // Use the poll's document ID to find it
60 populate: {
61 votes: {
62 populate: {
63 option: {
64 fields: ["id", "value"], // Include vote option details
65 },
66 user: {
67 fields: ["id", "username", "email"], // Include user details for each vote
68 },
69 },
70 },
71 user: {
72 fields: ["id", "username", "email"], // Include poll creator's details
73 },
74 },
75 });
76 // Attach the user details to the response
77 response.data.user = {
78 id: pollDocument.user.id,
79 documentId: pollDocument.user.documentId,
80 username: pollDocument.user.username,
81 email: pollDocument.user.email,
82 };
83 // Attach the votes to the response
84 response.data.votes = pollDocument.votes;
85 // Return the modified response with user and vote information
86 return response;
87 },
88 })
89);
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.
To get started with Instant DB, create a new account if you don't have one already at https://www.instantdb.com/dash.
You can go through the onboarding process to create your app:
On the home page of the dashboard, you should see the App ID at the top:
To obtain your Admin secret key, navigate to the admin page by clicking on Admin at the side navigation:
First, we have to install the InstantDB Admin SDK. Run the following command in your terminal:
npm i @instantdb/admin
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
Next, in the src/index.ts
file, modify the code to this:
1// ./src/index.ts
2// import type { Core } from '@strapi/strapi';
3import { init } from "@instantdb/admin";
4type InstantDBSchema = {
5 votes: {
6 user: {
7 documentId: string;
8 username: string;
9 email: string;
10 };
11 poll: {
12 question: string;
13 documentId: string;
14 };
15 option: {
16 value: string;
17 documentId: string;
18 };
19 createdAt: string;
20 };
21};
22export const db = init<InstantDBSchema>({
23 appId: process.env.INSTANT_APP_ID,
24 adminToken: process.env.INSTANT_ADMIN_TOKEN,
25});
26export default {
27 /**
28 * An asynchronous register function that runs before
29 * your application is initialized.
30 *
31 * This gives you an opportunity to extend code.
32 */
33 register(/* { strapi }: { strapi: Core.Strapi } */) {},
34 /**
35 * An asynchronous bootstrap function that runs before
36 * your application gets started.
37 *
38 * This gives you an opportunity to set up your data model,
39 * run jobs, or perform some special logic.
40 */
41 bootstrap(/* { strapi }: { strapi: Core.Strapi } */) {},
42};
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.
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:
1// ./src/extensions/users-permissions/strapi-server.ts
2// Import the initialized Instant DB instance (db) to interact with the database
3import { db } from "../..";
4// Export a function that modifies the default plugin (users-permissions)
5module.exports = (plugin) => {
6 // Store references to the original register and callback controllers for later use
7 const register = plugin.controllers.auth.register;
8 const callback = plugin.controllers.auth.callback;
9 // Override the default register method to include InstantDB token creation
10 plugin.controllers.auth.register = async (ctx) => {
11 // Extract the user's email from the registration request body
12 const { email } = ctx.request.body;
13 // Create an authentication token for the user in InstantDB using their email
14 const token = await db.auth.createToken(email);
15 // Call the original register function to handle the default registration process
16 await register(ctx);
17 // Access the response body after registration
18 const body = ctx.response.body;
19 // Add the InstantDB token to the response body
20 body.instantdbToken = token;
21 };
22 // Override the default callback method (used for login) to include InstantDB token creation
23 plugin.controllers.auth.callback = async (ctx) => {
24 // Extract the identifier (usually the email or username) from the login request body
25 const { identifier } = ctx.request.body;
26 // Create an authentication token for the user in InstantDB using the identifier
27 const token = await db.auth.createToken(identifier);
28 // Call the original callback function to handle the default login process
29 await callback(ctx);
30 // Access the response body after login
31 const body = ctx.response.body;
32 // Add the InstantDB token to the response body
33 body.instantdbToken = token;
34 };
35 // Return the modified plugin object with the updated controller methods
36 return plugin;
37};
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:
1{
2 "username": "james",
3 "email": "james@gmail.com",
4 "password": "Pass1234"
5}
With that, we should have something like this:
Here's the response data:
1{
2 "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNzI3NjA2ODAxLCJleHAiOjE3MzAxOTg4MDF9.UudUAIcX8dMyqX-pqfRQKweQoDjqBavkjdLNYsYdQ0A",
3 "user": {
4 "id": 2,
5 "documentId": "s0z5llgt1cuitio2bv2u7ryz",
6 "username": "james",
7 "email": "james@gmail.com",
8 "provider": "local",
9 "confirmed": true,
10 "blocked": false,
11 "createdAt": "2024-09-29T10:46:41.898Z",
12 "updatedAt": "2024-09-29T10:46:41.898Z",
13 "publishedAt": "2024-09-29T10:46:41.899Z",
14 "locale": null
15 },
16 "instantdbToken": "659f0759-c2bf-47ca-8818-380d6f2e241b"
17}
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:
1{
2 "identifier": "james@gmail.com",
3 "password": "Pass1234"
4}
With that, we should have something like this:
You can also see the token here as well.
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:
1// ./src/api/vote/middlewares/on-vote-create.ts
2/**
3 * `on-vote-create` middleware
4 * This middleware executes logic when a new vote is created.
5 */
6import type { Core } from "@strapi/strapi";
7import { db } from "../../..";
8import { id, tx } from "@instantdb/admin";
9export default (config, { strapi }: { strapi: Core.Strapi }) => {
10 // Add your own logic here.
11 return async (ctx, next) => {
12 strapi.log.info("In on-vote-create middleware.");
13 // ...
14
15 // Proceed to the next middleware or controller
16 await next();
17
18 // Retrieve the document ID of the new vote from the response body
19 const voteDocumentId = ctx.response.body.data.documentId;
20 // Retrieve the document data from the response body
21 const document = ctx.response.body.data;
22 // Create a new record in the InstantDB database for the vote
23 const res = await db
24 .asUser({
25 // Use the user's information to create the vote record
26 email: user.email,
27 })
28 .transact(
29 tx.votes[id()].update({
30 // Use the user's information to create the vote record
31 user: {
32 documentId: user.documentId,
33 username: user.username,
34 email: user.email,
35 },
36 // Use the poll and option information from the vote document
37 poll: {
38 documentId: document.poll,documentId,
39 question: document.poll.question,
40 },
41 // Use the option information from the vote document
42 option: {
43 documentId: document.option.documentId,
44 value: document.option.value,
45 },
46 // Use the creation timestamp from the vote document
47 createdAt: document.createdAt,
48 })
49 );
50 console.log("🟢🟢🟢🟢 ~ instantDB record created", res);
51
52 // ...
53 };
54};
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:
Great. Now, if we check our Instant DB dashboard, we should see the records we've created so far:
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.