Encryption ensures data security through scrambling so that only authorized people can access and decode the information or data.
What we will be building
This post discusses using the Appwrite database feature to build a chat app. In this article, we’ll ensure data security using the cryptr library for encrypting and decrypting strings.
GitHub URL
https://github.com/Iheanacho-ai/chat-app-nextjs
Prerequisites
This article is part 2 of the two-part series that describes how to build an end-to-end encrypted chat using Appwrite in a Next.js app. It is crucial to start with the first part to get the most out of this article. Check out the first part of the article here.
Installing cryptr
crypt is a simple aes-256-gcm module for encrypting and decrypting values of UTF-8 strings.
To install crypt, run this terminal command in our project directory.
npm install cryptr
Creating collection and attributes
On the left side of the Appwrite Console dashboard, click on the Database tab.
Click on the Add Database button to create a new database. Creating a new database will lead us to the Collection page.
Next, we’ll create a collection in our database tab by clicking the Add Collection button. This action redirects us to a Permissions page.
At the Collection Level, we want to assign a Read Access and Write Access with a role:all value. We can customize these roles later to specify who has access to read or write to our database.
On the right side of the Permissions page, copy the Collection ID, which we’ll need to perform operations on the collection’s documents.
Next, go to the attributes tab to create the properties we want a document to have.
Let’s create a string attribute of message with a size of 256 bits.
Adding Chat app Interaction with Database
In the chat.jsx file inside pages folder, import the useState hook for handling states, Appwrite’s client instance, and Appwrite’s Databases method.
import { useState } from 'react';
import { client} from '../init'
import { Databases } from 'appwrite';
Next, create two state variables:
- A
messagesstate variable to hold the messages that a user is about to send. - A
databaseMessagesstate variable to hold the messages retrieved from the database.
const [message, setMessages] = useState("");
const [databaseMessages, setDatabaseMessages] = useState(["hey"])
We then create a databases instance using the Appwrite Databases method. This Databases method receives the client and the databaseID as parameters.
const databases = new Databases(client, 'DatabaseID');
Next, we create a listMessages function and a sendMessage function.
const listMessages = async () => {
const promise = await databases.listDocuments('CollectionID');
promise.documents.map((document) => setDatabaseMessages(prevArray => [...prevArray, document.message]))
}
The listMessages in the code block above does the following:
- Lists all the documents in a collection using Appwrite’s
listDocumentsmethod. ThislistDocumentsmethod receives a collectionID as a parameter. - Updates the
databaseMessagesstate variable with the messages saved in the documents.
const sendMessage = async () => {
try {
await databases.createDocument('CollectionID', 'unique()', {
"message": message
});
alert('message sent')
setMessages("")
listMessages()
}catch (error) {
console.log(error)
}
}
The sendMessage function above does the following:
- Creates a new document using Appwrite’s
createDocument()function. ThiscreateDocument()function receives a collection ID, a unique() string, and attribute values as parameters. - Alerts us when we have successfully saved the message.
- Clears out the
messagevariable and calls thelistMessages()function. - Logs any errors encountered to the console.
Next, we check whether the databaseMessages array is empty, then we loop through the data in the databaseMessages array and render the messages in our chat app.
<div className='messages'>
{
databaseMessages ? (
<div className="message-container">
{
databaseMessages.map((databaseMessage)=> (
<div className="user-message">{databaseMessage}</div>
))
}
</div>
) : null
}
</div>
Next, we’ll pass the message variable as a value to our input field and our sendMessage function to the onClick event listener on our send button.
<div className='input-area'>
<input type="text" className='message-input' value={message} onChange={(e) => setMessages(e.target.value)}/>
<button className='send' type='button' onClick={sendMessage}>send</button>
</div>
After we are done with this section, this is how our chat.jsx file looks.
import { useState } from 'react';
import { client} from '../init'
import { Databases } from 'appwrite';
const Chat = () => {
const [message, setMessages] = useState("");
const [databaseMessages, setDatabaseMessages] = useState(["hey"])
const databases = new Databases(client, 'DatabaseID');
const listMessages = async () => {
const promise = await databases.listDocuments('CollectionID');
promise.documents.map((document) => setDatabaseMessages(prevArray => [...prevArray, document.message]))
}
const sendMessage = async () => {
try {
await databases.createDocument('CollectionID', 'unique()', {
"message": message
});
alert('message sent')
setMessages("")
listMessages()
} catch (error) {
console.log(error)
}
}
return (
<div className='chat'>
<div className='user-chat'>
<div className="user-chat-header">USER</div>
<div className='messages'>
{
databaseMessages.map((databaseMessage)=> (
<div className="user-message">{databaseMessage}</div>
))
}
</div>
<div className='input-area'>
<input type="text" className='message-input' value={message} onChange={(e) => setMessages(e.target.value)}/>
<button className='send' type='button' onClick={sendMessage}>send</button>
</div>
</div>
</div>
)
};
export default Chat;
Here is how our chat app looks.
Encryption
Cryptr requires a secret key to encrypt or decrypt a string. At the root of our project, we create a .env.local file that will contain our secret key.
## .env.local
NEXT_PUBLIC_KEY = "********"
Using the "NEXT_PUBLIC" prefix when storing our secret key allows the environment variable to be available in our component.
Next, we import the crypt library in our chat.jsx file.
import { useEffect, useState } from 'react';
import { client} from '../init'
import { Databases } from 'appwrite';
const Cryptr = require('cryptr');
We then create a cryptr instance to encrypt and decrypt our string using our secret key.
import { useEffect, useState } from 'react';
import { client} from '../init'
import { Databases } from 'appwrite';
const Cryptr = require('cryptr');
const Chat = () => {
...
const cryptr = new Cryptr(process.env.NEXT_PUBLIC_KEY);
return (
...
)
};
The sendMessages function in our chat.jsx file will be responsible for encrypting the data. The crypt library lets us use the encrypt function to encrypt strings in our application.
const sendMessage = async () => {
// encrypt the string in our message state variable
const encryptedMessage = cryptr.encrypt(message)
try {
await databases.createDocument('62dc54f155f11d4c38cb', 'unique()', {
// stores the encrypted message instead of the original message
"message": encryptedMessage
});
alert('message sent')
setMessages("")
listMessages()
} catch (error) {
console.log(error)
}
}
The sendMessage function encrypts the data in the message state variable and then stores the encrypted data on our Appwrite database.
The "hey" message gets encrypted and stored in our database as a bunch of numbers.
Next, we’ll retrieve the encrypted data from our database and decrypt it to get the original message.
In the listMessages function, we’ll now decrypt the message we obtained from the Appwrite database.
const listMessages = async () => {
const promise = await databases.listDocuments('62dc54f155f11d4c38cb');
setDatabaseMessages([])
promise.documents.map((document) =>{
// map through the documents in the collection and decrypt each message
const decryptedMessage = cryptr.decrypt(document.message)
setDatabaseMessages(prevArray => [...prevArray, decryptedMessage])
}
)
}
The listMessages function cleans out the databaseMessages array before looping and decrypting the messages in the document.
Here is how our chat app should look.
Conclusion
This article discussed achieving an end-to-end encrypted chat with cryptr and Appwrite.











Oldest comments (4)
Warning!
It is important to notice that NEXT_PUBLIC_ environment variable prefixes should only be used for values that are non-sensitive. It's not secure to store your secret encryption key on a NEXT_PUBLIC_ env variable. Consider using Next.js API routes to isolate any service-oriented business logic to the server-side of things. ( for example implement and call from client-side a route like /api/sendEncryptedMessageToDB/ and handle encryption from there (Nextjs's server-side), before sending to the client side of NextJs)
More info here
Thanks for the well written post.
Would it be possible to make it so that only the users can decrypt the data?
I think that for an accurate E2E encryption model, the developers should not be able to decrypt the data.
okay, ill work on it and create a an article
This is not an end-to-end encrypted chat. Also I highly recommend to edit this article to not use a "NEXT_PUBLIC_" env variable.