Skip to main content

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!

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:

  1. Sticky Sessions - ALB must route same client to same server
  2. WebSocket Support - ALB supports this natively
  3. 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:

  1. Install SignalR package
  2. Create hubs (SocialHub, NotificationHub)
  3. Configure SignalR in Program.cs
  4. Update controllers to send real-time updates
  5. 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:

  1. Install SignalR client libraries
  2. Create SignalR service classes
  3. Connect in components
  4. 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​

  1. Use Groups - Don't broadcast to all clients
  2. Filter on Server - Only send relevant updates
  3. Batch Updates - Group multiple updates together
  4. Connection Pooling - Reuse connections
  5. 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)​

  1. Install SignalR
  2. Create hubs
  3. Update controllers to ALSO send SignalR updates
  4. Keep REST API unchanged
  5. Frontend continues using REST API

Phase 2: Frontend Integration​

  1. Add SignalR client to frontend
  2. Connect for real-time updates
  3. Keep REST API for initial data load
  4. Use SignalR for live updates

Phase 3: Optimize​

  1. Reduce REST API polling
  2. Rely more on SignalR
  3. 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​

  1. ✅ Review this strategy
  2. ⬜ Install SignalR package
  3. ⬜ Create SocialHub and NotificationHub
  4. ⬜ Update Program.cs
  5. ⬜ Update SocialController to send real-time updates
  6. ⬜ Test with SignalR client (Postman or simple HTML page)
  7. ⬜ Enable sticky sessions on ALB (optional)
  8. ⬜ Document for frontend team

Resources​


Last Updated: December 2025
Status: Strategy Document - Ready for Implementation
Recommendation: Implement SignalR backend now, frontend integration later