Chat backend with Supabase & Socket.io
Tags: node, javascript, postgres, serverless
Published: 2023-01-30 15:06:13

Let’s build the back end for a real time chat application in Supabase using Node.js.

This is also the second part in a three-part series of building a React Native Chat app, so if you're curious, check them out:
* [Part 1 - Setting up WhatsApp UI in React Native](https://dev.to/jacksbridger/whatsapp-ui-in-react-native-part-1-4nbm)
* [Part 3 - Using real data in our app](https://dev.to/jacksbridger/whatsapp-in-react-native-part-3-1gmf)

[Source code](https://github.com/nsmet/supabase-node-chat-backend)

### Step 0 - Download a http client

We will be using a http client extensively in this tutorial. We use [Postman](https://www.postman.com/) and. We uploaded our Postman collection on GitHub - so you can import it into Postman and use the queries we set up.

### **Step 1. Setting up your Supabase account**

Head over to [https://supabase.com/](https://supabase.com/) and create a new project.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkmjc9rlg4sl1ujcu7nf.png)

Take note of your anon public key and service role, you’ll need this later on. You can also retrieve this later in the settings.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xaga05fk4jtf1y5jq72y.png)



### Step 2. Creating your database

First step in setting up our database is creating the needed tables. In this case we go for the simplest approach. 

Our base models: 

- **User:** username, id , created date
- **Conversation**: name of the conversation, who the owner is, id, created date
- **Message**: conversation id, the actual message content, who sent it, id, created date

We can capture this setup in the following Query:

```tsx
CREATE TABLE users (
    id UUID PRIMARY KEY default uuid_generate_v4(),
    username VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

CREATE TABLE conversations (
    id UUID PRIMARY KEY default uuid_generate_v4(),
    name VARCHAR(255) NOT NULL,
    owner_user_id UUID REFERENCES users(id) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

CREATE TABLE user_conversation (
    id UUID PRIMARY KEY default uuid_generate_v4(),
    user_id UUID REFERENCES users(id),
    conversation_id UUID REFERENCES conversations(id)
);

CREATE TABLE messages (
    id UUID PRIMARY KEY default uuid_generate_v4(),
    conversation_id UUID REFERENCES conversations(id),
    user_id UUID REFERENCES users(id),
    message TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL
);
```

Head over to Supabase → **SQL Editor** and copy paste the above Query. Now hit run. You can head back over to **Table Editor** to see your newly created tables.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yk0l2ak637mylev2jxi1.png)



Now we’re done with our database setup we’ll create a Node.JS server.

### Step 3. Setup a new Node.JS server

[Source code](https://github.com/nsmet/supabase-node-chat-backend)
 
Create a new folder locally and run 

```tsx
npm init
```

Run through the wizard, and at the end you should have a base package.json for us to start from. 

Now let’s install some dependencies we’ll need for this project. 

```tsx
npm install @supabase/supabase-js body-parser dotenv express socket.io
npm install -D @types/express @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint nodemon ts-node typescript
```

Our main dependencies are:

- **Supabase** (for intereacting with our database)
- **Body-parser** for our requests
- **Dotenv** for env variables
- **Express** for our server
- **Socket.IO** for our websocket

For our development dependencies we’re going to install: 

- **Typescript**
- Types for Typescript, **Eslint**, **Express**
- **Eslint**
- **Nodemon** to watch for file changes
- **Ts-node** for Node

In your **package.json** add the following changes

```tsx
"main": "index.ts"

"scripts": {
    "start": "nodemon -r dotenv/config src/index.ts",
    "lint": "eslint . --fix",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
```

Your **package.json** should look this

```tsx
{
  "name": "supabase-node-chat-backend",
  "version": "1.0.0",
  "description": "Node backend for a chat application, using Supabase",
  "main": "index.ts",
  "scripts": {
    "start": "nodemon -r dotenv/config src/index.ts",
    "lint": "eslint . --fix",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nsmet/supabase-node-chat-backend.git"
  },
  "keywords": [
    "supabase",
    "node",
    "chat"
  ],
  "author": "Nick Smet",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/nsmet/supabase-node-chat-backend/issues"
  },
  "homepage": "https://github.com/nsmet/supabase-node-chat-backend#readme",
  "dependencies": {
    "@supabase/supabase-js": "^2.4.1",
    "body-parser": "^1.20.1",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "socket.io": "^4.5.4"
  },
  "devDependencies": {
    "@types/express": "^4.17.15",
    "@types/node": "^18.11.18",
    "@typescript-eslint/eslint-plugin": "^5.48.2",
    "@typescript-eslint/parser": "^5.48.2",
    "eslint": "^8.32.0",
    "nodemon": "^2.0.20",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  }
}
```

Next up create a basic folder layout, and create the files shown in this list. Note: I’m using Yarn as my dependency manager here, but you can also use NPM.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/syns852ua8g8cx70hvtt.png)



Below is some boilerplate code for some files already

**.env.example + .env**

```tsx
SUPABASE_PROJECT_URL=
SUPABASE_PUBLIC_ANON=
```

**.eslintignore**

```tsx
dist
```

**.eslintrc.js**

```tsx
module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: "latest", // Allows the use of modern ECMAScript features
    sourceType: "module", // Allows for the use of imports
  },
  extends: ["plugin:@typescript-eslint/recommended"], // Uses the linting rules from @typescript-eslint/eslint-plugin
  env: {
    node: true, // Enable Node.js global variables
  },
  rules: {
    'no-console': 'off',
    'import/prefer-default-export': 'off',
    '@typescript-eslint/no-unused-vars': 'warn',
  },
};
```

**.gitignore**

```tsx
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Dependency directories
node_modules/

# dotenv environment variables file
.env
.env.test
dist
```

**tsconfig.json**

```tsx
{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "lib": ["es6"],                                      /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    "rootDir": "src",                                    /* Specify the root folder within your source files. */
    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
    "resolveJsonModule": true,                           /* Enable importing .json files. */
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    "allowJs": true,                                     /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
    "outDir": "build",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    "noImplicitAny": true,                               /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}
```

So what have you done exactly? You created a new project, with Typescript support, and that also has built-in Eslint validation.

Now that the admin is done, let’s get to the fun stuff!

### Step 4. Create a basic Express server

Open up **index.ts** and put in the following code

```tsx
import express from "express";
import bodyParser from "body-parser";
import cors from 'cors';

const app = express();

app.use(bodyParser.urlencoded({ extended: false}));
app.use(bodyParser.json());
app.use(cors())

app.get("/", function (req, res) {
	res.send("Hello World");
});
app.listen(3000);
```

Now open up the terminal and run 

```tsx
npm run start 
```

Open up the browser and go to **http://localhost:3000**, You should now see “Hello world”


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



Congrats, you just setup your Express server!

### **Step 4. Setup our Types**

We’re going to have two type files: 

- **supabase.types.ts** → This contains our database types, generated by Supabase
- **types.ts** → General model types of our application

Open up the terminal, and go inside of the “src” folder. Now run 

```tsx
// First authenticate with Supabase
npx supabase login

// Generate the types file
npx supabase gen types typescript --project-id "YOUR_PROJECT_ID" --schema public > src/supabase.types.ts
```

In your **types.ts** file you can insert the following

```tsx
import { Socket } from "socket.io"

export interface TypedRequestBody<T> extends Express.Request {
    body: T
  }
  
export interface TypedRequestQuery<T> extends Express.Request {
    query: T
}

export interface TypedRequestQueryWithBodyAndParams<Params, ReqBody> extends Express.Request {
    body: ReqBody,
    params: Params
}

export interface TypedRequestQueryAndParams<Params, Query> extends Express.Request {
    params: Params
    query: Query,
}

export interface TypedRequestQueryWithParams<Params> extends Express.Request {
    params: Params
}

export interface User {
    id: string;
    username: string;
    created_at: string;
}

export interface Conversation {
    id: string;
    name: string;
    user_owner_id: string;
    created_at: string;
}

export interface Message {
    id: string;
    user_id: string;
    message: string;
    created_at: string;
}

export interface UserConversation {
    user_id: string;
    conversation_id: string;
}

export interface SocketConnectedUsers {
    [key: string]: {
        socketId: string;
        socket: Socket;
        user: User;
    }
}

export interface SocketSocketIdUserId {
    [key: string]: string
}
```

For now focus on **User** / **Message** / **Conversation** / **UserConversation**. The other ones will become clearer in a minute. 

### Step 5. Connect to your Supabase DB

in **utils/supabase.ts** insert the following

```tsx
import { createClient } from '@supabase/supabase-js';
import { Database } from '../supabase.types';

const supabase = createClient<Database>(process.env.SUPABASE_PROJECT_URL as string, process.env.SUPABASE_PUBLIC_ANON as string);

export default supabase;
```

We will create a new client that connects to our database. 

Don’t forget to fill in your env with **SUPABASE_PROJECT_URL,** and **SUPABASE_PUBLIC_ANON**

### Step 6. First endpoints: Creating, and searching for users

In this example we won’t allow users to authenticate, they can just signup with a username. 

**This is not production ready code, in production you do want some form of authentication.** 

Open up **src/controllers/user.controller.ts** and ****Add in the following

```tsx
import { Response } from "express"
import supabase from "../utils/supabase"
import { TypedRequestBody, TypedRequestQuery } from "../types"

export const createUser = async function (req: TypedRequestBody<{username: string}>, res: Response) {
    const { data, error } = await supabase
        .from('users')
        .upsert({ 
            username: req.body.username,
            created_at: ((new Date()).toISOString()).toLocaleString()
        })
        .select()

    if (error) {
        res.send(500)
    } else {
        res.send(data[0])
    }
}

export const searchUsers = async function (req: TypedRequestQuery<{user_id: string, q: string}>, res: Response) {
    
    let query = supabase
      .from('users')
      .select();
    
    if (req.query.q) {
        query = query.like('username', `%${req.query.q}%`)
    }

    query = query.neq('id', req.query.user_id)
    .limit(50);
  
    const { data, error } = await query;
    
    if (error) {
        res.send(500)
    } else {
        res.send(data)
    }
}
```

We have two functions: 

- **createUser**: When a new user wants to start messaging, they should first signup, and receive their user id
- **searchUsers**: Searching on all users when starting a new conversation

**Note:** For searching users you can see that “q” is optional.

Now in **src/index.ts** define the endpoints. 

```tsx
// Add the import 
import { createUser, searchUsers } from './controllers/user.controller';

// Add the user endpoints
app.post("/users/create", createUser);
app.get("/users/search", searchUsers);
```

If we try the **/users/create** endpoint out from a http client (e.g. Postman), it works perfectly!


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rh073cxjsk3tyr8f13ty.png)



### Step 7. Fetching all conversations, and creating a new one

Open up **src/controllers/conversation.controller.ts** and add in the following code

```tsx
import { Response } from "express"
import supabase from "../utils/supabase"
import { 
    TypedRequestBody, 
    TypedRequestQuery, 
    TypedRequestQueryWithBodyAndParams, 
    TypedRequestQueryAndParams,
    User,
    Message
} from '../types';

export const getAllConversations = async function (req: TypedRequestQuery<{user_id: string}>, res: Response) {
    // get all conversations this user is attached to 
    const paticipatingConversationIds = await supabase
        .from('user_conversation')
        .select('conversation_id')
        .eq('user_id', req.query.user_id)

    if (!paticipatingConversationIds.data?.length) {
        return res.send([]);
    }

    const conversations = await supabase
        .from('conversations')
        .select(`
            *, 
            messages (
                id,
                conversation_id,
                message,
                created_at,
                users (
                    id,
                    username
                )
            )
        `)
        .or(`owner_user_id.eq.${req.query.user_id},or(id.in.(${paticipatingConversationIds.data.map((item: any) => item.conversation_id)}))`)

    return res.send(conversations.data)
}
```

Our first function will be retrieving all conversations this user is a part of. 

A user can be part of a conversation in two ways: 

- As the owner (**owner_id** on the conversations table)
- As a participant of a conversation (**user_conversation** table)

First let's look at conversations which they’re a participant in. 

Then, we will get all conversations where they’re the owner, or where the id is found in our participant conversations. 

### Quick Aside

_Currently we return all messages within the conversation. But ideally we’d like to return the most recent 10 messages and then get more as they scroll up. But Supabase in their client SDK doesn’t allow a good way to do this at the moment (as far as we’ve seen!)

To get around this, you could potentially write your own Postgres function (also known as a stored procedure) for fetching messages incrementally. _

Next up, add in the following code to create the conversation

```tsx
//src/controllers/conversation.controller.ts

export const createConversation = async function (req: TypedRequestBody<{owner_id: string, participant_ids: string[], group_name: string}>, res: Response) {
    const {
      owner_id,
      participant_ids,
      group_name,
    } = req.body;
  
    // first create the conversation 
    const conversation = await supabase
      .from('conversations')
      .upsert({ 
        name: group_name,
        owner_user_id: owner_id,
        created_at: ((new Date()).toISOString()).toLocaleString()
       })
      .select()

    if (conversation.error) {
        res.send(500)
    }

    let participants: User[] = [];

    if (participant_ids.length > 1 && conversation.data?.length) {
        // attach all our users to this conversation
        const pivotData = await supabase
            .from('user_conversation')
            .upsert(participant_ids.map((participant_id) => {
            return { 
                user_id: participant_id, 
                conversation_id: conversation.data[0].id
            }
            }))
            .select()

            if (pivotData.data?.length) {
                // find our actual users 
                const actualParticipantUsers = await supabase
                    .from('users')
                    .select()
                    .in('id', participant_ids)

                if (actualParticipantUsers.data?.length) participants = actualParticipantUsers.data;
            }
    }

 
    
    if (conversation.error) {
        return res.sendStatus(500)
    } else {
        const conv: Conversation = {
            ...conversation.data[0],
            participants
        };

        return res.send(conv);
    }
}
```

We receive the desired group name, the participants, and who the owner is. We will create our base conversation first, so we have its **id**. 

Then we fill in the **user_conversation** table to add in the participants.

In **index.ts** add the following imports, and endpoints

```tsx
import { 
  createConversation, 
  getAllConversations, 
} from './controllers/conversation.controller';

// CONVERSATION ENDPOINTS
app.post("/conversations/create", createConversation);
app.get("/conversations", getAllConversations)
```

Try it out in Postman, and you should have just created your first chat conversation!


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h4adx65mjwyhbyq3fqid.png)



### Step 8. Getting conversation messages, and sending a new message

In the same **src/controllers/conversation.controller.ts** file, add in these two functions

```tsx
// src/controllers/conversation.controller.ts
export const getConversationMessages = async function (req: TypedRequestQueryAndParams<{conversation_id: string} ,{last_message_date: Date}>, res: Response) {
    const { conversation_id } = req.params;
    const { last_message_date } = req.query;

    let query = supabase
        .from('messages')
        .select(`
            id,
            conversation_id,
            message,
            created_at,
    
            users (
                id,
                username
            )
        `)
        .order('created_at', { ascending: true })
        .eq('conversation_id', conversation_id)
        
        if (last_message_date){
            query = query.gt('created_at', last_message_date)
        }

    const messages = await query;    

    res.send(messages.data)
}
```

The client sends us **conversation_id** and we use that to get all messages in a specific conversation using **.eq('conversation_id', conversation_id)**

There is also the option to give a **last_message_date**, this you could use to only fetch the new messages created after the last message the client has in memory.

```tsx
// src/controllers/conversation.controller.ts
export const addMessageToConversation = async function (req: TypedRequestQueryWithBodyAndParams<{conversation_id: string}, {user_id: string, message: string}>, res: Response) {
    const conversationid = req.params.conversation_id;
    const {
      user_id,
      message,
    } = req.body;

    const data = await supabase
      .from('messages')
      .upsert({ 
        conversation_id: conversationid,
        user_id,
        message,
        created_at: ((new Date()).toISOString()).toLocaleString()
      })
      .select(`
        *,
        users (
            id,
            username
        ),
        conversations (*)
      `)

    // get the users in this chat, except for the current one
    const userConversationIds = await supabase
        .from('user_conversation')
        .select('user_id')
        .eq('conversation_id', conversationid)

    if (data.error) {
        res.send(500)
    } else {
        res.send(
            data.data[0]
        )
    }
}
```

a**ddMessageToConversation** is used to create a new message, and link the message to a specific conversation.

Try creating a new message from your http client, and you should get something like the following response.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jaa1zysxvn5u4cqz03r4.png)



