Skip to content

Repository files navigation

ISM - Instant Messenger for SaaS Backends

Version License Rust

ISM (Instant SaaS Messenger) is a high-performance, scalable messaging solution specifically designed for SaaS backends. Written in Rust, it leverages Tokio and Axum to provide an asynchronous, stable, and efficient infrastructure.

✨ If you have the same struggles as me to find an easy plug-and-play open source instant messaging solution, I would be happy if you contribute and use ISM! 🙏

I am using ISM as my social backend in my own app, here you see an example video with ISM in action:

ism_preview_video.mp4

Table of Contents

Key Features

  • Scalability: Built with the asynchronous Tokio runtime, ISM efficiently handles thousands of simultaneous connections.
  • OAUTH2 & OIDC: Supports JWT-based authentication via OpenID Connect (OIDC) Identity Providers (IDPs). (Currently tested only with Keycloak).
  • Easy Integration: Designed for seamless integration with existing SaaS architectures.
  • Real-time Notifications: Delivers messages in real-time using Server-Sent Events (SSE), typically achieving latency under 30ms.
  • Persistent Storage: Persists user and chat room metadata to a relational database, while storing room messages in a NoSQL database for optimal performance.
  • Custom Notifications: Send custom JSON payloads as notifications to your users via Kafka or Webhooks.
  • S3 Support: Upload room images and media content to S3-compatible object storage.
  • Event-Driven Chat Protocol: ISM utilizes a simple, event-driven chat protocol. Clients query for the latest data via standard HTTP requests and receive real-time updates through an SSE connection. All data is exchanged in JSON format.

Chat Functionalities

  • Room Types: Supports private rooms (two users) and group rooms (multiple users).
  • Message Types: Handles text, media, reply, and room change messages.
  • Room Management: Allows users to create rooms, invite others, and leave rooms.
  • Chat History: Provides support for scrolling through the entire chat timeline of a room.
  • Multi-Device Support: A single account can use an ISM chat client on multiple devices concurrently. Data is synchronized across devices thanks to the event-driven architecture.
  • Read Status Tracking: Tracks the read status for each user within a room, indicating which messages have been seen.
  • Friend System: Built-in friend request system with accept/reject functionality.
  • User Blocking: Block/unblock users to prevent unwanted interactions.

Supported Databases

  • Message Storage (NoSQL):
    • ScyllaDB: Store message data in your ScyllaDB Cluster.
    • Apache Cassandra: Store message data in your Apache Cassandra Cluster.
  • User/Room Metadata Storage (Relational):
    • PostgreSQL: Retrieve and manage user and room metadata.
  • Object Storage:
    • S3-Compatible Storage: MinIO, AWS S3, or any S3-compatible storage for media uploads.

Quick Start

Using Docker Compose

  1. Create a production.config.toml configuration file (see Configuration section below)

  2. Create a compose.yaml:

services:
  ism:
    image: ghcr.io/jrtimha/ism:latest
    container_name: ism-container
    ports:
      - "5403:5403"
    environment:
      ISM_MODE: production
    volumes:
      - ./production.config.toml:/app/production.config.toml
  1. Start the container:
docker compose up -d
  1. Verify ISM is running by visiting http://localhost:5403 - you should see:
Hello, world! I'm your new ISM. 🤗

Configuration

To configure the ISM container, you need to mount a configuration file named production.config.toml to the /app directory within the container.

These are the available configuration settings:

ism_url = "127.0.0.1" #Root URL, dont use localhost if you want to use IPv4 instead of IPv6
ism_port= 5403 #ISM is listening at this port
log_level = "info" # Logging level (e.g., "info", "debug", "warn", "error")
cors_origin = "http://localhost:4200" # Allowed CORS origin for frontend applications (wildcards are not supported)
use_kafka = false # Set to true to enable Kafka integration for custom notifications

[message_db_config] # Configuration for the NoSQL object_storage (Cassandra/ScyllaDB)
db_url = "localhost:9042"
db_user = "cassandra"
db_password = "cassandra"
db_keyspace = "messaging"
with_db_init = true # Set to true to initialize required object_storage tables on startup (use with caution in production)

