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