### Step 9. Adding [Socket.IO](http://Socket.IO) for realtime communication

Open up your **src/utils/socket.ts** file and paste in the following starting code

```tsx
// src/utils/socket.ts
import { Server } from "socket.io";
import { SocketConnectedUsers, SocketSocketIdUserId, User, Message, Conversation } from '../types';

class Socket {
    private static _instance: Socket;

    private io;
    private users: SocketConnectedUsers = {};
    private socketIdUserId: SocketSocketIdUserId = {};

    private constructor(server: Server) {
        this.io = server;

        this.io.on('connection', (socket) => {
            console.log('a user connected');

            socket.on('join', (user: User) => {
                this.users[user.id] = {
                    socketId: socket.id,
                    socket: socket,
                    user
                }

                this.socketIdUserId[socket.id] = user.id;
            });

            socket.on('disconnect', () => {
                const userId = this.socketIdUserId[socket.id];

                if (userId) {
                    delete this.users[userId];
                    delete this.socketIdUserId[socket.id]
                }
            });
        });
    }

    static getInstance(server?: Server) {
        if (this._instance) {
            return this._instance;
        }

        if (server) {
            this._instance = new Socket(server);
            return this._instance;
        }

        return Error("Failed to init socket");
    }
}

export default Socket;
```

The class is going to be a **singleton** (a class that can have only one object) and will be used across our app. 