[user_db_config] # Configuration for the relational object_storage (PostgreSQL)
db_host = "localhost"
db_port = "32768"
db_user = "postgres"
db_password = "postgres"
db_name = "postgres"

[token_issuer]  # OIDC Identity Provider configuration
iss_host = "http://localhost:8180/" #Keycloak Root URL
iss_realm = "my-realm" #Keycloak Realm

[object_db_config] #connection to the S3 Bucket, used for images
db_user = "minioadmin"
db_url = "http://localhost:9000"
db_password = "minioadmin"
bucket_name = "meventure"


[kafka_config]  #OPTIONAL: Kafka configuration (only used if use_kafka = true)
bootstrap_host = "localhost"
bootstrap_port = 19192
topic = "user-notification-events"
client_id = "ism-1"
partition = [0]
consumer_group = "ism"

API Documentation

All API endpoints require authentication via JWT Bearer token in the Authorization header, except for the root endpoint and health check.

Authentication

All protected endpoints require a valid JWT token from your OIDC provider (Keycloak):

Authorization: Bearer <your_jwt_token>

Public Endpoints

Health Check

  • GET /health
    • Returns server health status
    • Response: 200 OK with "Healthy"

Root

  • GET /
    • Returns welcome message
    • Response: "Hello, world! I'm your new ISM. 🤗"

Real-Time Communication

Server-Sent Events (SSE)

  • GET /api/sse
    • Establishes a Server-Sent Events connection for real-time updates
    • Client receives push notifications for new messages, room updates, and custom notifications
    • Connection stays open with keep-alive every 5 seconds
    • Response: Stream of SSE events containing JSON data

Get Notifications

  • GET /api/notifications
    • Replays durable notification events since a given per-user sequence number
    • Query Parameters:
      • last_seq (number, required): Retrieve events with seq > last_seq (use 0 for everything still retained)
    • Response: 200 OK with array of notification objects; a single Resync element if the gap is no longer retained

Get Notification Cursor

  • GET /api/notifications/cursor
    • Returns the highest sequence currently issued to the caller without advancing it
    • Used to seed the stored cursor after a full REST sync (which connects to the stream without last_seq)
    • Response: 200 OK with { "seq": <number> } (0 if no event issued yet)

Messaging

Send Message

  • POST /api/send-msg
    • Sends a message to a chat room
    • Request Body:
      {
        "chatRoomId": "uuid",
        "msgType": "Text|Media|Reply",
        "msgBody": {
          // For Text messages:
          "text": "string (1-4000 chars)",
      
          // For Media messages:
          "mediaUrl": "string (1-250 chars)",
          "mediaType": "string (1-80 chars)",
          "mimeType": "string (optional)",
          "altText": "string (optional)",
      
          // For Reply messages:
          "replyMsgId": "uuid",
          "replyCreatedAt": "datetime",
          "replyText": "string (1-4000 chars)"
        }
      }
    • Response: 200 OK with created message object

Room Management

Create Room

  • POST /api/rooms/create-room
    • Creates a new chat room (Single or Group)
    • Request Body:
      {
        "roomType": "Single|Group",
        "roomName": "string (optional, required for groups)",
        "invitedUsers": ["uuid", "uuid", ...]
      }
    • Validation:
      • Single rooms: exactly 2 users (sender + one other)
      • Group rooms: minimum 2 users
      • Sender must be in invitedUsers list
      • Blocked users are automatically filtered out
    • Response: 200 OK with created room object

Get Joined Rooms

  • GET /api/rooms
    • Retrieves all rooms the authenticated user is a member of
    • Response: 200 OK with array of room objects

Get Room Details

  • GET /api/rooms/{room_id}
    • Gets basic room information for display in room lists
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Response: 200 OK with room object

Get Room with Full Details

  • GET /api/rooms/{room_id}/detailed
    • Gets comprehensive room information including all members
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Response: 200 OK with detailed room object including users array

