https://github.com/serbanuntu/messenger
A lightweight instant messaging application built with Express.js
https://github.com/serbanuntu/messenger
express jwt postgresql react socket-io
Last synced: 2 months ago
JSON representation
A lightweight instant messaging application built with Express.js
- Host: GitHub
- URL: https://github.com/serbanuntu/messenger
- Owner: SerbanUntu
- Created: 2025-01-31T17:38:35.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2025-02-09T18:42:22.000Z (4 months ago)
- Last Synced: 2025-04-05T09:15:11.083Z (2 months ago)
- Topics: express, jwt, postgresql, react, socket-io
- Language: TypeScript
- Homepage: https://messenger-su.vercel.app
- Size: 2.55 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Messenger
A lightweight instant messaging application built with Express.js

## About
This chat application allows users to create conversations and send messages to each other in real time.
**Features**:
- One-to-one conversations
- Group conversations (3 or more people)
- Real-time updates when receiving messages or being added to conversations
- Optimistic UI updates to minimize waiting for server operations
- Website rendered as a SPA allowing navigation to different pages without a full browser refresh
- Fully responsive UI (works on both mobile and desktop)
- Account system## Technologies

### Frontend
- **React**: A framework for manipulating the DOM declaratively.
- **v0 by Vercel**: An AI tool used to build user interfaces from prompts. It outputs Next.js code by default. I used it to build the UI very quickly.
- **shadcn/ui**: A registry of unstyled reusable components. Used for more complex components such as toast notifications. Also used by v0.
- **TailwindCSS**: A CSS library that includes utility classes to allow styling of components direclty in the HTML.
- **Webpack**: A build tool used to bundle all the React code into a single minified, optimized and compressed plain Javascript file ready for production.### Backend
- **Node.js**: A runtime for Javascript on the server (note that `ts-node` is used to run the Typescript code which is not natively supported by Node).
- **Express.js**: A framework for handling server-side routing, browser requests, middleware and the responses sent back to the browser.
- **Socket.io**: A library for handling Websocket connections for realtime updates to the messages and conversations.
- **JSON Web Tokens**: A stateless way of managing user authentication with symmetric encryption.
- **node-postgres**: A library for sending SQL queries to the Postgres database from the Node server.### Deployment
- **Neon**: A Postgres serverless runtime built with Rust. I highly recommend their [Postgres tutorial](https://neon.tech/postgresql/tutorial).
- **Google Cloud Run**: A cloud service that manages and runs the server instance.
- **Docker**: A platform used to define a reproducible configuration for the server build, so it can be hosted anywhere.## What I Learned
### Authentication with JWT
For this project I rolled out my own authentication to avoid importing a heavy library and to learn more about how to keep sensitive information secure.
When the user logs in, the server will generate a new token using the `JWT_SECRET` environment variable, which acts as the encryption / decryption key.
The token stores the user id in its payload, so it can be used to fetch the user that made the request if needed.
```ts
// auth.ts
export const getCurrentUser = async (req: Request, res: Response) => {
const token = req.cookies.messenger_jwt
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { user_id: number };
const user = await getUserById(decoded.user_id)
res.status(200).json(user)
} catch (err) {
res.status(401).json({ error: (err as Error).message })
}
}
```### Compression
I fell into a bit of a compression rabbit hole while trying to get that 100 Performance Lighthouse score.
I learned about the 4 main types of compression used by browsers (gzip, deflate, brotli and zstandard) and decided to also serve a Brotli-encoded variant of my bundled JS.
```ts
// webpack.config.ts
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11
},
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
}),
```The compression middleware in the `compression.ts` file will look into the `Accept-Encoding` header from the request to see if the browser supports Brotli.
This simple change reduces the size of the JS file sent over the network from 510 KB to **120 KB**.
### Real-time Communication
On the server, I store the `Socket` objects for each connected user in a *hashmap*, so I can only target the necessary users that should be notified.
```ts
// websockets.ts
const sockets: Map = new Map()export const emitMessage = async (message: Message) => {
const targets = await getUsersInConversation(message.conversation_id, message.author_id) as { user_id: number }[]
targets.forEach(u => {
const socket = sockets.get(u.user_id)
if (socket) {
socket.emit('message', message)
console.log(`Message from user ${message.author_id} received by ${u.user_id}`)
}
})
}
```*Note: It might be possible to use channels and subscriptions to achieve a cleaner implementation*.
### Deployment
Since this application requires long-lived connections between the server and client for realtime updates, serverless providers such as Vercel weren't an option.
I initially tried to upload the Github repo directly to Google Cloud Run but I didn't succeed.
To simplify the configuration, I got my server up and running using Docker before uploading it to Cloud Run again.
## Further development
- Light theme
- Image sharing
- Message reactions
- Enhanced onboarding and usage instructions
- Message encryption