Real-Time Updates Strategy
Comprehensive guide for implementing real-time updates in FishingLog for instant data synchronization across backend, web, and mobile apps.
Understanding the Difference
Webhooks (Server-to-Server)
- What: External services notify YOUR server (e.g., Stripe → Your API)
- Use Case: Payment webhooks, third-party integrations
- Example: Stripe sends webhook when payment succeeds
Real-Time Client Updates (Client-to-Server)
- What: YOUR server pushes updates to connected clients instantly
- Use Case: Social feed updates, notifications, live data
- Example: New post appears in feed immediately for all users
You need real-time client updates, not webhooks!
Recommended Solution: SignalR
SignalR is Microsoft's real-time communication library for .NET. It's perfect for your use case.
Why SignalR?
✅ Native .NET integration - Built for ASP.NET Core
✅ Automatic fallback - WebSockets → Server-Sent Events → Long Polling
✅ Built-in authentication - Works with your JWT tokens
✅ Scalable - Supports Redis backplane for multiple servers
✅ Easy to use - Simple API for both backend and frontend
✅ AWS compatible - Works with ALB and ECS
How It Works
Client (React/React Native)
↓ (WebSocket connection)
SignalR Hub (Your API)
↓ (Broadcast to connected clients)
All Connected Clients receive update instantly
Architecture Overview
Current Architecture (REST API)
Frontend → HTTP Request → API → Database
Frontend ← HTTP Response ← API ← Database
With SignalR (Real-Time)
Frontend → WebSocket → SignalR Hub → Database
Frontend ← Real-Time Update ← SignalR Hub ← Event Trigger
Implementation Plan
Phase 1: Backend Setup (Do This Now)
1. Install SignalR Package
cd FishingLog.API
dotnet add package Microsoft.AspNetCore.SignalR
2. Create SignalR Hubs
Social Hub - For social feed updates:
// Hubs/SocialHub.cs
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;
[Authorize]
public class SocialHub : Hub
{
// Called when client connects
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier; // From JWT token
await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{userId}");
await base.OnConnectedAsync();
}
// Called when client disconnects
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.UserIdentifier;
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}");
await base.OnDisconnectedAsync(exception);
}
// Client can subscribe to specific user's posts
public async Task SubscribeToUser(string userId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"user-posts-{userId}");
}
// Client can subscribe to circle updates
public async Task SubscribeToCircle(string circleId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"circle-{circleId}");
}
}
Notification Hub - For user notifications:
// Hubs/NotificationHub.cs
[Authorize]
public class NotificationHub : Hub
{
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier;
// Add to user's notification group
await Groups.AddToGroupAsync(Context.ConnectionId, $"notifications-{userId}");
await base.OnConnectedAsync();
}
}
3. Configure SignalR in Program.cs
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
// Add SignalR
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
// ... existing code ...
var app = builder.Build();
// ... existing middleware ...
// Map SignalR hubs (BEFORE MapControllers)
app.MapHub<SocialHub>("/hubs/social");
app.MapHub<NotificationHub>("/hubs/notifications");
app.MapControllers();
app.Run();
4. Update Controllers to Send Real-Time Updates
Example: SocialController - When Post is Created
// Controllers/SocialController.cs
public class SocialController : ControllerBase
{
private readonly AppDbContext _db;
private readonly IHubContext<SocialHub> _hubContext; // Add this
private readonly ILogger<SocialController> _logger;
public SocialController(
AppDbContext db,
IHubContext<SocialHub> hubContext, // Add this
ILogger<SocialController> logger)
{
_db = db;
_hubContext = hubContext; // Add this
_logger = logger;
}
[HttpPost("posts")]
public async Task<ActionResult<Post>> CreatePost([FromBody] CreatePostRequest request)
{
var currentUser = await AuthorizationHelper.GetCurrentUserAsync(_db, User);
if (currentUser == null)
return Unauthorized();
var post = new Post
{
UserId = currentUser.Id,
Content = request.Content,
Visibility = request.Visibility,
CreatedAt = DateTime.UtcNow
};
_db.Posts.Add(post);
await _db.SaveChangesAsync();
// Load related data for broadcast
await _db.Entry(post)
.Reference(p => p.User)
.LoadAsync();
// Send real-time update to connected clients
await _hubContext.Clients.Group($"user-{currentUser.Id}")
.SendAsync("PostCreated", post);
// Also send to friends/circle members based on visibility
if (post.Visibility == PostVisibility.Public)
{
// Send to all connected users (or use more specific groups)
await _hubContext.Clients.All.SendAsync("NewPost", post);
}
else if (post.Visibility == PostVisibility.Friends)
{
// Get user's friends and send to them
var friends = await GetUserFriendsAsync(currentUser.Id);
foreach (var friend in friends)
{
await _hubContext.Clients.Group($"user-{friend.Id}")
.SendAsync("NewPost", post);
}
}
return CreatedAtAction(nameof(GetPost), new { id = post.Id }, post);
}
[HttpPost("posts/{id}/like")]
public async Task<ActionResult<PostLike>> LikePost(Guid id)
{
// ... existing like logic ...
// Send real-time update
await _hubContext.Clients.Group($"post-{id}")
.SendAsync("PostLiked", new
{
PostId = id,
UserId = currentUser.Id,
UserName = currentUser.UserName,
LikeCount = post.LikeCount
});
return CreatedAtAction(nameof(GetPost), new { id }, createdLike);
}
[HttpPost("posts/{id}/comments")]
public async Task<ActionResult<Comment>> AddComment(Guid id, [FromBody] Comment comment)
{
// ... existing comment logic ...
// Send real-time update
await _hubContext.Clients.Group($"post-{id}")
.SendAsync("CommentAdded", comment);
return CreatedAtAction(nameof(GetPost), new { id }, comment);
}
}
5. Create Real-Time Service Helper
// Services/RealtimeNotificationService.cs
public class RealtimeNotificationService
{
private readonly IHubContext<NotificationHub> _hubContext;
public RealtimeNotificationService(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
public async Task NotifyUserAsync(string userId, Notification notification)
{
await _hubContext.Clients.Group($"notifications-{userId}")
.SendAsync("NotificationReceived", notification);
}
public async Task NotifyUsersAsync(List<string> userIds, Notification notification)
{
var tasks = userIds.Select(userId =>
_hubContext.Clients.Group($"notifications-{userId}")
.SendAsync("NotificationReceived", notification));
await Task.WhenAll(tasks);
}
}
Phase 2: Frontend Integration (Web App)
1. Install SignalR Client
cd web-app
npm install @microsoft/signalr
2. Create SignalR Service
// lib/signalr/socialHub.ts
import * as signalR from '@microsoft/signalr';
import { getAuthToken } from '@/lib/auth';
class SocialHubService {
private connection: signalR.HubConnection | null = null;
async connect() {
const token = await getAuthToken();
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${process.env.NEXT_PUBLIC_API_URL}/hubs/social`, {
accessTokenFactory: () => token,
})
.withAutomaticReconnect()
.build();
// Handle reconnection
this.connection.onreconnecting(() => {
console.log('SignalR reconnecting...');
});
this.connection.onreconnected(() => {
console.log('SignalR reconnected');
});
await this.connection.start();
console.log('SignalR connected');
}
async disconnect() {
if (this.connection) {
await this.connection.stop();
this.connection = null;
}
}
// Subscribe to new posts
onNewPost(callback: (post: Post) => void) {
this.connection?.on('NewPost', callback);
}
// Subscribe to post likes
onPostLiked(callback: (data: { postId: string; userId: string; likeCount: number }) => void) {
this.connection?.on('PostLiked', callback);
}
// Subscribe to comments
onCommentAdded(callback: (comment: Comment) => void) {
this.connection?.on('CommentAdded', callback);
}
// Subscribe to specific user's posts
async subscribeToUser(userId: string) {
await this.connection?.invoke('SubscribeToUser', userId);
}
// Subscribe to circle updates
async subscribeToCircle(circleId: string) {
await this.connection?.invoke('SubscribeToCircle', circleId);
}
}
export const socialHub = new SocialHubService();
3. Use in React Component
// components/SocialFeed.tsx
'use client';
import { useEffect, useState } from 'react';
import { socialHub } from '@/lib/signalr/socialHub';
import { useQuery, useQueryClient } from '@tanstack/react-query';
export function SocialFeed() {
const queryClient = useQueryClient();
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: () => fetchPosts(),
});
useEffect(() => {
// Connect to SignalR
socialHub.connect();
// Listen for new posts
socialHub.onNewPost((newPost) => {
// Update React Query cache
queryClient.setQueryData(['posts'], (old: Post[]) => {
return [newPost, ...(old || [])];
});
});
// Listen for likes
socialHub.onPostLiked((data) => {
queryClient.setQueryData(['posts'], (old: Post[]) => {
return old?.map(post =>
post.id === data.postId
? { ...post, likeCount: data.likeCount }
: post
) || [];
});
});
// Cleanup on unmount
return () => {
socialHub.disconnect();
};
}, [queryClient]);
return (
<div>
{posts?.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Phase 3: Mobile App Integration (React Native)
1. Install SignalR Client
cd mobile-app
npm install @microsoft/signalr
npm install react-native-get-random-values # Required for SignalR
2. Create SignalR Service
// services/signalr/socialHub.ts
import * as signalR from '@microsoft/signalr';
import { getAuthToken } from '../auth';
class SocialHubService {
private connection: signalR.HubConnection | null = null;
async connect() {
const token = await getAuthToken();
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${API_URL}/hubs/social`, {
accessTokenFactory: () => token,
headers: {
'Authorization': `Bearer ${token}`,
},
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
return Math.min(1000 * Math.pow(2, retryContext.previousRetryCount), 30000);
},
})
.build();
await this.connection.start();
}
async disconnect() {
if (this.connection) {
await this.connection.stop();
}
}
onNewPost(callback: (post: Post) => void) {
this.connection?.on('NewPost', callback);
}
onPostLiked(callback: (data: any) => void) {
this.connection?.on('PostLiked', callback);
}
}
export const socialHub = new SocialHubService();
3. Use in React Native Component
// screens/SocialFeedScreen.tsx
import { useEffect } from 'react';
import { socialHub } from '../services/signalr/socialHub';
import { useQuery, useQueryClient } from '@tanstack/react-query';
export function SocialFeedScreen() {
const queryClient = useQueryClient();
useEffect(() => {
socialHub.connect();
socialHub.onNewPost((newPost) => {
queryClient.setQueryData(['posts'], (old: Post[]) => {
return [newPost, ...(old || [])];
});
});
return () => {
socialHub.disconnect();
};
}, []);
// ... rest of component
}
AWS Infrastructure Considerations
Current Setup (ALB + ECS)
Your current setup:
- Application Load Balancer (ALB) - Routes HTTP traffic
- ECS Fargate - Runs your containers
- Multiple instances - For scaling
SignalR Requirements
SignalR needs:
- Sticky Sessions - ALB must route same client to same server
- WebSocket Support - ALB supports this natively
- Redis Backplane - For multi-server scaling (optional initially)
ALB Configuration
Enable Sticky Sessions:
# In your ECS task definition or ALB target group
TargetGroupAttributes:
- Key: stickiness.enabled
Value: true
- Key: stickiness.type
Value: lb_cookie
- Key: stickiness.lb_cookie.duration_seconds
Value: 3600
WebSocket Support:
ALB automatically supports WebSockets. No additional config needed!
Redis Backplane (For Scaling)
When you have multiple ECS instances, use Redis to sync SignalR across servers:
// Program.cs
using Microsoft.AspNetCore.SignalR;
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
// Add Redis for SignalR backplane
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var redisConnectionString = builder.Configuration["Redis:ConnectionString"];
return ConnectionMultiplexer.Connect(redisConnectionString);
});
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.ConnectionString = builder.Configuration["Redis:ConnectionString"];
options.Configuration.ChannelPrefix = "FishingLog:SignalR:";
});
AWS ElastiCache Redis:
# CloudFormation/Terraform
ElastiCacheRedis:
Type: AWS::ElastiCache::ReplicationGroup
Properties:
ReplicationGroupId: fishinglog-signalr-redis
Engine: redis
NodeType: cache.t3.micro # Start small
NumCacheClusters: 1
AutomaticFailoverEnabled: false # Enable later for HA
Use Cases & Implementation
1. Social Feed Updates
When: New post created, post liked, comment added
Backend:
// After creating post
await _hubContext.Clients.Group($"user-{userId}")
.SendAsync("NewPost", post);
Frontend:
socialHub.onNewPost((post) => {
// Add to feed immediately
setPosts(prev => [post, ...prev]);
});
2. Live Notifications
When: User receives notification (like, comment, mention)
Backend:
await _notificationService.NotifyUserAsync(userId, notification);
Frontend:
notificationHub.onNotification((notification) => {
// Show toast/banner
showNotification(notification);
});
3. Tournament Leaderboard Updates
When: New catch submitted, leaderboard changes
Backend:
await _hubContext.Clients.Group($"tournament-{tournamentId}")
.SendAsync("LeaderboardUpdated", leaderboard);
Frontend:
tournamentHub.onLeaderboardUpdate((leaderboard) => {
// Update leaderboard instantly
setLeaderboard(leaderboard);
});
4. Payment Status Updates
When: Payment succeeds/fails (after webhook)
Backend:
// In StripeWebhookController after processing
await _hubContext.Clients.User(userId)
.SendAsync("PaymentStatusChanged", new
{
PaymentIntentId = paymentIntentId,
Status = "succeeded"
});
Frontend:
paymentHub.onPaymentStatus((status) => {
// Update UI immediately
setPaymentStatus(status);
});
5. Moderation Queue Updates (Admin)
When: New content needs moderation
Backend:
await _hubContext.Clients.Group("admins")
.SendAsync("NewModerationItem", item);
Frontend (Admin Panel):
adminHub.onNewModerationItem((item) => {
// Add to moderation queue
addToQueue(item);
});
What Needs to Change NOW vs LATER
Do NOW (Before Frontend Apps)
✅ Backend Setup:
- Install SignalR package
- Create hubs (SocialHub, NotificationHub)
- Configure SignalR in Program.cs
- Update controllers to send real-time updates
- Test with Postman/Insomnia (can test SignalR endpoints)
Why Now:
- Backend changes are independent
- Frontend can be built to use it later
- No breaking changes to existing API
Do LATER (When Building Frontend)
⏳ Frontend Integration:
- Install SignalR client libraries
- Create SignalR service classes
- Connect in components
- Handle reconnection logic
Why Later:
- Frontend apps aren't built yet
- Can add real-time features incrementally
- Start with REST API, add SignalR later
Infrastructure Changes
NOW (Optional but Recommended):
- Enable sticky sessions on ALB
- Plan for Redis backplane (if scaling)
LATER (When Scaling):
- Set up ElastiCache Redis
- Configure SignalR backplane
- Monitor connection counts
Testing SignalR
Backend Testing
// Test SignalR hub
[Fact]
public async Task SocialHub_BroadcastsNewPost()
{
var hubContext = new Mock<IHubContext<SocialHub>>();
var clients = new Mock<IHubClients>();
var group = new Mock<IClientProxy>();
clients.Setup(c => c.Group(It.IsAny<string>())).Returns(group.Object);
hubContext.Setup(h => h.Clients).Returns(clients.Object);
// Test your controller with mocked hub context
}
Frontend Testing
// Mock SignalR connection
jest.mock('@microsoft/signalr', () => ({
HubConnectionBuilder: jest.fn(() => ({
withUrl: jest.fn().mockReturnThis(),
withAutomaticReconnect: jest.fn().mockReturnThis(),
build: jest.fn(() => ({
start: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
invoke: jest.fn(),
})),
})),
}));
Performance Considerations
Connection Limits
- Single Server: ~20,000 concurrent connections
- With Redis Backplane: Unlimited (scales horizontally)
- ALB: Supports millions of connections
Optimization Tips
- Use Groups - Don't broadcast to all clients
- Filter on Server - Only send relevant updates
- Batch Updates - Group multiple updates together
- Connection Pooling - Reuse connections
- Monitor - Track connection counts and performance
Security Considerations
Authentication
SignalR uses your existing JWT authentication:
[Authorize]
public class SocialHub : Hub
{
// Context.User is available (from JWT)
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier; // From JWT
// ...
}
}
Authorization
// Only allow users to subscribe to their own notifications
public async Task SubscribeToNotifications()
{
var userId = Context.UserIdentifier;
await Groups.AddToGroupAsync(Context.ConnectionId, $"notifications-{userId}");
}
Monitoring
CloudWatch Metrics
Track:
- Active SignalR connections
- Messages sent/received
- Reconnection attempts
- Error rates
Logging
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
// Log connection events
public override async Task OnConnectedAsync()
{
_logger.LogInformation("SignalR client connected: {ConnectionId}", Context.ConnectionId);
await base.OnConnectedAsync();
}
Cost Estimation
AWS Costs
- ALB: $0.0225/hour = ~$16/month (base)
- ECS: No additional cost (uses existing tasks)
- ElastiCache Redis: $15/month (cache.t3.micro) - Optional initially
Total: ~$16/month (without Redis), ~$31/month (with Redis)
Migration Strategy
Phase 1: Add SignalR (No Breaking Changes)
- Install SignalR
- Create hubs
- Update controllers to ALSO send SignalR updates
- Keep REST API unchanged
- Frontend continues using REST API
Phase 2: Frontend Integration
- Add SignalR client to frontend
- Connect for real-time updates
- Keep REST API for initial data load
- Use SignalR for live updates
Phase 3: Optimize
- Reduce REST API polling
- Rely more on SignalR
- Add Redis backplane if needed
Example: Complete Social Feed Flow
Backend
[HttpPost("posts")]
public async Task<ActionResult<Post>> CreatePost([FromBody] CreatePostRequest request)
{
// 1. Create post (existing logic)
var post = new Post { /* ... */ };
_db.Posts.Add(post);
await _db.SaveChangesAsync();
// 2. Send REST API response (existing)
var response = CreatedAtAction(nameof(GetPost), new { id = post.Id }, post);
// 3. Send real-time update (NEW)
await _hubContext.Clients.Group($"user-{currentUser.Id}")
.SendAsync("PostCreated", post);
// 4. Notify friends/circle members
if (post.Visibility == PostVisibility.Public)
{
await _hubContext.Clients.All.SendAsync("NewPost", post);
}
return response;
}
Frontend
// Initial load (REST API)
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/social/posts').then(r => r.json()),
});
// Real-time updates (SignalR)
useEffect(() => {
socialHub.connect();
socialHub.onNewPost((newPost) => {
// Add to feed instantly
queryClient.setQueryData(['posts'], (old) => [newPost, ...(old || [])]);
});
}, []);
Next Steps
- ✅ Review this strategy
- ⬜ Install SignalR package
- ⬜ Create SocialHub and NotificationHub
- ⬜ Update Program.cs
- ⬜ Update SocialController to send real-time updates
- ⬜ Test with SignalR client (Postman or simple HTML page)
- ⬜ Enable sticky sessions on ALB (optional)
- ⬜ Document for frontend team
Resources
Last Updated: December 2025
Status: Strategy Document - Ready for Implementation
Recommendation: Implement SignalR backend now, frontend integration later