Get Room Users

  • GET /api/rooms/{room_id}/users
    • Lists all members of a specific room
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Response: 200 OK with array of user objects

Search for Existing Single Room

  • GET /api/rooms/search
    • Checks if a single (private) room already exists with another user
    • Query Parameters:
      • withUser (UUID): The other user's ID
    • Response: 200 OK with room UUID or null

Leave Room

  • POST /api/rooms/{room_id}/leave
    • Removes the authenticated user from a room
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Response: 200 OK

Invite User to Room

  • POST /api/rooms/{room_id}/invite/{user_id}
    • Invites a user to join a room
    • Path Parameters:
      • room_id (UUID): Room identifier
      • user_id (UUID): User to invite
    • Response: 200 OK
    • Error: 403 Blocked if user is blocked

Upload Room Image

  • POST /api/rooms/{room_id}/upload-img
    • Uploads a room image/avatar to S3 storage
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Request: multipart/form-data with image field
    • Max Size: 5MB
    • Response: 200 OK with upload response containing URL

Timeline & Messages

Get Room Timeline

  • GET /api/rooms/{room_id}/timeline
    • Retrieves message history for a room with pagination
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Query Parameters:
      • timestamp (DateTime): Load messages before this timestamp
    • Response: 200 OK with a TimelinePage object: { messages: [...], senders: [...] }, where senders is the deduplicated set of room members that authored a message in the page (plus the original authors referenced by replies; authors who have since left still resolve, with null participant fields). Combined with the sender field on live ChatMessage events, the client never needs a separate sender lookup.

Mark Room as Read

  • POST /api/rooms/{room_id}/mark-read
    • Marks all messages in a room as read for the authenticated user
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Response: 200 OK

Get Read States

  • GET /api/rooms/{room_id}/read-states
    • Retrieves read status for all members in a room
    • Path Parameters:
      • room_id (UUID): Room identifier
    • Response: 200 OK with array of room member objects including read timestamps

User Management

Search User by ID

  • GET /api/users/{user_id}
    • Retrieves user profile and relationship status with authenticated user
    • Path Parameters:
      • user_id (UUID): User identifier
    • Response: 200 OK with user object and relationship type

Search Users by Name

  • GET /api/users/search
    • Searches for users by display name with pagination
    • Query Parameters:
      • username (string): Search query
      • cursor (string, optional): Pagination cursor for next page
    • Response: 200 OK with results and next cursor

Friend System

Get Friends

  • GET /api/users/friends
    • Retrieves the authenticated user's friend list
    • Response: 200 OK with array of user objects

Get Friend Requests

  • GET /api/users/friends/requests
    • Retrieves pending friend requests received by the authenticated user
    • Response: 200 OK with array of user objects

Send Friend Request

  • POST /api/users/friends/add/{user_id}
    • Sends a friend request to another user
    • Path Parameters:
      • user_id (UUID): User to send request to
    • Response: 200 OK

Accept Friend Request

  • POST /api/users/friends/accept-request/{sender_id}
    • Accepts a pending friend request
    • Path Parameters:
      • sender_id (UUID): User who sent the request
    • Response: 200 OK

Reject Friend Request

  • DELETE /api/users/friends/reject-request/{sender_id}
    • Rejects a pending friend request
    • Path Parameters:
      • sender_id (UUID): User who sent the request
    • Response: 200 OK

Remove Friend

  • DELETE /api/users/friends/{friend_id}
    • Removes a user from friend list
    • Path Parameters:
      • friend_id (UUID): Friend to remove
    • Response: 200 OK

User Blocking

Block User

  • POST /api/users/ignore/{user_id}
    • Blocks a user and automatically leaves any private room with them
    • Path Parameters:
      • user_id (UUID): User to block
    • Response: 200 OK with updated relationship state

Unblock User

  • DELETE /api/users/ignore/{user_id}
    • Unblocks a previously blocked user
    • Path Parameters:
      • user_id (UUID): User to unblock
    • Response: 200 OK with updated relationship state

Data Models

