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
  • SurveyJS - Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App
  • InfluxDB - Power Real-Time Data Analytics at Scale
  • WorkOS - The modern identity platform for B2B SaaS
  • 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.

  • SurveyJS

    Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App. With SurveyJS form UI libraries, you can build and style forms in a fully-integrated drag & drop form builder, render them in your JS app, and store form submission data in any backend, inc. PHP, ASP.NET Core, and Node.js.

    SurveyJS logo
  • 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" /> <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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>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. </p> <p>But what exactly are we doing here? We have created a React Query mutation with the <code>useMutation</code> hook. It allows us to then call <code>addMessageMutation.mutate(input)</code> on the “Submit message” button. And even more importantly, in the hook, we added an <code>onSucess</code> 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.</p> <h3> <a name="rendering-messages" href="#rendering-messages"> </a> Rendering messages </h3> <p>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 <code>src/appwrite.js</code> file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>// other code export const getMessages = async () => { const { documents: messages } = await databases.listDocuments(database, collection); return messages; }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Now we can fetch them with the React Query <code>useQuery</code> hook:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>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.</p> <h3> <a name="deleting-messages" href="#deleting-messages"> </a> Deleting messages </h3> <p>The last missing step is the ability to delete messages. Once again, we first need to add this function to <code>src/appwrite.js</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>// other code export const deleteMessage = async (id) => { await databases.deleteDocument(database, collection, id); }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>And then, all that’s left is adding one more mutation and the <code>onClick</code> action on the delete button to the <code>src/pages/index.js</code> page. Don’t copy the whole component. Only copy the added delete mutation and the new return statement. Also, make sure to import <code>deleteMessage</code> from AppWrite.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Once again, after successfully deleting a message we tell React Query to refetch all messages so that the deleted message disappears from the list.</p> <h2> <a name="authentication" href="#authentication"> </a> Authentication </h2> <p>To add authentication, we first need some additional actions in the <code>src/appwrite.js</code> 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 <code>createContext</code> from <code>react</code>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>// 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); </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>We now need to fetch the user automatically on every application start and make it available through our context. We’ll do this in <code>src/pages/_app.js</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Then we can build the masks for sign up. For this add a new file <code>src/pages/signup.js</code> with the following content:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>And sign in at <code>src/pages/signin.js</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <h2> <a name="signing-out" href="#signing-out"> </a> Signing out </h2> <p>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 <code>user</code> session exists and if yes, display a “Sign Out” button that calls the <code>signOut</code> function from AppWrite. We’ll change the <code>src/pages/index.js</code> file to this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <h2> <a name="access-control" href="#access-control"> </a> Access control </h2> <p>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 <code>messages</code> schema permissions to this:</p> <p><a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k5CyhanZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqady72m98062monfejf.png" class="article-body-image-wrapper"><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k5CyhanZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqady72m98062monfejf.png" alt="9" loading="lazy" width="800" height="261"></a></p> <p>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”.</p> <p><a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_TIKQAai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dz1iodesc7ite5po0szo.png" class="article-body-image-wrapper"><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_TIKQAai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dz1iodesc7ite5po0szo.png" alt="10" loading="lazy" width="800" height="292"></a></p> <p>Back in the code, modify the <code>addMessage</code> function in <code>src/appwrite.js</code> as follows. Also, don’t forget to update the import statement as shown.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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))] ); }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>That way, we explicitly state that only the owner of the document can delete it. Now, in <code>src/app/index.js</code> in the add message button action, also pass the <code>user.$id</code> of our current user, so AppWrite knows which user we want to give delete permission to:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code><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> </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Now, create a new user on <code>/signup</code>and then use the same credentials on<code>/signin</code>to sign in. Afterward, you will be able to create new messages linked to your account. Try deleting those messages and see what happens.</p> <h2> <a name="showing-the-auth-state-in-the-ui" href="#showing-the-auth-state-in-the-ui"> </a> Showing the auth state in the UI </h2> <p>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:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>{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> </> )} </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>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 <code>user.$id</code> will be present in the <code>delete</code> permission field. We’ll write a function to check for that:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>const canDelete = (userID, array) => { return array.some((element) => element.includes('delete') && element.includes(userID)); }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>And then check for that in the UI for each message to see if we need to render the delete button:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>{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> ))} </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <h2> <a name="wrapping-up" href="#wrapping-up"> </a> Wrapping up </h2> <p>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.</p> <p>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:</p> <ul> <li>Add comments to messages. This will teach you how to do relations in AppWrite.</li> <li>Add infinite scrolling or pagination to deal with a large number of messages that cannot be rendered all at once.</li> <li>Use Next’s Server Side Rendering to fetch and render messages on the server instead of the client.</li> </ul> <p>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 <a href="https://nextjs.org/docs/app/building-your-application/deploying#docker-image">guide</a> by Vercel. </p> <p>And while you’re at it, you can also set up preview environments for your project using <a href="https://preevy.dev/">Preevy</a>. 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.</p> <p>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.</p> <p>Hope you enjoyed and found this guide helpful. </p> <p>Good luck!</p>

  • 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" /> <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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>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. </p> <p>But what exactly are we doing here? We have created a React Query mutation with the <code>useMutation</code> hook. It allows us to then call <code>addMessageMutation.mutate(input)</code> on the “Submit message” button. And even more importantly, in the hook, we added an <code>onSucess</code> 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.</p> <h3> <a name="rendering-messages" href="#rendering-messages"> </a> Rendering messages </h3> <p>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 <code>src/appwrite.js</code> file:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>// other code export const getMessages = async () => { const { documents: messages } = await databases.listDocuments(database, collection); return messages; }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Now we can fetch them with the React Query <code>useQuery</code> hook:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>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.</p> <h3> <a name="deleting-messages" href="#deleting-messages"> </a> Deleting messages </h3> <p>The last missing step is the ability to delete messages. Once again, we first need to add this function to <code>src/appwrite.js</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>// other code export const deleteMessage = async (id) => { await databases.deleteDocument(database, collection, id); }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>And then, all that’s left is adding one more mutation and the <code>onClick</code> action on the delete button to the <code>src/pages/index.js</code> page. Don’t copy the whole component. Only copy the added delete mutation and the new return statement. Also, make sure to import <code>deleteMessage</code> from AppWrite.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Once again, after successfully deleting a message we tell React Query to refetch all messages so that the deleted message disappears from the list.</p> <h2> <a name="authentication" href="#authentication"> </a> Authentication </h2> <p>To add authentication, we first need some additional actions in the <code>src/appwrite.js</code> 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 <code>createContext</code> from <code>react</code>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>// 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); </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>We now need to fetch the user automatically on every application start and make it available through our context. We’ll do this in <code>src/pages/_app.js</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Then we can build the masks for sign up. For this add a new file <code>src/pages/signup.js</code> with the following content:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>And sign in at <code>src/pages/signin.js</code>:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <h2> <a name="signing-out" href="#signing-out"> </a> Signing out </h2> <p>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 <code>user</code> session exists and if yes, display a “Sign Out” button that calls the <code>signOut</code> function from AppWrite. We’ll change the <code>src/pages/index.js</code> file to this:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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> ); } </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <h2> <a name="access-control" href="#access-control"> </a> Access control </h2> <p>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 <code>messages</code> schema permissions to this:</p> <p><a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k5CyhanZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqady72m98062monfejf.png" class="article-body-image-wrapper"><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k5CyhanZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqady72m98062monfejf.png" alt="9" loading="lazy" width="800" height="261"></a></p> <p>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”.</p> <p><a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_TIKQAai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dz1iodesc7ite5po0szo.png" class="article-body-image-wrapper"><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_TIKQAai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dz1iodesc7ite5po0szo.png" alt="10" loading="lazy" width="800" height="292"></a></p> <p>Back in the code, modify the <code>addMessage</code> function in <code>src/appwrite.js</code> as follows. Also, don’t forget to update the import statement as shown.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>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))] ); }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>That way, we explicitly state that only the owner of the document can delete it. Now, in <code>src/app/index.js</code> in the add message button action, also pass the <code>user.$id</code> of our current user, so AppWrite knows which user we want to give delete permission to:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code><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> </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>Now, create a new user on <code>/signup</code>and then use the same credentials on<code>/signin</code>to sign in. Afterward, you will be able to create new messages linked to your account. Try deleting those messages and see what happens.</p> <h2> <a name="showing-the-auth-state-in-the-ui" href="#showing-the-auth-state-in-the-ui"> </a> Showing the auth state in the UI </h2> <p>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:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>{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> </> )} </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>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 <code>user.$id</code> will be present in the <code>delete</code> permission field. We’ll write a function to check for that:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>const canDelete = (userID, array) => { return array.some((element) => element.includes('delete') && element.includes(userID)); }; </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <p>And then check for that in the UI for each message to see if we need to render the delete button:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>{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> ))} </code></pre> <div class="highlight__panel js-actions-panel"> <div class="highlight__panel-action js-fullscreen-code-action"> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title> <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path> </svg> <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title> <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path> </svg> </div> </div> </div> <h2> <a name="wrapping-up" href="#wrapping-up"> </a> Wrapping up </h2> <p>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.</p> <p>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:</p> <ul> <li>Add comments to messages. This will teach you how to do relations in AppWrite.</li> <li>Add infinite scrolling or pagination to deal with a large number of messages that cannot be rendered all at once.</li> <li>Use Next’s Server Side Rendering to fetch and render messages on the server instead of the client.</li> </ul> <p>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 <a href="https://nextjs.org/docs/app/building-your-application/deploying#docker-image">guide</a> by Vercel. </p> <p>And while you’re at it, you can also set up preview environments for your project using <a href="https://preevy.dev/">Preevy</a>. 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.</p> <p>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.</p> <p>Hope you enjoyed and found this guide helpful. </p> <p>Good luck!</p>

  • Appwrite

    Build like a team of hundreds_

  • 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

    Power Real-Time Data Analytics at Scale. Get real-time insights from all types of time series data with InfluxDB. Ingest, query, and analyze billions of data points in real-time with unbounded cardinality.

    InfluxDB logo
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