Update README.md
Browse files
README.md
CHANGED
|
@@ -1,194 +1,11 @@
|
|
| 1 |
-
# P2P Signal Server
|
| 2 |
-
|
| 3 |
-
Real-time WebRTC signaling server for 10,000 concurrent users.
|
| 4 |
-
Runs on **Hugging Face Spaces Free Tier** (Docker, port 7860).
|
| 5 |
-
|
| 6 |
-
---
|
| 7 |
-
|
| 8 |
-
## Architecture
|
| 9 |
-
|
| 10 |
-
```
|
| 11 |
-
Client A ──WS──► HF Space (Signal Server)
|
| 12 |
-
│
|
| 13 |
-
├─ Redis Bucket 1 (Presence) uid→socketId, online/offline
|
| 14 |
-
├─ Redis Bucket 2 (Msg Queue) offline_msgs:{uid} list
|
| 15 |
-
├─ MongoDB Atlas user profiles, friend lists
|
| 16 |
-
└─ Supabase Storage media files (server sees 0 bytes)
|
| 17 |
-
|
| 18 |
-
Client A ◄──────────────────────────────► Client B (WebRTC P2P after handshake)
|
| 19 |
-
◄── WS relay fallback if ICE fails ─────────►
|
| 20 |
-
```
|
| 21 |
-
|
| 22 |
-
---
|
| 23 |
-
|
| 24 |
-
## Quick Start
|
| 25 |
-
|
| 26 |
-
### 1. Clone & install
|
| 27 |
-
|
| 28 |
-
```bash
|
| 29 |
-
git clone https://github.com/you/p2p-signal-server
|
| 30 |
-
cd p2p-signal-server
|
| 31 |
-
npm install
|
| 32 |
-
cp .env.example .env
|
| 33 |
-
# Fill in all values in .env
|
| 34 |
-
npm run dev
|
| 35 |
-
```
|
| 36 |
-
|
| 37 |
-
### 2. Build & run (Docker)
|
| 38 |
-
|
| 39 |
-
```bash
|
| 40 |
-
docker build -t p2p-signal .
|
| 41 |
-
docker run --env-file .env -p 7860:7860 p2p-signal
|
| 42 |
-
```
|
| 43 |
-
|
| 44 |
---
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
| `JWT_SECRET` | `node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"` |
|
| 53 |
-
| `REDIS_PRESENCE_URL` | [Upstash](https://upstash.com) → Create Redis DB #1 → Copy Redis URL |
|
| 54 |
-
| `REDIS_MESSAGE_URL` | Upstash → Create Redis DB **#2** (separate!) → Copy Redis URL |
|
| 55 |
-
| `MONGO_URI` | [MongoDB Atlas](https://cloud.mongodb.com) → M0 Free → Connect → Driver URL |
|
| 56 |
-
| `SUPABASE_URL` | [Supabase](https://supabase.com) → Project Settings → API → Project URL |
|
| 57 |
-
| `SUPABASE_SERVICE_KEY` | Supabase → Project Settings → API → `service_role` key |
|
| 58 |
-
| `SUPABASE_BUCKET` | Create a bucket named `media` in Supabase Storage, set to **public** |
|
| 59 |
-
| `TURN_URL` | [metered.ca/tools/openrelay](https://www.metered.ca/tools/openrelay/) free TURN |
|
| 60 |
-
| `TURN_USERNAME` | From the same TURN provider |
|
| 61 |
-
| `TURN_CREDENTIAL` | From the same TURN provider |
|
| 62 |
-
| `ALLOWED_ORIGINS` | Your frontend domain(s), comma-separated e.g. `https://myapp.vercel.app` |
|
| 63 |
-
|
| 64 |
-
Optional (have defaults):
|
| 65 |
-
|
| 66 |
-
| Variable | Default | Notes |
|
| 67 |
-
|---|---|---|
|
| 68 |
-
| `PORT` | `7860` | Must stay 7860 for HF Spaces |
|
| 69 |
-
| `STUN_SERVERS` | Google STUN | Comma-separated stun: URIs |
|
| 70 |
-
| `PRESENCE_TTL_S` | `60` | Redis TTL for online status |
|
| 71 |
-
| `OFFLINE_MSG_TTL_S` | `604800` | 7 days — how long to hold queued msgs |
|
| 72 |
-
| `HEARTBEAT_INTERVAL_MS` | `30000` | Client sends ping every 30s |
|
| 73 |
-
| `MAX_FILE_SIZE_MB` | `99` | Max single file size |
|
| 74 |
-
| `MAX_FILE_COUNT` | `10` | Max files per upload request |
|
| 75 |
-
|
| 76 |
---
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
1. Create a new Space → **Docker** SDK
|
| 81 |
-
2. Push this repo to the Space's git remote
|
| 82 |
-
3. Add all secrets in Space settings
|
| 83 |
-
4. The Space will auto-build via the Dockerfile
|
| 84 |
-
|
| 85 |
-
Health check: `GET /health` → `{ status: 'ok', connections: N, ts: epoch }`
|
| 86 |
-
|
| 87 |
-
---
|
| 88 |
-
|
| 89 |
-
## WebSocket Protocol
|
| 90 |
-
|
| 91 |
-
Connect: `wss://your-space.hf.space?token=<JWT>`
|
| 92 |
-
|
| 93 |
-
### Client → Server
|
| 94 |
-
|
| 95 |
-
```jsonc
|
| 96 |
-
// Heartbeat (every 30s)
|
| 97 |
-
{ "type": "heartbeat", "payload": {} }
|
| 98 |
-
|
| 99 |
-
// Send a text message
|
| 100 |
-
{ "type": "message", "payload": { "toUid": "bob", "type": "text", "content": "Hello" } }
|
| 101 |
-
|
| 102 |
-
// Send a media message (after uploading to Supabase)
|
| 103 |
-
{ "type": "message", "payload": { "toUid": "bob", "type": "media", "content": "https://...", "mediaType": "image/jpeg" } }
|
| 104 |
-
|
| 105 |
-
// Request signed upload URLs
|
| 106 |
-
{ "type": "get-upload-url", "requestId": "abc123",
|
| 107 |
-
"payload": { "files": [{ "fileName": "photo.jpg", "mimeType": "image/jpeg", "sizeBytes": 1048576 }] } }
|
| 108 |
-
|
| 109 |
-
// WebRTC offer
|
| 110 |
-
{ "type": "offer", "payload": { "toUid": "bob", "sdp": { ...RTCSessionDescription } } }
|
| 111 |
-
|
| 112 |
-
// WebRTC answer
|
| 113 |
-
{ "type": "answer", "payload": { "toUid": "alice", "sdp": { ...RTCSessionDescription } } }
|
| 114 |
-
|
| 115 |
-
// ICE candidate
|
| 116 |
-
{ "type": "ice-candidate", "payload": { "toUid": "bob", "candidate": { ...RTCIceCandidate } } }
|
| 117 |
-
|
| 118 |
-
// WS relay fallback (when ICE fails)
|
| 119 |
-
{ "type": "relay", "payload": { "toUid": "bob", "data": { ...anything } } }
|
| 120 |
-
|
| 121 |
-
// Friend actions
|
| 122 |
-
{ "type": "friend", "payload": { "action": "send-request", "targetUid": "bob" } }
|
| 123 |
-
{ "type": "friend", "payload": { "action": "accept", "targetUid": "alice" } }
|
| 124 |
-
{ "type": "friend", "payload": { "action": "reject", "targetUid": "alice" } }
|
| 125 |
-
{ "type": "friend", "payload": { "action": "remove", "targetUid": "bob" } }
|
| 126 |
-
{ "type": "friend", "payload": { "action": "list" } }
|
| 127 |
-
{ "type": "friend", "payload": { "action": "status", "targetUid": "bob" } }
|
| 128 |
-
```
|
| 129 |
-
|
| 130 |
-
### Server → Client
|
| 131 |
-
|
| 132 |
-
```jsonc
|
| 133 |
-
{ "type": "connected", "payload": { "uid": "...", "socketId": "..." } }
|
| 134 |
-
{ "type": "ice-servers", "payload": { "iceServers": [...] } }
|
| 135 |
-
{ "type": "offline-flush", "payload": { "messages": [...], "count": N } }
|
| 136 |
-
{ "type": "message", "payload": { "fromUid": "...", "content": "...", ... } }
|
| 137 |
-
{ "type": "peer-online", "payload": { "uid": "bob" } }
|
| 138 |
-
{ "type": "peer-offline", "payload": { "uid": "bob" } }
|
| 139 |
-
{ "type": "upload-url", "payload": { "urls": [{ "signedUrl", "objectPath", "publicUrl" }] } }
|
| 140 |
-
{ "type": "offer", "payload": { "fromUid": "...", "sdp": {...} } }
|
| 141 |
-
{ "type": "answer", "payload": { "fromUid": "...", "sdp": {...} } }
|
| 142 |
-
{ "type": "ice-candidate", "payload": { "fromUid": "...", "candidate": {...} } }
|
| 143 |
-
{ "type": "relay", "payload": { "fromUid": "...", "data": {...} } }
|
| 144 |
-
{ "type": "error", "payload": { "message": "..." } }
|
| 145 |
-
```
|
| 146 |
-
|
| 147 |
-
---
|
| 148 |
-
|
| 149 |
-
## File Upload Flow
|
| 150 |
-
|
| 151 |
-
```
|
| 152 |
-
1. Client → server: { type: 'get-upload-url', payload: { files: [...] } }
|
| 153 |
-
2. Server → Supabase: createSignedUploadUrl (no bytes transferred)
|
| 154 |
-
3. Server → client: { type: 'upload-url', payload: { urls: [...] } }
|
| 155 |
-
4. Client → Supabase: PUT file directly (server sees 0 bytes, HF RAM unaffected)
|
| 156 |
-
5. Client → server: { type: 'message', payload: { type: 'media', content: publicUrl } }
|
| 157 |
-
6. Server → recipient: deliver or queue offline
|
| 158 |
-
```
|
| 159 |
-
|
| 160 |
-
---
|
| 161 |
-
|
| 162 |
-
## File Structure
|
| 163 |
-
|
| 164 |
-
```
|
| 165 |
-
src/
|
| 166 |
-
├── index.ts # Entry point, HTTP+WS server
|
| 167 |
-
├── config.ts # All env vars, ICE server builder
|
| 168 |
-
├── db/
|
| 169 |
-
│ └── mongo.ts # MongoDB connection, User + Message models
|
| 170 |
-
├── redis/
|
| 171 |
-
│ ├── presenceClient.ts # Bucket 1: uid→socketId, online/offline
|
| 172 |
-
│ └── messageClient.ts # Bucket 2: offline message queue
|
| 173 |
-
├── services/
|
| 174 |
-
│ ├── connectionRegistry.ts # In-process socket map + zombie pruner
|
| 175 |
-
│ ├── supabaseService.ts # Signed URL generation, file validation
|
| 176 |
-
│ └── friendService.ts # Friend request/accept/remove logic
|
| 177 |
-
├── handlers/
|
| 178 |
-
│ ├── connectionHandler.ts # WS connect/disconnect, message router
|
| 179 |
-
│ ├── signalingHandler.ts # WebRTC SDP/ICE forwarding
|
| 180 |
-
│ ├── messageHandler.ts # Text/media delivery + offline queue
|
| 181 |
-
│ ├── fileHandler.ts # Upload URL generation
|
| 182 |
-
│ └── friendHandler.ts # Friend CRUD over WS
|
| 183 |
-
├── middleware/
|
| 184 |
-
│ └── rateLimiter.ts # Token bucket, 20 conn/IP/60s
|
| 185 |
-
├── models/
|
| 186 |
-
│ └── friend.ts # FriendRequest mongoose model
|
| 187 |
-
└── utils/
|
| 188 |
-
├── jwt.ts # Token verification (uid always from JWT)
|
| 189 |
-
├── logger.ts # Structured JSON logger
|
| 190 |
-
└── protocol.ts # WS message types + safe serialization
|
| 191 |
-
|
| 192 |
-
client-reference/
|
| 193 |
-
└── client.ts # Browser client (copy into your frontend)
|
| 194 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Chatapi
|
| 3 |
+
emoji: 🏆
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
short_description: A simple chatting api
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|