Message Types

  • Text: Simple text message (1-4000 characters)
  • Media: Link to media content (images, videos, etc.)
  • Reply: Reply to another message
  • RoomChange: System messages for user joined/left/invited events

Room Types

  • Single: Private room between two users
  • Group: Group room with multiple users

Relationship States

  • FRIEND: Users are friends
  • INVITE_SENT: Friend request sent by authenticated user
  • INVITE_RECEIVED: Friend request received from other user
  • CLIENT_BLOCKED: Authenticated user blocked the other user
  • CLIENT_GOT_BLOCKED: Authenticated user was blocked by the other user

Usage Examples

Connecting to SSE Stream

const token = "your_jwt_token";
const eventSource = new EventSource(`http://localhost:5403/api/sse`, {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

eventSource.onmessage = (event) => {
  const notification = JSON.parse(event.data);
  console.log('Received:', notification);
  // Handle new messages, room updates, etc.
};

Creating a Room

curl -X POST http://localhost:5403/api/rooms/create-room \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "roomType": "Single",
    "invitedUsers": ["user-uuid-1", "user-uuid-2"]
  }'

Sending a Text Message

curl -X POST http://localhost:5403/api/send-msg \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "chatRoomId": "room-uuid",
    "msgType": "Text",
    "msgBody": {
      "text": "Hello, World!"
    }
  }'

Fetching Chat Timeline

curl -X GET "http://localhost:5403/api/rooms/{room_id}/timeline?timestamp=2024-01-01T00:00:00Z" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Searching for Users

curl -X GET "http://localhost:5403/api/users/search?username=john" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Development

Prerequisites

  • Rust 2024 edition or later
  • PostgreSQL database
  • ScyllaDB or Apache Cassandra cluster
  • Keycloak or compatible OIDC provider
  • (Optional) S3-compatible object storage
  • (Optional) Kafka cluster

Building from Source

  1. Clone the repository:
git clone https://github.com/JrTimha/ism.git
cd ism
  1. Set up environment variables (create a .env file):
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
  1. Run database migrations:
cargo install sqlx-cli
sqlx migrate run
  1. Build the project:
cargo build --release
  1. Run the application:
cargo run --release

Database Migrations

This project uses sqlx-cli for database schema management:

# Apply pending migrations
sqlx migrate run

# Revert last migration
sqlx migrate revert

# Create a new migration
sqlx migrate add <migration_name>

Working with SQLx

If you modify any SQL queries in the code, you must prepare the offline query metadata:

cargo sqlx prepare

This generates compile-time checked query metadata in .sqlx/ directory, ensuring type safety for your SQL queries.

Project Structure

src/
├── broadcast/        # SSE and notification broadcasting
├── cache/           # Redis caching and pub/sub
├── core/            # Core application state and config
├── database/        # Database connections (Cassandra, PostgreSQL, S3)
├── errors.rs        # Error handling
├── kafka/           # Kafka integration for notifications
├── keycloak/        # JWT authentication and OIDC
├── messaging/       # Message handling and routes
├── model/           # Data models and DTOs
├── repository/      # Database repository layer
├── rooms/           # Room management and timeline
├── router.rs        # API route definitions
├── user_relationship/ # Friend system and user blocking
└── main.rs          # Application entry point

Running Tests

cargo test

Docker Build

Build your own Docker image:

docker build -t ism:latest .

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0) - see the LICENSE file for details.


Support

If you encounter any issues or have questions:

  • Open an issue on GitHub Issues
  • Check existing issues for solutions
  • Contribute to the project!

Roadmap

  • JWT/OIDC Authentication
  • Real-time messaging via SSE
  • Friend system
  • User blocking
  • S3 image uploads
  • End-to-end encryption
  • Voice/Video call signaling
  • Message reactions
  • Typing indicators
  • Message search functionality
  • Admin dashboard

Built with ❤️ using Rust, Tokio, and Axum

About

ISM, the [i]n[s]tant [m]essaging microservice for SaaS backends, built with rust

Topics

Resources

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages