Building a message board with Next.js and AppWrite
Tags: nextjs, webdev, tutorial, opensource
Published: 2023-05-24 10:57:04

## TL;DR

In this article, we will look at how to build a simple message board web app with Next.js and AppWrite. This is a good context for learning how to better use these tools and to build something useful along the way.

## A small request

I'm trying to reach 1K GitHub stars for "[Preevy](https://github.com/livecycle/preevy)" - an open source tool my team and I just launched for easily creating shareable preview environments. 
Can you help us out by starring the GitHub repository? It would help us a lot! Thank you! [https://github.com/livecycle/preevy] (https://github.com/livecycle/preevy)

And now, if you're ready, let's dive in. 


![dive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o3v2y51fxsc2c9oic412.gif)


## Introduction

In this article, we will look at how to build a simple message board web app with [Next.js ](https://nextjs.org/) and [AppWrite](https://appwrite.io/). In the end, we’ll have a working web app where users can authenticate, post their messages and read the messages of others. I worked with developers on my team to put this guide together. We think this is a good context for learning how to better use these tools and to build something useful along the way.

Next.js, a popular React framework, offers exceptional performance and server-side rendering capabilities, making it a go-to choice for creating interactive web apps. AppWrite, on the other hand, makes it super simple to write backend code without having to set up your own backend. You can call it directly from your Next.js front-end. AppWrite can handle all tasks of a traditional backend for you: databases, authentication, file uploads, and much more.

For styling, we will use [TailwindCSS](https://tailwindcss.com/). And to handle calling AppWrite from the front-end, we will use the data fetching and caching library React Query.


## Setup


### System prerequisites

* Have a current version of[ Node.js](https://nodejs.org/en) along with NPM installed
* Have[ Docker](https://www.docker.com/) Desktop installed and running


### AppWrite installation

During development, we host an instance of AppWrite on our local machine. This can be done with Docker. You should already have this set up on your machine. Internally, AppWrite consists of various services. Such as Redis and MariaDB. Setting up all these services manually would be a hassle. Luckily, AppWrite comes with a `docker-compose` configuration, which can start all services in one command.

To get started, create a new folder` message-board/` and in there run this command to provision and start the AppWrite service. Downloading the Docker images might take a while. During the setup process, chose the default options everywhere and enter a random secret key.


```bash
docker run -it --rm \
    --volume /var/run/docker.sock:/var/run/docker.sock \
    --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
    --entrypoint="install" \
    appwrite/appwrite:1.3.4
```


Afterward, go to[ http://localhost](http://localhost) and create a new admin account to manage AppWrite. With this, we can now create and configure our AppWrite project. First off, sign up with a new account. Then, click “Create Project” and enter `message-board` as the name.


![1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yovo4v9q296aoyc65ucu.png)


Now select “Add a Platform” → “Web App”. Name it `message-board` again and use `*` for the hostname since this is just for local development and we want to make sure we can access AppWrite from the Next.js app.

![6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wp891ywf4a9y7b3mdfdo.png)

You can skip the optional steps regarding the setup in JavaScript. We’ll do that later. Once you’re in the dashboard, go to “Databases” and create a new Database. Again, name it `message-board`. Leave the “Database ID” empty so that it gets randomly generated.

![3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xarzn2kkq80lx6hc1xf3.png)

Now create a collection inside the newly created database. For this, click “Create collection” and call it `messages`. Once again, leave the ID field empty.

![4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qin5ob9iw6a8z2dksc36.png)

Inside the `messages` schema, create a new “String” attribute with a size of `1024` and mark it as “Required”. This is the attribute where we’ll later store the messages users submit.

![5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ygf9aymz3yp2z5cxbf02.png)


While already here, we should also set up the permissions for accessing this collection. Go to the collection “Settings” tab and configure the permissions according to this. We’ll adjust these again once we get to the authentication section of the tutorial.


![6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4jwgjjm84yowyqcax20.png)

### Next.js Setup

Now back to the terminal. In the previously created folder `message-board/` run this command to provision a new Next app. Observe the settings we picked below. You must say Yes to Tailwind CSS and No to using the App Router.

![7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bq1leblbna0p2rl8h0h0.png)

Now change into the `message-board/message-board-app/` folder and install the dependencies we need for AppWrite and ReactQuery:


```bash
npm install appwrite react-query
```


Let’s open the Next.js app in your editor of choice. In the `src/pages/_app.js` file, configure React Query. We’ll use that later to fetch data from AppWrite.


```jsx
import { QueryClient, QueryClientProvider } from 'react-query';

import '@/styles/globals.css'

export default function App({ Component, pageProps }) {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  );
}
```


Now, inside your Next.js folder, create a new file called `.env.local`. In there, we need to store our AppWrite access information:


```bash
NEXT_PUBLIC_ENDPOINT=http://localhost/v1
NEXT_PUBLIC_PROJECT=646XXX
NEXT_PUBLIC_DATABASE=646XXX
NEXT_PUBLIC_MESSAGES_COLLECTION=646XXX
```


The endpoint will always be the same, provided you’ve followed the docker-compose installation. The other three IDs need to be copied from the AppWrite dashboard. You can find them right next to the heading of the Project, Database, and Collection name. Be careful to not mix up the project, database and collection IDs, as they all look very similar.

![8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqowas32fyxxxd8xj196.png)


## Building the message board


### Basic scaffolding

On the `src/pages/index.js` page first remove the existing boilerplate and then create the scaffolding for displaying, creating, and deleting messages. For now, we will display static data and add the interactive functionality later.


```jsx
import { useState } from 'react';

const messages = [
  { $id: 1, message: 'Hello world' },
  { $id: 2, message: 'Hello world 2' },
];

export default function Home() {
  const [input, setInput] = useState('');

  return (
    <main className="flex min-h-screen justify-center p-24 text-black">
      <div className="bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-4xl py-8 font-bold text-center">Message board</h1>
        <ul className="space-y-4">
          {messages?.map((message) => (
            <li key={message.$id} className="flex items-center justify-between w-full space-x-4">
              <p className="text-gray-700">{message.message}</p>
              <button className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md">Delete</button>
            </li>
          ))}
        </ul>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />

        <button className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4">Submit message</button>
      </div>
    </main>
  );
}
```


By the way, if you want to see how the application looks, start it with by typing `npm run dev `in your terminal in the `message-board-app/ `directory.


### Adding new messages

First, create a new file at `src/appwrite.js` which will contain all our communication with the AppWrite backend. The `addMessage` is an asynchronous function. It takes the message string as an input. Then it will make a call to our AppWrite instance where the message will be saved to the database.


```jsx
import { Account, Client, Databases, ID } from 'appwrite';

const client = new Client();
const account = new Account(client);

const database = process.env.NEXT_PUBLIC_DATABASE;
const collection = process.env.NEXT_PUBLIC_MESSAGES_COLLECTION;

client.setEndpoint(process.env.NEXT_PUBLIC_ENDPOINT).setProject(process.env.NEXT_PUBLIC_PROJECT);

const databases = new Databases(client);

export const addMessage = async (message) => {
  await databases.createDocument(
    database,
    collection,
    ID.unique(),
    {
      message,
    }
  );
};
```


Then modify the `src/pages/index.js` component to hook up the form for adding messages:


```jsx
import { useState } from 'react';
import { useMutation, useQueryClient } from 'react-query';

import { addMessage } from '@/appwrite';

const messages = [
  { $id: 1, message: 'Hello world' },
  { $id: 2, message: 'Hello world 2' },
];

export default function Home() {
  const [input, setInput] = useState('');

  const queryClient = useQueryClient();

  const addMessageMutation = useMutation(addMessage, {
    onSuccess: () => {
      setInput('');
      queryClient.invalidateQueries('messages');
    },
  });

  return (
    <main className="flex min-h-screen justify-center p-24 text-black">
      <div className="bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-4xl py-8 font-bold text-center">Message board</h1>
        <ul className="space-y-4">
          {messages?.map((message) => (
            <li key={message.$id} className="flex items-center justify-between w-full space-x-4">
              <p className="text-gray-700">{message.message}</p>
              <button className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md">Delete</button>
            </li>
          ))}
        </ul>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />
        <button
          onClick={() => addMessageMutation.mutate(input)}
          className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4"
        >
          Submit message
        </button>
      </div>
    </main>
  );
}
```


Now we can add messages to the database. However, we can’t see them yet as we are not fetching any messages from AppWrite. We will fix that shortly. 

But what exactly are we doing here? We have created a React Query mutation with the `useMutation` hook. It allows us to then call `addMessageMutation.mutate(input)` on the “Submit message” button. And even more importantly, in the hook, we added an `onSucess` callback. So whenever adding a new message is successful, we can clear the input field and invalidate the query cache. Invalidating the cache will prompt React Query to fetch all messages again. This will become useful in the next section where we fetch messages from AppWrite.


### Rendering messages

Now that we can add messages to the database, we also want to display them. First we’ll need to add the fetch messages action to the `src/appwrite.js` file:


```jsx
// other code

export const getMessages = async () => {
  const { documents: messages } = await databases.listDocuments(database, collection);

  return messages;
};
```


Now we can fetch them with the React Query `useQuery` hook:


```jsx
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from 'react-query';

import { addMessage, getMessages } from '@/appwrite';

export default function Home() {
  const [input, setInput] = useState('');

  const queryClient = useQueryClient();

  const { data: messages } = useQuery('messages', getMessages);

  const addMessageMutation = useMutation(addMessage, {
    onSuccess: () => {
      setInput('');
      queryClient.invalidateQueries('messages');
    },
  });

  return (
    <main className="flex min-h-screen justify-center p-24 text-black">
      <div className="bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-4xl py-8 font-bold text-center">Message board</h1>
        <ul className="space-y-4">
          {messages?.map((message) => (
            <li key={message.$id} className="flex items-center justify-between w-full space-x-4">
              <p className="text-gray-700">{message.message}</p>
              <button
                className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md"
              >
                Delete
              </button>
            </li>
          ))}
        </ul>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />
        <button
          onClick={() => addMessageMutation.mutate(input)}
          className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4"
        >
          Submit message
        </button>
      </div>
    </main>
  );
}
```


Add some messages via the form so you can see them! Notice that whenever you submit a new message, the existing messages get automatically reloaded - so that the new message appears in the list. That’s the magic of React Query.


### Deleting messages

The last missing step is the ability to delete messages. Once again, we first need to add this function to `src/appwrite.js`:


```jsx
// other code

export const deleteMessage = async (id) => {
  await databases.deleteDocument(database, collection, id);
};
```


And then, all that’s left is adding one more mutation and the `onClick` action on the delete button to the `src/pages/index.js` page. Don’t copy the whole component. Only copy the added delete mutation and the new return statement. Also, make sure to import `deleteMessage` from AppWrite.


```jsx
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from 'react-query';

import { addMessage, deleteMessage, getMessages } from '@/appwrite';

export default function Home() {
  // other code

  const deleteMessageMutation = useMutation(deleteMessage, {
    onSuccess: () => {
      queryClient.invalidateQueries('messages');
    },
  });

  return (
    <main className="flex min-h-screen justify-center p-24 text-black">
      <div className="bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-4xl py-8 font-bold text-center">Message board</h1>
        <ul className="space-y-4">
          {messages?.map((message) => (
            <li key={message.$id} className="flex items-center justify-between w-full space-x-4">
              <p className="text-gray-700">{message.message}</p>
              <button
                onClick={() => deleteMessageMutation.mutate(message.$id)}
                className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md"
              >
                Delete
              </button>
            </li>
          ))}
        </ul>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />
        <button
          onClick={() => addMessageMutation.mutate(input)}
          className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4"
        >
          Submit message
        </button>
      </div>
    </main>
  );
}
```


Once again, after successfully deleting a message we tell React Query to refetch all messages so that the deleted message disappears from the list.


## Authentication

To add authentication, we first need some additional actions in the `src/appwrite.js` file. This is so that users can sign up and afterward sign in. Also, we need a sign out action and a function to fetch the user session. To store the user session, also create a new React Context in this file. Make sure to import `createContext` from `react`.


```jsx
// other code

export const signUp = async (email, password) => {
  return await account.create(ID.unique(), email, password);
};

export const signIn = async (email, password) => {
  return await account.createEmailSession(email, password);
};

export const signOut = async () => {
  return await account.deleteSessions();
};

export const getUser = async () => {
  try {
    return await account.get();
  } catch {
    return undefined;
  }
};


// import createContext from react beforehand
export const UserContext = createContext(null);
```


We now need to fetch the user automatically on every application start and make it available through our context. We’ll do this in `src/pages/_app.js`:


```jsx
import { useEffect, useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';

import { UserContext, getUser } from '@/appwrite';

import '@/styles/globals.css';

export default function App({ Component, pageProps }) {
  const [user, setUser] = useState(null);

  const queryClient = new QueryClient();

  useEffect(() => {
    const user = async () => {
      const user = await getUser();

      if (!user) return;

      setUser(user);
    };

    user();
  }, []);

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <QueryClientProvider client={queryClient}>
        <Component {...pageProps} />
      </QueryClientProvider>
    </UserContext.Provider>
  );
}
```


Then we can build the masks for sign up. For this add a new file `src/pages/signup.js` with the following content:


```jsx
import { useRouter } from 'next/router';
import { useState } from 'react';

import { signUp } from '@/appwrite';

export default function SignUp() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      await signUp(email, password);

      router.push('/');
    } catch {
      console.log('Error signing up');
    }
  };

  return (
    <div className="flex items-center justify-center h-screen">
      <div className="max-w-sm mx-auto">
        <form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 py-6 mb-4">
          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
              Email
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              id="email"
              type="email"
              placeholder="Enter your email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </div>

          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="password">
              Password
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              id="password"
              type="password"
              placeholder="Enter your password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
          </div>

          {/* Submit button */}
          <div className="flex items-center justify-between">
            <button
              className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
              type="submit"
            >
              Sign Up
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}
```


And sign in at `src/pages/signin.js`:


```jsx
import { useRouter } from 'next/router';
import { useState } from 'react';

import { signIn } from '@/appwrite';

export default function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      await signIn(email, password);

      router.push('/');
    } catch {
      console.log('Error signing in');
    }
  };

  return (
    <div className="flex items-center justify-center h-screen">
      <div className="max-w-sm mx-auto">
        <form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 py-6 mb-4">
          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
              Email
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              id="email"
              type="email"
              placeholder="Enter your email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </div>

          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="password">
              Password
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              id="password"
              type="password"
              placeholder="Enter your password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
          </div>

          <div className="flex items-center justify-between">
            <button
              className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
              type="submit"
            >
              Sign In
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}
```


## Signing out

Now, for the last piece of the user authentication, we need to hook up the sign out functionality. We already have all the pieces for this. We just need to check if the `user` session exists and if yes, display a “Sign Out” button that calls the `signOut` function from AppWrite. We’ll change the `src/pages/index.js` file to this:


```jsx
import { useContext, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from 'react-query';

import { addMessage, getMessages, deleteMessage, UserContext, signOut } from '@/appwrite';

export default function Home() {
  const [input, setInput] = useState('');

  const queryClient = useQueryClient();
  const { user, setUser } = useContext(UserContext);

  const { data: messages } = useQuery('messages', getMessages);

  const addMessageMutation = useMutation(addMessage, {
    onSuccess: () => {
      setInput('');
      queryClient.invalidateQueries('messages');
    },
  });

  const deleteMessageMutation = useMutation(deleteMessage, {
    onSuccess: () => {
      queryClient.invalidateQueries('messages');
    },
  });

  const handleSignOut = async () => {
    await signOut();

    setUser(null);
  };

  return (
    <main className="flex min-h-screen justify-center p-24 text-black">
      <div className="bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-4xl py-8 font-bold text-center">Message board</h1>
        <ul className="space-y-4">
          {messages?.map((message) => (
            <li key={message.$id} className="flex items-center justify-between w-full space-x-4">
              <p className="text-gray-700">{message.message}</p>
              <button
                onClick={() => deleteMessageMutation.mutate(message.$id)}
                className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md"
              >
                Delete
              </button>
            </li>
          ))}
        </ul>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />
        <button
          onClick={() => addMessageMutation.mutate(input)}
          className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4"
        >
          Submit message
        </button>
        {user && (
          <button
            onClick={handleSignOut}
            className="bg-red-500 ml-4 hover:bg-red-600 text-white px-4 py-2 rounded-md mt-4"
          >
            Sign out
          </button>
        )}
      </div>
    </main>
  );
}
```

## Access control

With authentication implemented, we now want to ensure that only authenticated users can create new messages. And that users can only delete their own messages. So let’s go back to the dashboard and change the `messages` schema permissions to this:


![9](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqady72m98062monfejf.png)

As you can see, unauthenticated users can only read messages from now on. If you want to create messages, you need to be authenticated. But how can we enable users to delete their own messages? We’ll do this on a per-document basis. Think of a document as one row in the database. So for that, on the same screen, also make sure you turn on “Document Security”.

![10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dz1iodesc7ite5po0szo.png)

Back in the code, modify the `addMessage` function in `src/appwrite.js` as follows. Also, don’t forget to update the import statement as shown.


```jsx
import { Account, Client, Databases, Permission, Role, ID } from 'appwrite';

export const addMessage = async ({ message, userId }) => {
  await databases.createDocument(
    database,
    collection,
    ID.unique(),
    {
      message,
    },
    [Permission.delete(Role.user(userId))]
  );
};
```


That way, we explicitly state that only the owner of the document can delete it. Now, in `src/app/index.js` in the add message button action, also pass the `user.$id` of our current user, so AppWrite knows which user we want to give delete permission to:


```jsx
<button
  onClick={() => addMessageMutation.mutate({ message: input, userId: user?.$id })}
  className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4"
>
  Submit message
</button>
```


Now, create a new user on `/signup `and then use the same credentials on` /signin `to sign in. Afterward, you will be able to create new messages linked to your account. Try deleting those messages and see what happens.


## Showing the auth state in the UI

Great! Now only owners of messages can delete them. But we’re still showing a delete button beside every message. No matter which user we’re logged in with. Same for the submit a new message field which is visible for unauthenticated users. So we need to change our UI only to show these options when they can actually be performed by the user. For the submit a new message form that’s simple. We’ll just show it whenever a user is logged in:


```jsx
{user && (
  <>
    <textarea
      value={input}
      onChange={(e) => setInput(e.target.value)}
      className="w-full border border-gray-300 rounded-md p-2 mt-4"
    />
    <button
      onClick={() => addMessageMutation.mutate({ message: input, userId: user?.$id })}
      className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4"
    >
      Submit message
    </button>
    <button
      onClick={handleSignOut}
      className="bg-red-500 ml-4 hover:bg-red-600 text-white px-4 py-2 rounded-md mt-4"
    >
      Sign out
    </button>
  </>
)}
```


It’s a bit more complicated for checking if a user owns the message. Every message comes with a permissions array. If the user owns that message, their `user.$id` will be present in the `delete` permission field. We’ll write a function to check for that:


```jsx
const canDelete = (userID, array) => {
  return array.some((element) => element.includes('delete') && element.includes(userID));
};
```


And then check for that in the UI for each message to see if we need to render the delete button:


```jsx
{messages?.map((message) => (
  <li key={message.$id} className="flex items-center justify-between w-full space-x-4">
    <p className="text-gray-700">{message.message}</p>
    {canDelete(user?.$id, message.$permissions) && (
      <button
        onClick={() => deleteMessageMutation.mutate(message.$id, user.$id)}
        className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md"
      >
        Delete
      </button>
    )}
  </li>
))}
```



## Wrapping up

And with this, we’re done! Users will now only see the actions they actually have permission for. As this tutorial showed, Next.js, AppWrite, and React Query played very nicely together. Thanks to AppWrite in combination with React Query we get an amazing full-stack development experience without writing any backend code.

We hope you learned something new while following this tutorial. The application we built can serve as a starting point for your own project. If you want to continue building this message board, here are a few feature ideas:

* Add comments to messages. This will teach you how to do relations in AppWrite.
* Add infinite scrolling or pagination to deal with a large number of messages that cannot be rendered all at once.
* Use Next’s Server Side Rendering to fetch and render messages on the server instead of the client.

Regarding deployment, we already set up the AppWrite in a neatly packaged docker-compose setup. You could host this on popular Cloud Providers such as Google Cloud or AWS. As a bonus, add Next.js into the Docker environment using this [guide](https://nextjs.org/docs/app/building-your-application/deploying#docker-image) by Vercel. 

And while you’re at it, you can also set up preview environments for your project using [Preevy](https://preevy.dev/). This will easily provision preview environments for your application that you can share with others to get quick feedback and keep your development workflow moving at a good clip.

If you don’t want to go down the self-hosting route, AppWrite also offers a cloud service where that host it for you. But then you’ll need to host the Next.js separately on Vercel or Netlify.

Hope you enjoyed and found this guide helpful. 

Good luck!