We initialise our **websocket** in the constructor, and keep track of who is connected. We also write a function **getInstance** that returns the singleton. This will be used by the rest of our application. 

In the client we use the event “**join**” as our connected event. This indicates a new client has come online, and wants to connect to our websocket for realtime events. 

When this happens, we keep track of this new user that is connected. 

And we also keep track of it in the reverse format - so we link socket id to user id. 

```jsx
 socket.on('join', (user: User) => {
                this.users[user.id] = {
                    socketId: socket.id,
                    socket: socket,
                    user
                }

                this.socketIdUserId[socket.id] = user.id;
            });
```

The event “**disconnect**” is used to clean up our users when the client goes offline. So our in memory list of users doesn’t keep growing endlessly.

We also remove the data we created above.

```jsx
socket.on('disconnect', () => {
                const userId = this.socketIdUserId[socket.id];

                if (userId) {
                    delete this.users[userId];
                    delete this.socketIdUserId[socket.id]
                }
            });
```

Head over to **index.ts** and update it with the following code ****

```tsx
...
import { Server } from 'socket.io';
import http from 'http';
import Socket from "./utils/socket";

...
const server = http.createServer(app);
const ioServer = new Server(server);
Socket.getInstance(ioServer);

...
// change to
app.listen(3000); -> server.listen(3000);
```

