A production-quality, open-source engagement backend for live streaming platforms. TipCurrent handles tips, reactions, and other engagement events that drive interaction and monetization in gaming, live events, webinars, and creator content.
TipCurrent provides a reliable, self-hosted solution for ingesting and broadcasting engagement events from live streaming platforms. It supports multiple event types with real-time WebSocket broadcasting, webhook notifications, and pre-aggregated analytics.
- Tips API - Create and query tip events with sender, recipient, and amount
- Reactions API - Create and query emoji reactions to streams/content
- Real-time WebSocket Broadcasting - Instant event delivery to subscribers
- Webhooks - Event notifications to external systems with HMAC signature verification and retries
- Idempotency - Safe retries via
Idempotency-Keyheader - Analytics API - Pre-aggregated statistics with OLTP/OLAP separation
- Scheduled Aggregation - Hourly stats computation for tips and reactions
- PostgreSQL persistence with proper indexing
- Docker Compose for easy local development
- 61 integration tests with Testcontainers
- Built with Spring Boot 4.0.1 and Java 25
- Java 25 or higher
- Maven 3.9+
- Docker and Docker Compose
- curl or similar HTTP client (for testing)
docker-compose up -dThis starts a PostgreSQL 17 container with the database pre-configured.
On macOS/Linux:
./mvnw clean packageOn Windows:
mvnw.cmd clean packageOn macOS/Linux:
./mvnw spring-boot:runOn Windows:
mvnw.cmd spring-boot:runThe service will start on http://localhost:8080.
curl -X POST http://localhost:8080/api/tips \
-H "Content-Type: application/json" \
-d '{
"roomId": "gaming_stream_123",
"senderId": "alice",
"recipientId": "bob",
"amount": 100.00,
"message": "Great play!",
"metadata": "{\"type\":\"celebration\"}"
}'curl -X POST http://localhost:8080/api/reactions \
-H "Content-Type: application/json" \
-d '{
"roomId": "gaming_stream_123",
"userId": "alice",
"emoji": "🔥",
"targetId": "msg_123"
}'Endpoint: POST /api/tips
Headers:
| Header | Required | Description |
|---|---|---|
| Idempotency-Key | No | UUID for safe retries (24-hour expiration) |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| roomId | string | Yes | Identifier for the room/stream |
| senderId | string | Yes | User sending the tip |
| recipientId | string | Yes | User receiving the tip |
| amount | decimal | Yes | Tip amount (e.g., tokens, currency units) |
| message | string | No | Optional message (max 1000 chars) |
| metadata | string | No | Optional JSON metadata |
Response: HTTP 201 Created (or 200 OK if idempotency key matched existing tip)
{
"id": 1,
"roomId": "gaming_stream_123",
"senderId": "alice",
"recipientId": "bob",
"amount": 100.00,
"message": "Great play!",
"metadata": "{\"type\":\"celebration\"}",
"createdAt": "2024-01-15T10:30:45.123Z"
}Endpoint: GET /api/tips
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| roomId | string | No | Filter by room ID |
| senderId | string | No | Filter by sender ID |
| recipientId | string | No | Filter by recipient ID |
| page | integer | No | Page number (default: 0) |
| size | integer | No | Page size (default: 20) |
Examples:
# Get all tips
curl http://localhost:8080/api/tips
# Get tips for a room
curl http://localhost:8080/api/tips?roomId=gaming_stream_123
# Get tips received by a user
curl http://localhost:8080/api/tips?recipientId=bobEndpoint: GET /api/tips/{id}
Response: HTTP 200 OK or 404 Not Found
Endpoint: POST /api/reactions
Headers:
| Header | Required | Description |
|---|---|---|
| Idempotency-Key | No | UUID for safe retries (24-hour expiration) |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| roomId | string | Yes | Identifier for the room/stream |
| userId | string | Yes | User sending the reaction |
| emoji | string | Yes | Emoji reaction (e.g., "🔥", "❤️", "👍") |
| targetId | string | No | ID of content being reacted to (message, moment, etc.) |
| metadata | string | No | Optional JSON metadata |
Response: HTTP 201 Created (or 200 OK if idempotency key matched)
{
"id": 1,
"roomId": "gaming_stream_123",
"userId": "alice",
"emoji": "🔥",
"targetId": "msg_123",
"metadata": null,
"createdAt": "2024-01-15T10:30:45.123Z"
}Endpoint: GET /api/reactions
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| roomId | string | No | Filter by room ID |
| userId | string | No | Filter by user ID |
| emoji | string | No | Filter by emoji (requires roomId) |
| targetId | string | No | Filter by target ID |
| page | integer | No | Page number (default: 0) |
| size | integer | No | Page size (default: 20) |
Examples:
# Get all reactions
curl http://localhost:8080/api/reactions
# Get reactions for a room
curl http://localhost:8080/api/reactions?roomId=gaming_stream_123
# Get fire reactions in a room
curl "http://localhost:8080/api/reactions?roomId=gaming_stream_123&emoji=🔥"
# Get reactions to a specific message
curl http://localhost:8080/api/reactions?targetId=msg_123Endpoint: GET /api/reactions/{id}
Response: HTTP 200 OK or 404 Not Found
TipCurrent broadcasts all events (tips and reactions) in real-time using WebSocket with STOMP protocol.
Connect to: ws://localhost:8080/ws
Subscribe to room-specific topics to receive all events:
Topic: /topic/rooms/{roomId}
Both tips and reactions for that room are broadcast to the same topic.
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/rooms/gaming_stream_123', function(message) {
const event = JSON.parse(message.body);
// Check event type by presence of fields
if (event.amount !== undefined) {
console.log('Tip received:', event);
displayTip(event);
} else if (event.emoji !== undefined) {
console.log('Reaction received:', event);
displayReaction(event);
}
});
});- Live Stream Overlays: Display tips and reactions in real-time
- Audience Engagement: Show reaction bursts during exciting moments
- Creator Dashboards: Real-time revenue and engagement tracking
- Moderation Tools: Monitor activity across rooms
TipCurrent provides production-quality analytics using pre-aggregated summary tables for both tips and reactions.
Event Created → events tables (OLTP, write-optimized)
↓
@Scheduled job (runs hourly at :05)
↓
room_stats_hourly (pre-aggregated summary table)
↓
Analytics API → Fast reads from summary table only
Key Benefits:
- No resource contention between writes and analytics
- Predictable, fast query performance
- Includes both tip and reaction statistics
Trade-offs:
- Data freshness: Up to 1 hour lag
Endpoint: GET /api/analytics/rooms/{roomId}/stats
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| startDate | instant | No | Filter from this time (ISO 8601) |
| endDate | instant | No | Filter to this time (ISO 8601) |
Response:
{
"roomId": "gaming_stream_123",
"stats": [
{
"periodStart": "2024-01-15T10:00:00Z",
"periodEnd": "2024-01-15T11:00:00Z",
"totalTips": 25,
"totalAmount": 2500.00,
"uniqueSenders": 12,
"uniqueRecipients": 3,
"averageTipAmount": 100.00,
"totalReactions": 150,
"uniqueReactors": 45
}
],
"summary": {
"totalTips": 25,
"totalAmount": 2500.00,
"averageTipAmount": 100.00,
"totalReactions": 150
}
}TipCurrent provides webhooks for real-time event notifications to external systems.
| Event | Description |
|---|---|
tip.created |
Triggered when a new tip is created |
reaction.created |
Triggered when a new reaction is created |
Endpoint: POST /api/webhooks
{
"url": "https://your-platform.com/webhooks/tipcurrent",
"event": "tip.created",
"secret": "your-webhook-secret",
"roomId": "gaming_stream_123",
"description": "Tip notifications for room 123"
}Note: roomId is optional. If omitted, webhook receives events from all rooms.
GET /api/webhooks- List all webhooksGET /api/webhooks/{id}- Get webhook detailsDELETE /api/webhooks/{id}- Delete webhookPATCH /api/webhooks/{id}/enable- Enable webhookPATCH /api/webhooks/{id}/disable- Disable webhookPOST /api/webhooks/{id}/test- Send test payloadGET /api/webhooks/{id}/deliveries- View delivery logs
Headers:
Content-Type: application/json
X-TipCurrent-Signature: <HMAC-SHA256 signature>
X-TipCurrent-Event: tip.created
X-TipCurrent-Delivery-Attempt: 1
Body (tip.created):
{
"id": 1,
"roomId": "gaming_stream_123",
"senderId": "alice",
"recipientId": "bob",
"amount": 100.00,
"message": "Great play!",
"createdAt": "2024-01-15T10:30:45.123Z"
}Body (reaction.created):
{
"id": 1,
"roomId": "gaming_stream_123",
"userId": "alice",
"emoji": "🔥",
"targetId": "msg_123",
"createdAt": "2024-01-15T10:30:45.123Z"
}TipCurrent signs payloads using HMAC-SHA256. Verify the X-TipCurrent-Signature header:
import hmac, hashlib, base64
def verify_signature(payload, signature, secret):
expected = base64.b64encode(
hmac.new(secret.encode(), payload.encode(), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature)- Automatic retry: 3 attempts with exponential backoff
- Connection timeout: 5 seconds
- Request timeout: 10 seconds
All POST endpoints support idempotency via the Idempotency-Key header:
curl -X POST http://localhost:8080/api/tips \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{"roomId": "room1", "senderId": "alice", "recipientId": "bob", "amount": 100}'- First request: Returns 201 Created
- Retry with same key: Returns 200 OK with cached result
- Keys expire after 24 hours
- Automatic cleanup via scheduled job
Note: Integration tests require Docker for Testcontainers.
On macOS/Linux:
./mvnw testOn Windows:
mvnw.cmd testCurrent test count: 61 integration tests
| Column | Type | Description |
|---|---|---|
| id | BIGSERIAL | Primary key |
| room_id | VARCHAR | Room identifier (indexed) |
| sender_id | VARCHAR | Tip sender |
| recipient_id | VARCHAR | Tip recipient (indexed) |
| amount | DECIMAL(19,2) | Tip amount |
| message | VARCHAR(1000) | Optional message |
| metadata | TEXT | Optional JSON |
| created_at | TIMESTAMP | Creation time (indexed) |
| Column | Type | Description |
|---|---|---|
| id | BIGSERIAL | Primary key |
| room_id | VARCHAR | Room identifier (indexed) |
| user_id | VARCHAR | User who reacted (indexed) |
| emoji | VARCHAR(100) | Emoji reaction |
| target_id | VARCHAR | Target content ID |
| metadata | TEXT | Optional JSON |
| created_at | TIMESTAMP | Creation time (indexed) |
| Column | Type | Description |
|---|---|---|
| id | BIGSERIAL | Primary key |
| room_id | VARCHAR | Room identifier |
| period_start | TIMESTAMP | Hour start (indexed with room_id) |
| period_end | TIMESTAMP | Hour end |
| total_tips | BIGINT | Tip count |
| total_amount | DECIMAL(19,2) | Sum of tips |
| unique_senders | BIGINT | Distinct tip senders |
| unique_recipients | BIGINT | Distinct tip recipients |
| average_tip_amount | DECIMAL(19,2) | Mean tip amount |
| total_reactions | BIGINT | Reaction count |
| unique_reactors | BIGINT | Distinct reactors |
| last_aggregated_at | TIMESTAMP | Aggregation time |
src/
├── main/java/com/mchekin/tipcurrent/
│ ├── config/ # WebSocket configuration
│ ├── controller/ # REST controllers
│ │ ├── TipController.java
│ │ ├── ReactionController.java
│ │ ├── AnalyticsController.java
│ │ └── WebhookController.java
│ ├── domain/ # JPA entities
│ │ ├── Tip.java
│ │ ├── Reaction.java
│ │ ├── RoomStatsHourly.java
│ │ ├── WebhookEndpoint.java
│ │ └── IdempotencyRecord.java
│ ├── dto/ # Request/Response DTOs
│ ├── repository/ # Spring Data repositories
│ ├── scheduler/ # Scheduled jobs
│ └── service/ # Business logic
└── test/java/com/mchekin/tipcurrent/
├── TipIntegrationTest.java
├── ReactionIntegrationTest.java
├── AnalyticsIntegrationTest.java
├── WebhookIntegrationTest.java
└── IdempotencyIntegrationTest.java
- Spring Boot 4.0.1
- Java 25
- PostgreSQL 17
- WebSocket with STOMP protocol
- Maven
- Lombok
- Testcontainers
Planned features:
- Additional event types (Follow, Subscribe, Raid)
- Goals and Leaderboards
- Authentication and authorization
- Rate limiting
- Caching layer
- Production deployment documentation (Kubernetes, Helm)
MIT License
Contributions should maintain the project's focus on clarity, correctness, and conventional Spring Boot patterns. See .claude/VISION.md for project principles.