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:
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:
- 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.
- Node.js and npm: Install Node.js and npm to run Strapi and Next.js.
- Instant DB Account: Sign up for Instant DB and get your app ID for real-time updates.
- 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
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:
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
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.
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.
Next, we start creating fields for our new collection type.
The fields we’ll be creating are:
- Question:
- Type: Text (Short Text)
- Name:
question
Click on Add another field to proceed.
- User:
- Type: Relation (Many-to-One)
- Name:
user
- Reference: User (from users-permissions)
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.
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
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:
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
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:
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.
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
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:
// ./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.",
};
}
};
};
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:
{
"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": {}
}
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.
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
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:
// ./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.",
};
}
};
};
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"],
},
},
});
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();
// ...
};
};
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!
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.
- 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
This is by far the easiest solution but we can still explore a few more custom solutions.
- 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.
- 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;
},
})
);
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.
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:
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
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:
// ./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 } */) {},
};
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;
};
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"
}
With that, we should have something like this:
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"
}
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
3
4
{
"identifier": "james@gmail.com",
"password": "Pass1234"
}
With that, we should have something like this:
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);
// ...
};
};
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:
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.