Skip to content

nihalsheikh/realtime-chat

Repository files navigation

Realtime Chat

A private, self-destructing realtime chat application built with Next.js, TypeScript, and Upstash.

Banner


Overview

Realtime Chat is a secure, ephemeral messaging platform where conversations automatically self-destruct after 10 minutes. Built for privacy-focused communication with realtime synchronization and optimistic UI updates.

Key Features

  • Self-Destructing Rooms: Chat rooms automatically expire after 10 minutes
  • Realtime Messaging: Instant message delivery via Upstash Realtime pub/sub
  • Secure Access: Token-based authentication limiting rooms to 2 participants
  • Optimistic UI: Instant feedback with TanStack React Query
  • Persistent History: Messages stored in Redis during room lifetime
  • Anonymous Identity: Auto-generated usernames on first visit

Tech Stack

Category Technology
Framework Next.js 16 (App Router)
Language TypeScript
Styling Tailwind CSS 4
State Management TanStack React Query
Backend Elysia (embedded in Next.js API routes)
Database Upstash Redis
Realtime Upstash Realtime
Validation Zod v4
Runtime Bun
Utilities nanoid, date-fns

Project Structure

realtime-chat/
├── public/                          # Static assets
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   └── [[...slugs]]/
│   │   │       ├── auth.ts          # Elysia auth middleware
│   │   │       └── route.ts         # Elysia server (rooms & messages API)
│   │   ├── realtime/
│   │   │   └── route.ts             # Upstash Realtime endpoint
│   │   ├── room/
│   │   │   └── [roomId]/
│   │   │       └── page.tsx         # Chat room page
│   │   ├── globals.css              # Global styles
│   │   ├── layout.tsx               # Root layout with providers
│   │   └── page.tsx                 # Home/lobby page
│   ├── components/
│   │   └── providers.tsx            # React Query & Realtime providers
│   ├── constants/
│   │   └── index.ts                 # App constants (animals, storage keys)
│   ├── hooks/
│   │   └── use-username.ts          # Username management hook
│   ├── lib/
│   │   ├── client.ts                # Elysia/Eden API client
│   │   ├── index.ts                 # Utility functions
│   │   ├── realtime.ts              # Upstash Realtime schema & types
│   │   ├── realtime-client.ts       # Realtime hook exporter
│   │   └── redis.ts                 # Upstash Redis client
│   └── proxy.ts                     # Next.js middleware for auth
├── .env                             # Environment variables
├── package.json
└── README.md

Database Architecture

Upstash Redis Data Model

The application uses Redis hashes and lists for data storage. All keys have automatic expiration (TTL) to enforce self-destruction.

Key Patterns

Key Pattern Type Description TTL
meta:{roomId} Hash Room metadata (connected users, creation time) 600s
messages:{roomId} List Message history for the room Synced with room TTL
history:{roomId} List Additional history storage Synced with room TTL
{roomId} - Reserved for realtime channel Synced with room TTL

Room Metadata Schema (meta:{roomId})

{
  connected: string[];  // Array of auth tokens (max 2 users)
  createdAt: number;    // Unix timestamp in milliseconds
}

Message Schema (stored in messages:{roomId})

{
  id: string;        // nanoid-generated
  sender: string;    // Username of message sender
  text: string;      // Message content (max 1000 chars)
  timestamp: number; // Unix timestamp in milliseconds
  roomId: string;    // Reference to chat room
  token?: string;    // Auth token (only visible to sender)
}

Redis Client Configuration

// src/lib/redis.ts
import { Redis } from "@upstash/redis";

export const redis = Redis.fromEnv();

The client automatically reads from environment variables:

  • UPSTASH_REDIS_REST_URL
  • UPSTASH_REDIS_REST_TOKEN

TTL Management

When a room is created or messages are sent, TTL is synchronized across all related keys:

// Room creation (src/app/api/[[...slugs]]/route.ts)
const ROOM_TTL_SECONDS = 60 * 10; // 10 minutes

await redis.expire(`meta:${roomId}`, ROOM_TTL_SECONDS);

// After sending a message, sync TTL to message keys
const remaining = await redis.ttl(`meta:${roomId}`);
await Promise.all([
  redis.expire(`messages:${roomId}`, remaining),
  redis.expire(`history:${roomId}`, remaining),
  redis.expire(roomId, remaining),
]);

Middleware & Authentication

Proxy Middleware (src/proxy.ts)

The middleware handles room access control and user authentication:

// src/proxy.ts
export const proxy = async (req: NextRequest) => {
  // 1. Validate room path
  // 2. Check if room exists in Redis
  // 3. Validate existing token or issue new one
  // 4. Enforce 2-user limit
  // 5. Update Redis connected users list
};

export const config = {
  matcher: "/room/:path*",
};

Authentication Flow

  1. Cookie Token: Users receive an x-auth-token cookie on first room access
  2. Token Validation: Middleware checks if token exists in room's connected array
  3. Room Capacity: Maximum 2 users per room enforced at middleware level
  4. Token Persistence: Cookies are HTTP-only and secure in production

Error States

Error Code Trigger Redirect
room-not-found Room doesn't exist or expired /?error=room-not-found
room-full Room has 2 users already /?error=room-full