In short: We create a new instance of our [Socket.IO](http://Socket.IO) server using our **getInstance** function, and listen on port 3000 for incoming messages.

Next step is to add in two socket events: 

- Notify when a conversation has a **new message**
- Notify when a user is added to a **new conversation**

In **src/utils/socket.ts** file add these two functions:

```tsx
public static sendMessageToUsers  (userIds: string[], message: Message) {
      userIds.forEach((item) => {
          const user = this._instance.users[item];

          if (user) {
              user.socket.emit('message', message);
          }
      })
  }

  public static notifyUsersOnConversationCreate  (userIds: string[], conversation: Conversation) {
      userIds.forEach((item) => {
          const user = this._instance.users[item];

          if (user) {
              user.socket.emit('newConversation', conversation);
          }
      })
  }
```

Both functions take an array of **userIds** that we will use to send direct messages to (if connected).

Last step is to hook it up in **src/controllers/conversation.controller.ts**

```tsx

// ADD AN IMPORT STATEMENT 
import Socket from '../utils/socket';

// UPDATE THE FOLLOWING FUNCTIONS
export const createConversation = async function (req: TypedRequestBody<{owner_id: string, participant_ids: string[], group_name: string}>, res: Response) {
...
// UPDATE
if (conversation.error) {
        return res.sendStatus(500)
    } else {
        const conv: Conversation = {
            ...conversation.data[0],
            participants
        };

        Socket.notifyUsersOnConversationCreate(participant_ids as string[], conv)
        return res.send(conv);
    }
}

export const addMessageToConversation = async function (req: TypedRequestQueryWithBodyAndParams<{conversation_id: string}, {user_id: string, message: string}>, res: Response) {
...
// UPDATE 
if (data.error) {
        res.send(500)
    } else {
        if (userConversationIds.data && userConversationIds.data?.length > 0) {
            const userIdsForMessages = userConversationIds.data.map((item) => item.user_id).filter((item) => item !== user_id);
            Socket.sendMessageToUsers(userIdsForMessages as string[], data.data[0] as Message)
        }

        res.send(
            data.data[0]
        )
    }
}
```

When a new conversation is created we notify the client of the connected users who are participants in that conversation with all the data they need to render that conversation. 

```jsx
Socket.notifyUsersOnConversationCreate(participant_ids as string[], conv)
```

And when a new message is added to a conversation, we also notify users who are in that conversation with the message data. 

```jsx
Socket.sendMessageToUsers(userIdsForMessages as string[], data.data[0] as Message)
```

And… that’s it! 

You just created your very first Supabase Node Chat backend from scratch. This code is way off being production-ready. 

But should give you a good idea or starting point to develop awesome chat applications of your own.

[You can find all the code here](https://github.com/nsmet/supabase-node-chat-backend)

# More articles in this series
* [Part 1 - Setting up WhatsApp UI in React Native](https://dev.to/jacksbridger/whatsapp-ui-in-react-native-part-1-4nbm)
* [Part 3 - Using real data in our app](https://dev.to/jacksbridger/whatsapp-in-react-native-part-3-1gmf)