Project Templates
Social Network
Real-Time Chat for Your Social Network
53 min
introduction in this tutorial, you will learn how to implement a real time messaging system for your social network application using back4app you'll build a complete chat functionality that allows users to send and receive messages instantly, see typing indicators, and manage conversations essential features for engaging social platforms back4app is a backend as a service (baas) platform built on parse server that provides powerful real time capabilities through its live query feature with back4app's real time infrastructure, you can create responsive messaging systems without managing complex websocket servers or scaling concerns by the end of this tutorial, you'll have created a fully functional messaging system similar to the one used in back4gram, a social network application you'll implement conversation creation, real time message exchange, typing indicators, and user search, giving your users a seamless communication experience back4gram project find here the complete code for a social network sample project built with back4app prerequisites to complete this tutorial, you will need a back4app account you can sign up for a free account at back4app com https //www back4app com a back4app project set up with the parse javascript sdk initialized node js installed on your local machine basic knowledge of javascript, react js, and back4app/parse server an authentication system already implemented if you haven't set this up yet, check out our authentication system tutorial https //www back4app com/docs/react/authentication tutorial familiarity with react hooks and component lifecycle step 1 – understanding back4app's real time capabilities before we start coding, let's understand how back4app enables real time functionality through its live query feature live query explained live query is a feature of parse server that allows clients to subscribe to queries and receive updates when objects matching those queries are created, updated, or deleted this is perfect for building real time applications like chat systems here's how live query works the client subscribes to a specific query (e g , "all messages in conversation x") when a matching object is created, updated, or deleted on the server live query automatically notifies all subscribed clients the clients can then update their ui in response to these events in the context of a messaging system, this means when a new message is sent, all users in that conversation receive it instantly when a user starts typing, other users can see a typing indicator in real time when a message is read, read receipts can be updated for all participants setting up live query on back4app to use live query, you need to enable it in your back4app dashboard log in to your back4app dashboard navigate to server settings > web hosting and live query enable live query add the classes you want to use with live query (in our case, "message" and "typingstatus") next, in your client side code, you need to initialize the live query client // initialize parse with your back4app credentials parse initialize("your app id", "your javascript key"); parse serverurl = "https //parseapi back4app com/"; // initialize live query parse livequeryserverurl = "wss\ //your app id back4app io"; now let's move on to designing our database schema for our messaging system back4gram project find here the complete code for a social network sample project built with back4app step 2 – designing the database schema for our messaging system, we'll need several parse classes conversation represents a chat between two or more users message represents individual messages within a conversation typingstatus tracks when users are typing in a conversation let's create these classes in back4app conversation class the conversation class will have the following fields participants (array of pointers to user) the users involved in the conversation lastmessage (string) the text of the most recent message updatedat (date) automatically updated by parse when the record changes message class the message class will have conversation (pointer to conversation) the conversation this message belongs to sender (pointer to user) the user who sent the message text (string) the content of the message createdat (date) automatically created by parse when the message is sent typingstatus class the typingstatus class will track typing indicators user (pointer to user) the user who is typing conversation (pointer to conversation) the conversation where typing is happening istyping (boolean) whether the user is currently typing now, let's set up our project structure to implement this messaging system back4gram project find here the complete code for a social network sample project built with back4app step 3 – setting up the project structure let's organize our code for the messaging system we'll focus on the essential components src/ ├── components/ │ └── ui/ │ ├── toaster js │ └── avatar js ├── pages/ │ └── messagespage js ├── app js └── parseconfig js let's create a simple toaster js for notifications // src/components/ui/toaster js export const toaster = { create ({ title, description, type }) => { // in a real app, you would use a proper toast notification component console log(`\[${type touppercase()}] ${title} ${description}`); alert(`${title} ${description}`); } }; and update our app js to include the messages route // src/app js import react from 'react'; import { browserrouter as router, routes, route } from 'react router dom'; import messagespage from ' /pages/messagespage'; // import other pages function app() { return ( \<router> \<routes> \<route path="/messages" element={\<messagespage />} /> {/ other routes /} \</routes> \</router> ); } export default app; now, let's implement the main messaging functionality back4gram project find here the complete code for a social network sample project built with back4app step 4 – creating the messages page component our messagespage js component will be the core of our messaging system it will display a list of conversations allow users to view and send messages show typing indicators enable searching for users to start new conversations let's build this step by step // src/pages/messagespage js import react, { usestate, useeffect, useref } from 'react'; import { box, flex, input, button, vstack, text, avatar, heading, spinner, center, hstack, divider, } from '@chakra ui/react'; import { usenavigate } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function messagespage() { const \[message, setmessage] = usestate(''); const \[istyping, setistyping] = usestate(false); const \[isloading, setisloading] = usestate(true); const \[issending, setissending] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); const \[conversations, setconversations] = usestate(\[]); const \[activeconversation, setactiveconversation] = usestate(null); const \[messages, setmessages] = usestate(\[]); const \[users, setusers] = usestate(\[]); const \[searchquery, setsearchquery] = usestate(''); const \[showusersearch, setshowusersearch] = usestate(false); const \[processedmessageids, setprocessedmessageids] = usestate(new set()); const \[otherusertyping, setotherusertyping] = usestate(false); const messagesendref = useref(null); const livequerysubscription = useref(null); const typingstatussubscription = useref(null); const typingtimerref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { console log('checking authentication '); const user = await parse user current(); if (!user) { console log('no user found, redirecting to login'); navigate('/login'); return; } console log('user authenticated ', user id, user get('username')); setcurrentuser(user); fetchconversations(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); // clean up subscriptions when component unmounts return () => { if (livequerysubscription current) { livequerysubscription current unsubscribe(); } if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); } if (typingtimerref current) { cleartimeout(typingtimerref current); } }; }, \[navigate]); // we'll implement the following functions next const fetchconversations = async (user) => { // coming up next }; const resetmessagestate = () => { // coming up next }; const fetchmessages = async (conversationid) => { // coming up next }; const setuplivequery = async (conversationid) => { // coming up next }; const setuptypingstatussubscription = async (conversationid) => { // coming up next }; const updatetypingstatus = async (istyping) => { // coming up next }; const sendmessage = async () => { // coming up next }; const searchusers = async (query) => { // coming up next }; const startconversation = async (userid) => { // coming up next }; // format time for messages const formatmessagetime = (date) => { return new date(date) tolocaletimestring(\[], { hour '2 digit', minute '2 digit' }); }; // format date for conversation list const formatconversationdate = (date) => { const now = new date(); const messagedate = new date(date); // if today, show time if (messagedate todatestring() === now\ todatestring()) { return messagedate tolocaletimestring(\[], { hour '2 digit', minute '2 digit' }); } // if this year, show month and day if (messagedate getfullyear() === now\ getfullyear()) { return messagedate tolocaledatestring(\[], { month 'short', day 'numeric' }); } // otherwise show full date return messagedate tolocaledatestring(); }; return ( \<flex h="100vh"> {/ we'll implement the ui next /} \</flex> ); } export default messagespage; now let's implement each of the functions needed for our messaging system back4gram project find here the complete code for a social network sample project built with back4app step 5 – fetching user conversations first, let's implement the fetchconversations function that loads all conversations for the current user const fetchconversations = async (user) => { setisloading(true); try { console log('fetching conversations for user ', user id); // query conversations where the current user is a participant const query = new parse query('conversation'); query equalto('participants', user); query include('participants'); query descending('updatedat'); const results = await query find(); console log('found conversations ', results length); // format conversations const formattedconversations = results map(conv => { const participants = conv get('participants'); // find the other participant (not the current user) const otherparticipant = participants find(p => p id !== user id); if (!otherparticipant) { console warn('could not find other participant in conversation ', conv id); return null; } return { id conv id, user { id otherparticipant id, username otherparticipant get('username'), avatar otherparticipant get('avatar') ? otherparticipant get('avatar') url() null }, lastmessage conv get('lastmessage') || '', updatedat conv get('updatedat') }; }) filter(boolean); // remove any null entries console log('formatted conversations ', formattedconversations); setconversations(formattedconversations); // if there are conversations, set the first one as active if (formattedconversations length > 0) { setactiveconversation(formattedconversations\[0]); fetchmessages(formattedconversations\[0] id); } } catch (error) { console error('error fetching conversations ', error); toaster create({ title 'error', description 'failed to load conversations', type 'error', }); } finally { setisloading(false); } }; this function queries the conversation class for conversations where the current user is a participant formats the results to display in the ui sets the first conversation as active if available back4gram project find here the complete code for a social network sample project built with back4app step 6 – handling messages and real time updates now let's implement the functions to fetch messages and set up real time updates using live query const resetmessagestate = () => { console log('resetting message state'); setmessages(\[]); setprocessedmessageids(new set()); setotherusertyping(false); if (livequerysubscription current) { livequerysubscription current unsubscribe(); livequerysubscription current = null; console log('unsubscribed from live query in resetmessagestate'); } if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); typingstatussubscription current = null; console log('unsubscribed from typing status subscription in resetmessagestate'); } }; const fetchmessages = async (conversationid) => { // reset message state to avoid any lingering messages or subscriptions resetmessagestate(); try { // query messages for this conversation const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); query ascending('createdat'); const results = await query find(); // format messages const formattedmessages = results map(msg => ({ id msg id, text msg get('text'), sender { id msg get('sender') id, username msg get('sender') get('username') }, createdat msg get('createdat') })); // initialize the set of processed message ids const messageids = new set(formattedmessages map(msg => msg id)); // set state after processing all messages setmessages(formattedmessages); setprocessedmessageids(messageids); // set up live query subscription for new messages setuplivequery(conversationid); // set up typing status subscription setuptypingstatussubscription(conversationid); // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); } catch (error) { console error('error fetching messages ', error); toaster create({ title 'error', description 'failed to load messages', type 'error', }); } }; now, let's implement the live query subscription for real time message updates const setuplivequery = async (conversationid) => { // capture the current user in a closure to avoid null reference later const captureduser = currentuser; // unsubscribe from previous subscription if exists if (livequerysubscription current) { livequerysubscription current unsubscribe(); console log('unsubscribed from previous live query'); } try { console log('setting up live query for conversation ', conversationid); // create a query that will be used for the subscription const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); console log('created query for message class with conversation id ', conversationid); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened for conversation ', conversationid); }); // handle new messages livequerysubscription current on('create', (message) => { console log('new message received via live query ', message id); // check if we've already processed this message if (processedmessageids has(message id)) { console log('skipping duplicate message ', message id); return; } // format the new message const newmessage = { id message id, text message get('text'), sender { id message get('sender') id, username message get('sender') get('username') }, createdat message get('createdat') }; console log('formatted new message ', newmessage); // add the message id to the set of processed ids setprocessedmessageids(previds => { const newids = new set(previds); newids add(message id); return newids; }); // add the new message to the messages state setmessages(prevmessages => { // check if the message is already in the list (additional duplicate check) if (prevmessages some(msg => msg id === message id)) { console log('message already in list, not adding again ', message id); return prevmessages; } return \[ prevmessages, newmessage]; }); // use the captured user to avoid null reference if (captureduser && message get('sender') id !== captureduser id) { setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === conversationid) { return { conv, lastmessage message get('text'), updatedat message get('createdat') }; } return conv; }); }); } else { // if user check fails, update without the check setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === conversationid) { return { conv, lastmessage message get('text'), updatedat message get('createdat') }; } return conv; }); }); } // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); // handle subscription close livequerysubscription current on('close', () => { console log('live query connection closed for conversation ', conversationid); }); } catch (error) { console error('error setting up live query ', error); toaster create({ title 'error', description 'failed to set up real time messaging', type 'error', }); } }; this live query setup creates a subscription to new messages in the current conversation handles incoming messages in real time updates the ui with new messages as they arrive updates the conversation list with the latest message and timestamp manages duplicate messages (which can occur when both sending and receiving via live query) back4gram project find here the complete code for a social network sample project built with back4app step 7 – implementing typing indicators to enhance the user experience, let's add real time typing indicators const setuptypingstatussubscription = async (conversationid) => { // cancel previous subscription, if it exists if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); console log('unsubscribed from previous typing status subscription'); } try { console log('setting up typing status subscription for conversation ', conversationid); // create a query for the typingstatus class const query = new parse query('typingstatus'); const conversation = new parse object('conversation'); conversation id = conversationid; // filter by conversation query equalto('conversation', conversation); // don't include the current user's status query notequalto('user', currentuser); // subscribe to the query typingstatussubscription current = await query subscribe(); console log('successfully subscribed to typing status'); // handle create events typingstatussubscription current on('create', (status) => { console log('typing status created ', status get('istyping')); setotherusertyping(status get('istyping')); }); // handle update events typingstatussubscription current on('update', (status) => { console log('typing status updated ', status get('istyping')); setotherusertyping(status get('istyping')); }); // handle errors typingstatussubscription current on('error', (error) => { console error('typing status subscription error ', error); }); } catch (error) { console error('error setting up typing status subscription ', error); } }; const updatetypingstatus = async (istyping) => { if (!activeconversation || !currentuser) return; try { // check if a typing status already exists for this user and conversation const query = new parse query('typingstatus'); const conversation = new parse object('conversation'); conversation id = activeconversation id; query equalto('user', currentuser); query equalto('conversation', conversation); const existingstatus = await query first(); if (existingstatus) { // update existing status existingstatus set('istyping', istyping); await existingstatus save(); } else { // create a new status const typingstatus = parse object extend('typingstatus'); const newstatus = new typingstatus(); newstatus set('user', currentuser); newstatus set('conversation', conversation); newstatus set('istyping', istyping); await newstatus save(); } } catch (error) { console error('error updating typing status ', error); } }; this implementation subscribes to typing status updates from other users in the conversation updates the ui when someone else is typing sends typing status updates when the current user is typing back4gram project find here the complete code for a social network sample project built with back4app step 8 – sending messages now let's implement the message sending functionality const sendmessage = async () => { if (!message trim() || !activeconversation) return; const messagetext = message trim(); // store the message text setissending(true); setmessage(''); // clear input immediately to prevent double sending setistyping(false); // clear typing status locally updatetypingstatus(false); // clear typing status on server // clear typing timer if (typingtimerref current) { cleartimeout(typingtimerref current); } try { // create message const message = parse object extend('message'); const newmessage = new message(); // set conversation pointer const conversation = new parse object('conversation'); conversation id = activeconversation id; newmessage set('conversation', conversation); newmessage set('sender', currentuser); newmessage set('text', messagetext); // save the message const savedmessage = await newmessage save(); console log('message saved ', savedmessage id); // add the message id to the set of processed ids to prevent duplication // when it comes back through live query setprocessedmessageids(previds => { const newids = new set(previds); newids add(savedmessage id); return newids; }); // check if this is a new conversation (no messages yet) // if it is, add the message to the ui immediately if (messages length === 0) { console log('first message in conversation, adding to ui immediately'); const formattedmessage = { id savedmessage id, text messagetext, sender { id currentuser id, username currentuser get('username') }, createdat new date() }; setmessages(\[formattedmessage]); // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); } // update conversation's lastmessage const conversationobj = await new parse query('conversation') get(activeconversation id); conversationobj set('lastmessage', messagetext); await conversationobj save(); // update the conversation in the list setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === activeconversation id) { return { conv, lastmessage messagetext, updatedat new date() }; } return conv; }); }); } catch (error) { console error('error sending message ', error); toaster create({ title 'error', description 'failed to send message', type 'error', }); // if there's an error, put the message back in the input setmessage(messagetext); } finally { setissending(false); } }; this function creates and saves a new message in the back4app database updates the conversation's last message and timestamp handles duplicate prevention when the message comes back via live query provides immediate feedback to the sender back4gram project find here the complete code for a social network sample project built with back4app step 9 – user search and starting new conversations let's implement the ability to search for users and start new conversations const searchusers = async (query) => { if (!query trim()) { setusers(\[]); return; } try { console log('searching for users with query ', query); // create a query for the user class const userquery = new parse query(parse user); // search for username containing the query string (case insensitive) userquery contains('username', query tolowercase()); // don't include the current user in results userquery notequalto('objectid', currentuser id); // limit to 10 results userquery limit(10); // execute the query const results = await userquery find(); console log('user search results ', results length); // format users const formattedusers = results map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null })); console log('formatted users ', formattedusers); setusers(formattedusers); } catch (error) { console error('error searching users ', error); toaster create({ title 'error', description 'failed to search users', type 'error', }); } }; const startconversation = async (userid) => { console log('starting conversation with user id ', userid); try { // check if a conversation already exists with this user const query = new parse query('conversation'); // create pointers to both users const currentuserpointer = parse user current(); const otheruserpointer = new parse user(); otheruserpointer id = userid; // find conversations where both users are participants query containsall('participants', \[currentuserpointer, otheruserpointer]); const existingconv = await query first(); if (existingconv) { console log('found existing conversation ', existingconv id); // get the other user object const userquery = new parse query(parse user); const otheruser = await userquery get(userid); // format the conversation const conversation = { id existingconv id, user { id otheruser id, username otheruser get('username'), avatar otheruser get('avatar') ? otheruser get('avatar') url() null }, lastmessage existingconv get('lastmessage') || '', updatedat existingconv get('updatedat') }; // set as active conversation setactiveconversation(conversation); // fetch messages and set up live query await fetchmessages(conversation id); } else { console log('creating new conversation with user ', userid); // get the other user object const userquery = new parse query(parse user); const otheruser = await userquery get(userid); // create a new conversation const conversation = parse object extend('conversation'); const newconversation = new conversation(); // set participants newconversation set('participants', \[currentuserpointer, otheruserpointer]); newconversation set('lastmessage', ''); // save the conversation const savedconv = await newconversation save(); console log('new conversation created ', savedconv id); // format the conversation const conversation = { id savedconv id, user { id otheruser id, username otheruser get('username'), avatar otheruser get('avatar') ? otheruser get('avatar') url() null }, lastmessage '', updatedat savedconv get('updatedat') }; // add to conversations list setconversations(prev => \[conversation, prev]); // set as active conversation and reset message state setactiveconversation(conversation); resetmessagestate(); // set up live query for the new conversation await setuplivequery(savedconv id); // set up typing status subscription await setuptypingstatussubscription(savedconv id); // reset search setshowusersearch(false); setsearchquery(''); setusers(\[]); } } catch (error) { console error('error starting conversation ', error); toaster create({ title 'error', description 'failed to start conversation', type 'error', }); } }; this code provides two important functions for our messaging system user search the searchusers function allows users to find other users by username it queries the user class in back4app, excludes the current user from results, and formats the data for display conversation creation the startconversation function handles both finding existing conversations and creating new ones it checks if a conversation already exists between the users if one exists, it loads that conversation and its messages if no conversation exists, it creates a new one sets up live query subscriptions for new messages and typing status with these functions implemented, users can easily find other users and start conversations with them, which is essential for a social network messaging system back4gram project find here the complete code for a social network sample project built with back4app step 10 – building the chat ui now that we've implemented all the functionality, let's create the user interface for our messaging system the ui will consist of a sidebar listing all conversations a search interface to find users a chat window showing messages a message input with typing indicator support here's the jsx for our messagespage component return ( \<flex h="100vh"> {/ chat list sidebar /} \<box w="300px" borderrightwidth="1px" bordercolor="gray 600" p={4} bg="gray 800"> \<heading size="md" mb={4}> messages \</heading> \<flex mb={4}> \<input placeholder="search users " value={searchquery} onchange={(e) => { const value = e target value; setsearchquery(value); if (value trim() length > 0) { searchusers(value); } else { setusers(\[]); } }} mr={2} /> \<button onclick={() => { setshowusersearch(!showusersearch); if (!showusersearch) { setsearchquery(''); setusers(\[]); } }} \> {showusersearch ? 'cancel' 'new'} \</button> \</flex> {isloading ? ( \<center py={10}> \<spinner size="lg" /> \</center> ) showusersearch ? ( // user search results \<vstack align="stretch" spacing={2}> {users length > 0 ? ( users map(user => ( \<box key={user id} p={3} borderradius="md" hover={{ bg "gray 700" }} cursor="pointer" onclick={() => startconversation(user id)} \> \<hstack> \<avatar size="sm" name={user username} src={user avatar} /> \<text>{user username}\</text> \</hstack> \</box> )) ) searchquery ? ( \<text color="gray 400" textalign="center">no users found\</text> ) ( \<text color="gray 400" textalign="center">search for users to message\</text> )} \</vstack> ) ( // conversations list \<vstack align="stretch" spacing={0}> {conversations length > 0 ? ( conversations map(conv => ( \<box key={conv id} p={3} borderradius="md" bg={activeconversation? id === conv id ? "gray 700" "transparent"} hover={{ bg "gray 700" }} cursor="pointer" onclick={() => { if (activeconversation? id !== conv id) { setactiveconversation(conv); fetchmessages(conv id); } }} \> \<hstack> \<avatar size="sm" name={conv user username} src={conv user avatar} /> \<box flex="1" overflow="hidden"> \<flex justify="space between" align="center"> \<text fontweight="bold" nooflines={1}>{conv user username}\</text> \<text fontsize="xs" color="gray 400"> {formatconversationdate(conv updatedat)} \</text> \</flex> \<text fontsize="sm" color="gray 400" nooflines={1}> {conv lastmessage || 'no messages yet'} \</text> \</box> \</hstack> \</box> )) ) ( \<text color="gray 400" textalign="center" mt={8}> no conversations yet start a new one! \</text> )} \</vstack> )} \</box> {/ chat window /} \<box flex={1} p={0} display="flex" flexdirection="column" bg="gray 900"> {activeconversation ? ( <> {/ chat header /} \<flex align="center" p={4} borderbottomwidth="1px" bordercolor="gray 700"> \<avatar size="sm" mr={2} name={activeconversation user username} src={activeconversation user avatar} /> \<text fontweight="bold">{activeconversation user username}\</text> {otherusertyping && ( \<text ml={2} color="gray 400" fontsize="sm"> is typing \</text> )} \</flex> {/ messages /} \<box flex={1} p={4} overflowy="auto" display="flex" flexdirection="column" \> {messages length > 0 ? ( messages map((msg) => ( \<box key={msg id} alignself={msg sender id === currentuser id ? "flex end" "flex start"} bg={msg sender id === currentuser id ? "blue 500" "gray 700"} color={msg sender id === currentuser id ? "white" "white"} p={3} borderradius="lg" maxw="70%" mb={2} \> \<text>{msg text}\</text> \<text fontsize="xs" color={msg sender id === currentuser id ? "blue 100" "gray 400"} textalign="right" mt={1}> {formatmessagetime(msg createdat)} \</text> \</box> )) ) ( \<center flex={1}> \<text color="gray 400"> no messages yet say hello! \</text> \</center> )} \<div ref={messagesendref} /> \</box> {/ message input /} \<flex p={4} bordertopwidth="1px" bordercolor="gray 700"> \<input placeholder="type a message " value={message} onchange={(e) => { setmessage(e target value); // set typing status to true locally setistyping(true); // update typing status on server updatetypingstatus(true); // clear any existing timer if (typingtimerref current) { cleartimeout(typingtimerref current); } // set a new timer to turn off typing status after 2 seconds of inactivity typingtimerref current = settimeout(() => { setistyping(false); updatetypingstatus(false); }, 2000); }} mr={2} onkeypress={(e) => { if (e key === 'enter' && !e shiftkey) { e preventdefault(); sendmessage(); // also clear typing status when sending a message setistyping(false); updatetypingstatus(false); if (typingtimerref current) { cleartimeout(typingtimerref current); } } }} /> \<button colorscheme="blue" onclick={sendmessage} isloading={issending} disabled={!message trim() || issending} \> send \</button> \</flex> \</> ) ( \<center flex={1}> \<vstack> \<text fontsize="xl" fontweight="bold">welcome to messages\</text> \<text color="gray 400"> select a conversation or start a new one \</text> \</vstack> \</center> )} \</box> \</flex> ); let's break down the ui into its main components conversations sidebar the left sidebar shows a list of all conversations each conversation shows the other user's name, avatar, and the last message users can click on any conversation to view and continue the chat a search bar at the top allows finding other users to start new conversations message display the main area displays messages for the active conversation messages from the current user appear on the right with a blue background messages from other users appear on the left with a gray background each message shows the message text and the time it was sent the message list automatically scrolls to the bottom when new messages arrive message input at the bottom is an input area to type and send messages users can press enter to send messages a send button also allows sending messages typing in the input area automatically triggers typing indicators the input is disabled until a conversation is selected typing indicators when the other user is typing, a "is typing " indicator appears the indicator is updated in real time using the typingstatus live query subscription this ui provides a clean and intuitive interface for messaging, similar to what users would expect from popular messaging applications back4gram project find here the complete code for a social network sample project built with back4app step 11 – testing live query connections to ensure that our live query connections are working properly, let's add a test function that verifies the connection when the component mounts // add this at the beginning of the component to test live query connection useeffect(() => { // test live query connection const testlivequery = async () => { try { console log('testing live query connection '); console log('live query url ', parse livequeryserverurl); const query = new parse query('message'); console log('created test query for message class'); const subscription = await query subscribe(); console log('live query subscription successful!'); subscription on('open', () => { console log('live query connection opened successfully'); }); subscription on('create', (object) => { console log('live query create event received ', object id); }); subscription on('error', (error) => { console error('live query error ', error); }); // unsubscribe after a few seconds to test settimeout(() => { subscription unsubscribe(); console log('unsubscribed from test live query'); }, 10000); } catch (error) { console error('error testing live query ', error); } }; testlivequery(); }, \[]); this test function attempts to create a subscription to the message class sets up event handlers for connection events logs the results of each step automatically unsubscribes after 10 seconds by watching the console logs, you can verify that the live query url is correctly configured the connection can be established events are being properly received if you encounter any issues with the live query connection, check the following back4app live query is enabled for your app the live query url is correctly set in your parse initialization the message class is added to the live query classes list in back4app your network allows websocket connections back4gram project find here the complete code for a social network sample project built with back4app step 12 – understanding how back4app enables real time messaging now that we've built a complete messaging system, let's take a deeper look at how back4app's features make real time messaging possible live query infrastructure back4app's live query feature is built on websockets, which provide a persistent connection between the client and server this is fundamentally different from the traditional http request/response model traditional rest api client makes a request → server responds → connection closes websockets/live query client establishes a persistent connection → server can push updates at any time this persistent connection is what enables the real time capabilities in our messaging system when a new message is created, back4app automatically pushes that update to all subscribed clients without them having to poll the server parse server's subscription model the subscription model in parse server is query based this means you subscribe to a specific query (e g , "all messages in conversation x") you receive updates only for objects that match that query you can receive different types of events ( create , update , delete , etc ) this query based approach is extremely powerful because it allows for precise subscriptions in our messaging system, we're only subscribing to messages for the active conversation, which is much more efficient than subscribing to all messages database considerations when using live query with back4app, there are some important database considerations indexing ensure that fields used in subscription queries are indexed for better performance data size keep objects small to reduce payload size and improve performance scaling live query connections use resources, so consider this when scaling your application for optimal performance, consider creating indexes on conversation fields in the message class using pointers rather than embedding large objects managing the lifecycle of subscriptions to avoid memory leaks security considerations when implementing real time messaging, security is crucial acls and clps use access control lists and class level permissions to protect your data connection authentication only authenticated users should be able to establish live query connections data validation validate message content on the server side using cloud code rate limiting implement rate limiting to prevent abuse for example, you might want to use cloud code to validate and sanitize messages before they're saved // in cloud code parse cloud beforesave("message", async (request) => { const message = request object; const text = message get("text"); // check if message is empty if (!text || text trim() length === 0) { throw new error("message cannot be empty"); } // sanitize message text message set("text", sanitizehtml(text)); // rate limiting check if user is sending too many messages const sender = message get("sender"); const query = new parse query("message"); query equalto("sender", sender); query greaterthan("createdat", new date(new date() 60000)); // last minute const count = await query count({usemasterkey true}); if (count > 10) { throw new error("you are sending messages too quickly please slow down "); } }); back4gram project find here the complete code for a social network sample project built with back4app step 13 – testing and optimizing the messaging system to ensure your messaging system functions correctly, you should thoroughly test it basic functionality testing message sending verify that messages can be sent and received conversation creation test creating new conversations user search confirm that users can be found and conversations started ui updates check that typing indicators, message lists, and conversation lists update correctly multi user testing to properly test a messaging system, you need to test with multiple users open two browser windows or tabs log in as different users in each start a conversation between them send messages back and forth test typing indicators and real time updates performance optimization for larger applications, consider these optimizations pagination load messages in batches rather than all at once connection management only establish live query connections for active conversations efficient queries use targeted queries and include only necessary fields caching implement client side caching for frequently accessed data here's an example of implementing pagination for message loading const fetchmessages = async (conversationid, limit = 20, skip = 0) => { try { const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); query descending('createdat'); query limit(limit); query skip(skip); const results = await query find(); // process results return { messages formattedmessages, hasmore results length === limit }; } catch (error) { console error('error fetching messages ', error); throw error; } }; error handling and recovery robust error handling is crucial for real time applications connection failures implement reconnection logic for websocket failures message failures handle failed message sends and provide retry options state recovery recover application state after connection interruptions for example, you might add connection monitoring // monitor live query connection status parse livequery on('open', () => { console log('live query connection established'); // re establish subscriptions if needed }); parse livequery on('close', () => { console log('live query connection closed'); // show connection status to user }); parse livequery on('error', (error) => { console error('live query error ', error); // handle error, potentially reconnect }); conclusion in this tutorial, you've built a comprehensive real time messaging system for your social network using back4app you've implemented user to user messaging created a system where users can find and message each other real time updates used back4app's live query to deliver messages instantly typing indicators enhanced the user experience with real time typing notifications conversation management built a ui for viewing and managing multiple conversations back4app's live query feature provided the real time infrastructure needed to make this possible without having to manage complex websocket servers or scaling concerns by leveraging parse server's built in subscription model, you were able to create a responsive and efficient messaging system the messaging system you've built provides a solid foundation that you can enhance with additional features next steps read receipts implement message read status tracking group conversations extend the system to support multiple participants media sharing add support for images, videos, and other attachments message reactions allow users to react to messages with emojis message deletion implement the ability to delete or edit messages advanced search add search capabilities within conversations for the complete code of the back4gram social network application, including its messaging system, you can check out the github repository https //github com/templates back4app/back4gram back4app's combination of database, apis, cloud functions, file storage, user management, and real time capabilities makes it an excellent choice for building feature rich social network applications by using back4app, you can focus on creating a great user experience rather than managing complex backend infrastructure