Building a message board with Next.js and AppWrite

This page summarizes the projects mentioned and recommended in the original post on dev.to

Our great sponsors
  • Appwrite - The open-source backend cloud platform
  • InfluxDB - Collect and Analyze Billions of Data Points in Real Time
  • Revelo Payroll - Free Global Payroll designed for tech teams
  • Onboard AI - Learn any GitHub repo in 59 seconds
  • Sonar - Write Clean JavaScript Code. Always.
  • docker

    Docker - the open-source application container engine (by microsoft)

    Have Docker Desktop installed and running

  • Tailwind CSS

    A utility-first CSS framework for rapid UI development.

    For styling, we will use TailwindCSS. And to handle calling AppWrite from the front-end, we will use the data fetching and caching library React Query.

  • Appwrite

    Appwrite - The open-source backend cloud platform. The open-source backend cloud platform for developing Web, Mobile, and Flutter applications. You can set up your backend faster with real-time APIs for authentication, databases, file storage, cloud functions, and much more!

  • preevy

    Quickly deploy preview environments to the cloud!

    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 ( Message board {messages?.map((message) => ( {message.message} Delete ))} setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> Submit message ); }

    Enter fullscreen mode Exit fullscreen mode

    By the way, if you want to see how the application looks, start it with by typing npm run devin 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.

    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,
        }
      );
    };
    
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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:

    // other code
    
    export const getMessages = async () => {
      const { documents: messages } = await databases.listDocuments(database, collection);
    
      return messages;
    };
    
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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:

    // other code
    
    export const deleteMessage = async (id) => {
      await databases.deleteDocument(database, collection, id);
    };
    
    Enter fullscreen mode Exit fullscreen mode

    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.

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      deleteMessageMutation.mutate(message.$id)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md" > Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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.

    // 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);
    
    Enter fullscreen mode Exit fullscreen mode

    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:

    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 (
        
          
            
          
        
      );
    }
    
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
    Email setEmail(e.target.value)} />
    Password setPassword(e.target.value)} />
    {/* Submit button */}
    Sign Up
    ); }
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
    Email setEmail(e.target.value)} />
    Password setPassword(e.target.value)} />
    Sign In
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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:

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      deleteMessageMutation.mutate(message.$id)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md" > Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message {user && ( Sign out )}
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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

    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

    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.

    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))]
      );
    };
    
    Enter fullscreen mode Exit fullscreen mode

    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:

     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
    
    
    Enter fullscreen mode Exit fullscreen mode

    Now, create a new user on /signupand then use the same credentials on/signinto 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:

    {user && (
      <>
         setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />
         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
        
        
          Sign out
        
      >
    )}
    
    Enter fullscreen mode Exit fullscreen mode

    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:

    const canDelete = (userID, array) => {
      return array.some((element) => element.includes('delete') && element.includes(userID));
    };
    
    Enter fullscreen mode Exit fullscreen mode

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

    {messages?.map((message) => (
      
  • {message.message}

    {canDelete(user?.$id, message.$permissions) && ( deleteMessageMutation.mutate(message.$id, user.$id)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md" > Delete )}
  • ))}
    Enter fullscreen mode Exit fullscreen mode

    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 by Vercel.

    And while you’re at it, you can also set up preview environments for your project using Preevy. 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!

  • Next.js

    The React Framework

    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 ( Message board {messages?.map((message) => ( {message.message} Delete ))} setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> Submit message ); }

    Enter fullscreen mode Exit fullscreen mode

    By the way, if you want to see how the application looks, start it with by typing npm run devin 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.

    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,
        }
      );
    };
    
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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:

    // other code
    
    export const getMessages = async () => {
      const { documents: messages } = await databases.listDocuments(database, collection);
    
      return messages;
    };
    
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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:

    // other code
    
    export const deleteMessage = async (id) => {
      await databases.deleteDocument(database, collection, id);
    };
    
    Enter fullscreen mode Exit fullscreen mode

    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.

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      deleteMessageMutation.mutate(message.$id)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md" > Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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.

    // 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);
    
    Enter fullscreen mode Exit fullscreen mode

    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:

    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 (
        
          
            
          
        
      );
    }
    
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
    Email setEmail(e.target.value)} />
    Password setPassword(e.target.value)} />
    {/* Submit button */}
    Sign Up
    ); }
    Enter fullscreen mode Exit fullscreen mode

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

    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 (
        
    Email setEmail(e.target.value)} />
    Password setPassword(e.target.value)} />
    Sign In
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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:

    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 (
        
          

    Message board

      {messages?.map((message) => (
    • {message.message}

      deleteMessageMutation.mutate(message.$id)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md" > Delete
    • ))}
    setInput(e.target.value)} className="w-full border border-gray-300 rounded-md p-2 mt-4" /> addMessageMutation.mutate(input)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md mt-4" > Submit message {user && ( Sign out )}
    ); }
    Enter fullscreen mode Exit fullscreen mode

    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

    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

    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.

    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))]
      );
    };
    
    Enter fullscreen mode Exit fullscreen mode

    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:

     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
    
    
    Enter fullscreen mode Exit fullscreen mode

    Now, create a new user on /signupand then use the same credentials on/signinto 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:

    {user && (
      <>
         setInput(e.target.value)}
          className="w-full border border-gray-300 rounded-md p-2 mt-4"
        />
         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
        
        
          Sign out
        
      >
    )}
    
    Enter fullscreen mode Exit fullscreen mode

    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:

    const canDelete = (userID, array) => {
      return array.some((element) => element.includes('delete') && element.includes(userID));
    };
    
    Enter fullscreen mode Exit fullscreen mode

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

    {messages?.map((message) => (
      
  • {message.message}

    {canDelete(user?.$id, message.$permissions) && ( deleteMessageMutation.mutate(message.$id, user.$id)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md" > Delete )}
  • ))}
    Enter fullscreen mode Exit fullscreen mode

    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 by Vercel.

    And while you’re at it, you can also set up preview environments for your project using Preevy. 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!

  • Appwrite

    Build Fast. Scale Big. All in One Place. Cloud is now available in public beta. 🌩

    In this article, we will look at how to build a simple message board web app with Next.js and AppWrite. 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.

  • InfluxDB

    Collect and Analyze Billions of Data Points in Real Time. Manage all types of time series data in a single, purpose-built database. Run at any scale in any environment in the cloud, on-premises, or at the edge.

NOTE: The number of mentions on this list indicates mentions on common posts plus user suggested alternatives. Hence, a higher number means a more popular project.

Suggest a related project

Related posts