API Reference

Base URL

All API routes are prefixed with /api and handled by Elysia.

Endpoints

Create Room

POST /api/room/create

Response:

{ "roomId": "abc123" }

Side Effects:

  • Creates meta:{roomId} hash in Redis
  • Sets 10-minute TTL

Get Room TTL

GET /api/room/ttl?roomId={roomId}

Query Parameters:

Parameter Type Required
roomId string Yes

Response:

{ "ttl": 480 }

Delete Room

DELETE /api/room/?roomId={roomId}

Query Parameters:

Parameter Type Required
roomId string Yes

Side Effects:

  • Emits chat.destroy event via Upstash Realtime
  • Deletes meta:{roomId}, messages:{roomId}, history:{roomId}

Send Message

POST /api/messages/?roomId={roomId}

Query Parameters:

Parameter Type Required
roomId string Yes

Body:

{
  "sender": "anonymous-wolf-x7k9m",
  "text": "Hello!"
}

Validation:

  • sender: max 100 characters
  • text: max 1000 characters

Side Effects:

  • Stores message in messages:{roomId} list
  • Emits chat.message event via Upstash Realtime
  • Synchronizes TTL across all room keys

Get Messages

GET /api/messages/?roomId={roomId}

Query Parameters:

Parameter Type Required
roomId string Yes

Response:

{
  "messages": [
    {
      "id": "msg123",
      "sender": "anonymous-wolf-x7k9m",
      "text": "Hello!",
      "timestamp": 1744567890123,
      "roomId": "abc123",
      "token": "tok_abc" // Only present if sent by current user
    }
  ]
}

Realtime Events

Schema Definition (src/lib/realtime.ts)

const schema = {
  chat: {
    message: z.object({
      id: z.string(),
      sender: z.string(),
      text: z.string(),
      timestamp: z.number(),
      roomId: z.string(),
      token: z.string().optional(),
    }),
    destroy: z.object({
      isDestoyed: z.literal(true),
    }),
  },
};

Event Types

Event Channel Payload Description
chat.message {roomId} Message New message received
chat.destroy {roomId} { isDestoyed: true } Room self-destructed

Using Realtime in Components

// src/app/room/[roomId]/page.tsx
useRealtime({
  channels: [roomId],
  events: ["chat.message", "chat.destroy"],
  onData: ({ event }) => {
    if (event === "chat.message") {
      refetch(); // Refresh messages from cache
    }
    if (event === "chat.destroy") {
      router.push("/?destroyed=true");
    }
  },
});

React Hooks

useUsername (src/hooks/use-username.ts)

Manages anonymous username generation and persistence.

Behavior:

  1. Checks localStorage for existing username
  2. If not found, generates new username using pattern: anonymous-{animal}-{nanoid(5)}
  3. Persists username to localStorage for future sessions

Usage:

const { username } = useUsername();

Return Value:

{
  username: string; // e.g., "anonymous-wolf-x7k9m"
}

Storage Key: chat_username (defined in src/constants/index.ts)


Environment Variables

Create a .env file in the project root:

# Upstash Redis (REST API)
UPSTASH_REDIS_REST_URL="https://your-app.upstash.io"
UPSTASH_REDIS_REST_TOKEN="your-token"

# Upstash Realtime (optional - defaults from REST)
UPSTASH_REALTIME_URL="wss://your-app.upstash.io"
UPSTASH_REALTIME_TOKEN="your-realtime-token"

Getting Upstash Credentials

  1. Create a database at upstash.com
  2. Copy the REST URL and token from the database dashboard
  3. For Realtime, enable the Realtime feature in your database settings

Getting Started

Prerequisites

  • Bun (recommended) or Node.js 18+
  • Upstash Redis database with Realtime enabled

Installation

# Clone the repository
git clone <repository-url>
cd realtime-chat

# Install dependencies
bun install

# Set up environment variables
cp .env.example .env
# Edit .env with your Upstash credentials

Development

# Start development server
bun dev

# Open http://localhost:3000

Production Build

# Build for production
bun build

# Start production server
bun start

Linting

bun lint

Room Lifecycle

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Room      │────▶│   Active    │────▶│  Destroyed  │
│   Created   │     │   Chatting  │     │  (Auto/Man) │
└─────────────┘     └─────────────┘     └─────────────┘
      │                   │                   │
      │                   │                   │
      ▼                   ▼                   ▼
  meta:{id}          messages:{id}        All keys
  connected: []      TTL synced         deleted
  TTL: 600s

Automatic Destruction

  • Triggered when TTL expires (10 minutes from creation)
  • Redis automatically removes expired keys
  • Realtime emits chat.destroy to connected clients

Manual Destruction

  • User clicks "DESTROY NOW" button
  • Emits chat.destroy event immediately
  • Deletes all room-related keys
  • Redirects all users to lobby with destroyed=true

Security Considerations

  1. Token-Based Auth: HTTP-only cookies prevent XSS token theft
  2. Room Isolation: Each room has unique ID, no cross-room data access
  3. Capacity Limits: 2-user maximum prevents room overcrowding
  4. Automatic Expiry: All data self-destructs, no permanent storage
  5. Secure Cookies: Secure flag enabled in production

License

MIT

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors