Modern web applications generate a continuous stream of user activities. Users log in, update their profiles, create records, upload files, change permissions, and perform many other actions throughout the day. In a small application, tracking these activities manually may seem manageable. However, as an application grows, understanding who did what, when an action occurred, and what changed becomes increasingly important.
This is where a real-time activity log becomes valuable.
A real-time activity log records important events within an application and makes them available to users, administrators, or support teams as they happen. Instead of searching through scattered server logs or asking users to explain what happened, developers can provide a centralized timeline of meaningful actions.
For example, a project management application might display the following activities:
- Sarah created a new project.
- Michael updated the project deadline.
- David uploaded a document.
- Sarah invited a new team member.
- An administrator changed a user's permissions.
These events help teams understand application activity, investigate problems, and collaborate more effectively.
In this article, we will explore how to design and build a real-time activity log using React, Python, FastAPI, PostgreSQL, and WebSockets. We will cover database design, event recording, real-time updates, security, performance optimization, and deployment considerations.
1. Understanding Real-Time Activity Logs
Before writing code, it is important to understand what an activity log does and how it differs from ordinary application logging.
Traditional application logs are primarily designed for developers and infrastructure teams. They record technical information such as exceptions, failed database connections, HTTP errors, and performance problems.
An activity log focuses on meaningful actions performed by users or systems.
For example:
- Technical log:
POST /api/projects returned status 201.
- Activity log:
Sarah created the project "Website Redesign".
Both records may describe the same operation, but they serve different purposes.
Common use cases
Real-time activity logs are useful in many applications.
Project management platforms: Track task creation, assignments, comments, and deadline changes.
E-commerce applications: Record order updates, refunds, inventory changes, and administrative actions.
CRM systems: Track customer record updates, sales activities, and changes to lead ownership.
Financial dashboards: Maintain auditable records of important account operations.
Administrative panels: Show user logins, permission changes, configuration updates, and security events.
A well-designed activity log should answer three fundamental questions:
- Who performed the action?
- What action occurred?
- When did the action happen?
Depending on the application, it may also record the affected resource, the previous and updated values, the source of the action, and the result.
2. Choosing the Technology Stack
Our example uses a stack that supports structured data, asynchronous APIs, and live browser updates.
Frontend: React
React renders the activity timeline and updates the interface when new events arrive. It can connect to a WebSocket endpoint and display incoming messages without requiring a complete page refresh.
Backend: Python and FastAPI
FastAPI handles authentication, validates incoming requests, records activities, and exposes endpoints for retrieving historical events. Its WebSocket support allows the backend to communicate with connected browsers.
Database: PostgreSQL
PostgreSQL stores activity records permanently. It supports relational queries, indexes, transactions, and pagination, making it suitable for an activity timeline.
Real-time communication: WebSockets
WebSockets establish a persistent connection between the browser and server. After a new activity is committed to the database, the backend can publish an event to authorized connected clients.
Optional infrastructure: Redis
For applications running multiple backend instances, Redis Pub/Sub or a durable message broker can distribute events between servers. Redis is not required for a single-instance prototype.
The architecture looks like this:
User performs an action
|
v
React frontend
|
v
FastAPI endpoint
|
v
Business operation + database transaction
|
v
PostgreSQL activity record
|
v
Event publisher
|
v
WebSocket connection
|
v
React activity timeline
The essential principle is that the database remains the source of truth. WebSockets deliver updates quickly, but clients should be able to retrieve historical events from the database if they disconnect.
3. Designing the Activity Log Database
A reliable activity log starts with a clear database schema. Each record should contain enough information to explain an event without storing unnecessary sensitive data.
We can create an activity_logs table with the following fields:
| Field | Purpose |
id | Unique event identifier |
actor_id | User or system responsible for the action |
action | Type of activity performed |
entity_type | Type of affected resource |
entity_id | Identifier of the affected resource |
description | Human-readable event summary |
metadata | Additional structured event details |
created_at | Timestamp when the event was recorded |
The metadata field can use PostgreSQL's JSONB type. This makes it possible to store flexible information without creating a new database column for every event type.
For example, a project update could contain:
{
"field": "deadline",
"old_value": "2026-11-10",
"new_value": "2026-11-15"
}
Avoid storing passwords, authentication tokens, payment card details, or unnecessary personal information in event metadata.
Creating the database table
A simple PostgreSQL migration might look like this:
CREATE TABLE activity_logs (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
actor_id BIGINT NULL,
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(100),
entity_id VARCHAR(100),
description TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_activity_logs_created_at
ON activity_logs (created_at DESC, id DESC);
CREATE INDEX idx_activity_logs_actor
ON activity_logs (actor_id, created_at DESC, id DESC);
CREATE INDEX idx_activity_logs_entity
ON activity_logs (
entity_type,
entity_id,
created_at DESC,
id DESC
);
These indexes support common queries, including retrieving recent events, filtering by actor, and viewing the history of a specific resource.
In a multi-tenant application, include a tenant_id or equivalent ownership identifier and design indexes around the actual access patterns.
The example uses a nullable actor_id because some events may be generated by scheduled jobs or automated processes. In a production database, define appropriate foreign keys and retention rules according to your user-deletion and audit requirements.
4. Defining Consistent Activity Events
A major challenge is maintaining consistency across different parts of an application.
Without a shared event structure, one feature might record "user updated" while another records "profile_changed". The frontend then becomes responsible for understanding many inconsistent formats.
A better approach is to define a standard event contract.
For example:
{
"id": "evt_1024",
"actor": {
"id": 42,
"name": "Sarah"
},
"action": "project.updated",
"entity_type": "project",
"entity_id": "project_15",
"description": "Sarah updated the project deadline",
"metadata": {
"field": "deadline"
},
"created_at": "2026-10-27T06:30:00Z"
}
The action name should describe the event in a predictable way. Names such as project.created, project.updated, task.assigned, and user.permission_changed are easier to maintain than arbitrary descriptions used as event identifiers.
Separate the machine-readable action from the human-readable description.
The action supports filtering, analytics, and integrations. The description is intended for display and can be localized later.
Also, distinguish between the person who performed an action and the person or object affected by it. For example, an administrator may change another user's permissions.
5. Building the FastAPI Activity Service
The backend should provide reusable functions for creating and retrieving activities. Individual business endpoints should call these functions rather than duplicating database logic.
The following example illustrates the structure of a service. It assumes that the application has already configured its database session and authentication system.
from datetime import datetime, timezone
from typing import Any
from pydantic import BaseModel, Field
class ActivityCreate(BaseModel):
actor_id: int | None = None
action: str = Field(min_length=1, max_length=100)
entity_type: str | None = None
entity_id: str | None = None
description: str = Field(min_length=1, max_length=500)
metadata: dict[str, Any] = Field(default_factory=dict)
class ActivityResponse(BaseModel):
id: int
actor_id: int | None
action: str
entity_type: str | None
entity_id: str | None
description: str
metadata: dict[str, Any]
created_at: datetime
Pydantic validates the shape of the data before it reaches the service. However, validation alone is not enough. The backend must also determine whether the authenticated actor is allowed to perform the action and whether the supplied resource belongs to the correct tenant.
Recording an activity
The activity service should insert the event into the database using the same transaction as the business operation whenever practical.
Conceptually, the service performs the following steps:
- Validate the event data.
- Confirm the actor's identity and permissions.
- Execute the business operation.
- Insert the activity record.
- Commit the transaction.
- Publish the event for real-time delivery.
This ordering matters. Broadcasting an event before the database transaction commits can cause the interface to display a change that is later rolled back.
For a small application, a database insert followed by publication may be sufficient. For production systems where losing an event is unacceptable, the transactional outbox pattern provides stronger reliability.
6. Connecting Business Operations to the Activity Log
The most useful logs are generated automatically by meaningful application operations.
Suppose a user creates a project. The backend should create the project and its activity record together.
A simplified FastAPI endpoint could follow this pattern:
from fastapi import APIRouter, Depends, HTTPException
router = APIRouter()
@router.post("/projects")
async def create_project(
payload: dict,
current_user=Depends(get_current_user),
db=Depends(get_db),
):
project = await save_project(db, payload, current_user.id)
activity = await save_activity(
db=db,
actor_id=current_user.id,
action="project.created",
entity_type="project",
entity_id=str(project.id),
description=f"{current_user.name} created a project",
metadata={"project_name": project.name},
)
await db.commit()
await publish_activity(activity)
return project
Here, get_current_user, get_db, save_project, save_activity, and publish_activity represent application-specific functions that must be implemented.
For true transactional safety, both database writes must use the same transaction, and failures must roll back both operations. The publishing function should also handle delivery failures without incorrectly reporting that a committed business operation failed.
A stronger design records an outbox message in the same transaction. A separate worker publishes committed messages and retries failed deliveries.
Which actions should be recorded?
Do not record every technical operation indiscriminately. Focus on events that help users understand changes or help administrators investigate important activity.
Useful examples include:
- Creating, editing, or deleting a record.
- Assigning a task to a team member.
- Updating an order status.
- Changing account permissions.
- Uploading or removing a document.
- Inviting or removing a user.
Read-only operations, routine polling requests, and other high-frequency events usually belong in application telemetry rather than the business activity timeline.
7. Building the WebSocket Endpoint
REST APIs are useful for retrieving historical activities, but repeatedly polling an endpoint is inefficient when users need immediate updates.
WebSockets allow the server to push newly published events to connected clients.
A minimal FastAPI implementation might look like this:
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.connections: set[WebSocket] = set()
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.connections.add(websocket)
def disconnect(self, websocket: WebSocket):
self.connections.discard(websocket)
async def broadcast(self, event: dict):
disconnected = []
for connection in list(self.connections):
try:
await connection.send_json(event)
except Exception:
disconnected.append(connection)
for connection in disconnected:
self.disconnect(connection)
manager = ConnectionManager()
@app.websocket("/ws/activity")
async def activity_socket(websocket: WebSocket):
# Authenticate and authorize the connection before accepting it.
user = await authenticate_websocket(websocket)
if user is None:
await websocket.close(code=1008)
return
await manager.connect(websocket)
try:
while True:
# Production implementations should also handle heartbeats,
# disconnects, and bounded message processing.
await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
This code illustrates the connection lifecycle rather than a complete production implementation. The authenticate_websocket function must be implemented using your application's authentication strategy.
The manager broadcasts only to sockets registered with it. In a real multi-user application, a global broadcast to every connection would be unsafe. Store connections by authorized tenant, project, organization, or user, and send each event only to clients entitled to see it.
Never assume that a user is authorized simply because they successfully established a WebSocket connection.
8. Implementing the React Activity Timeline
The frontend needs two capabilities: loading existing activities and receiving new ones.
The historical API might expose an endpoint such as GET /api/activity?limit=20. The browser can load the most recent records when the page opens, while the WebSocket delivers updates afterward.
A basic React component could look like this:
import { useEffect, useState } from "react";
export default function ActivityTimeline() {
const [activities, setActivities] = useState([]);
useEffect(() => {
let socket;
async function loadActivities() {
const response = await fetch("/api/activity?limit=20", {
credentials: "include",
});
if (!response.ok) {
throw new Error("Unable to load activity history");
}
const data = await response.json();
setActivities(data.items);
}
loadActivities().catch(console.error);
socket = new WebSocket("wss://example.com/ws/activity");
socket.onmessage = (message) => {
const activity = JSON.parse(message.data);
setActivities((current) => {
if (current.some((item) => item.id === activity.id)) {
return current;
}
return [activity, ...current].slice(0, 50);
});
};
return () => {
socket?.close();
};
}, []);
return (
<section>
<h2>Recent Activity</h2>
<ul>
{activities.map((activity) => (
<li key={activity.id}>
<strong>{activity.description}</strong>
<time dateTime={activity.created_at}>
{new Date(activity.created_at).toLocaleString()}
</time>
</li>
))}
</ul>
</section>
);
}
Replace the example domain with your own deployment address. The API response is assumed to contain an items array, and the WebSocket endpoint must return events matching the activity schema.
This example intentionally stays small. A production component should handle loading states, API errors, WebSocket reconnection, authentication expiration, and changes to network connectivity.
For session-cookie authentication, use secure cookie settings and validate the WebSocket handshake's origin. If your system uses bearer tokens, avoid placing long-lived credentials in URLs because URLs can appear in logs and monitoring systems.
Improving the user experience
An activity timeline should be easy to scan. Consider displaying:
- The actor's name and profile image, where appropriate.
- A short action description.
- A relative timestamp such as "2 minutes ago".
- A link to the affected resource.
- Filters for event type, user, or date range.
Use semantic HTML and accessible labels. Color can communicate event categories, but the interface should not depend on color alone.
Also, avoid inserting user-generated descriptions as raw HTML. Render them as text or sanitize them appropriately to prevent cross-site scripting.
An activity table can grow rapidly. Returning every event in one API response will eventually become slow and expensive.
Use pagination to retrieve only the records the interface needs.
For smaller datasets, limit-and-offset pagination is straightforward:
SELECT id, actor_id, action, entity_type,
entity_id, description, created_at
FROM activity_logs
WHERE tenant_id = :tenant_id
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 0;
This query assumes that a tenant_id column has been added to the table.
For large datasets, cursor-based pagination is usually preferable. The client supplies the timestamp and ID of the last event it received. The next page uses a condition such as:
SELECT id, actor_id, action, entity_type,
entity_id, description, created_at
FROM activity_logs
WHERE tenant_id = :tenant_id
AND (created_at, id) < (:cursor_time, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
The timestamp and ID together provide a stable ordering when multiple activities share the same timestamp.
Validate pagination parameters on the backend and apply a maximum page size. The database should return only fields the client is allowed to see.
10. Handling Real-Time Events Reliably
A WebSocket connection does not guarantee that every event will reach every browser.
Users may lose their network connection, close a laptop, refresh a page, or move between networks. A server may also restart while clients are connected.
For that reason, treat real-time delivery as an optimization rather than the permanent storage mechanism.
A reliable workflow should:
- Save the activity record.
- Commit the database transaction.
- Publish an event containing the record identifier and relevant payload.
- Retry publication if delivery fails.
- Allow clients to reconnect.
- Fetch missed activities after reconnection.
For higher reliability, use the transactional outbox pattern. In the same database transaction as the business change, insert an outbox record describing the event. A background worker reads unpublished records, publishes them to a message broker, and marks them as processed.
If the worker crashes after publishing but before marking a message as processed, it may publish the same event again. Therefore, consumers should be idempotent and use event IDs to avoid duplicate processing.
Redis Pub/Sub can distribute messages between application instances, but it does not provide durable message storage for disconnected consumers. If events must survive outages, consider a durable queue or streaming system appropriate to your workload.
11. Security and Access Control
Activity logs can contain sensitive information about business operations. Protecting them is as important as protecting the primary application database.
Authenticate every request
Require authentication for historical activity endpoints and WebSocket connections. Expired sessions and revoked permissions must be handled correctly.
Enforce authorization
Filter activity records by tenant, organization, project, or user permissions on the server. Never rely on the React interface to hide records that the user is not allowed to access.
Prevent tampering
For important audit trails, restrict modification and deletion privileges. Consider append-only database permissions, immutable backups, or external audit storage where appropriate.
Minimize sensitive data
Do not store passwords, session cookies, API keys, full payment details, or unnecessary personal information in event descriptions or metadata.
Validate WebSocket origins
For cookie-authenticated connections, validate the request origin against an explicit allowlist. Use encrypted connections in production and rate-limit connection attempts.
Separate business events from security logs
A business activity timeline and a security audit trail may overlap, but they are not identical. Security logs may require stricter access controls, longer retention, reliable ingestion, and records of privileged administrative actions.
Do not assume that a normal user-facing activity feed is sufficient for regulatory or security auditing.
As the number of users grows, the volume of activity events can increase significantly. A design that works for a small prototype may struggle under a large workload.
Use appropriate database indexes
Index fields used for filtering and ordering. Avoid adding indexes without a clear query pattern because every additional index increases storage requirements and write costs.
Limit WebSocket payloads
Send only the fields the recipient needs. Large metadata objects increase network traffic and browser processing time.
Avoid blocking database operations
Use asynchronous database access where appropriate, connection pooling, and carefully managed transaction boundaries.
Batch or queue publication
If many events are generated in a short period, a queue can absorb bursts and allow workers to publish at a controlled rate.
Apply retention policies
Not every event needs to remain in the primary database forever. Depending on business requirements, older records may be archived, partitioned, or deleted after an approved retention period.
Never introduce a retention policy that conflicts with legal, contractual, or audit obligations.
Monitor important metrics
Track event creation latency, publication failures, queue depth, WebSocket connection counts, query duration, and database storage growth.
These metrics help identify bottlenecks before users experience slow timelines or missing updates.
13. Testing the Activity Log
Testing should cover both the stored history and real-time behavior.
Unit tests: Verify event validation, action naming, metadata handling, and formatting.
Integration tests: Confirm that a successful business operation creates the expected activity record and that a failed transaction does not leave a misleading record.
WebSocket tests: Check that authorized clients receive relevant events and unauthorized clients cannot subscribe to restricted activity streams.
Pagination tests: Verify stable ordering when timestamps match and ensure that cursor-based requests do not unexpectedly repeat records.
Reconnect tests: Simulate a dropped connection, reconnect, and fetch missed events.
Security tests: Check tenant isolation, authorization changes, origin validation, and attempts to inject unsafe content.
One especially important test is whether a client can request another tenant's activity by changing a URL parameter or subscribing to a different WebSocket channel. The server must reject unauthorized access regardless of what the frontend displays.
14. Common Mistakes to Avoid
Several implementation mistakes can make an activity log unreliable or unsafe.
Publishing before committing: The browser may display an event for a transaction that later fails.
Broadcasting to every connected user: Private activities may be disclosed to unauthorized clients.
Using WebSockets as the only data store: Disconnected clients can miss events permanently unless they can retrieve history.
Logging every request: Excessive noise makes meaningful activity difficult to find and increases storage costs.
Storing entire database records: This can expose sensitive fields and produce large event payloads.
Ignoring duplicate delivery: Retries can cause the same activity to appear multiple times.
Using unstable pagination: Ordering only by timestamp can produce inconsistent pages when several events share the same timestamp.
Skipping authorization checks: A valid login does not automatically grant access to every project or organization.
Avoiding these mistakes early makes the system easier to maintain as the application grows.
15. Deployment Considerations
Before deploying the activity service, configure the infrastructure for production workloads.
Use environment variables or a managed secrets system for database credentials. Enable TLS for HTTPS and WSS traffic. Configure connection limits, request timeouts, database pooling, and reverse-proxy WebSocket support.
If multiple FastAPI instances are running, remember that an in-memory connection manager only knows about connections attached to its own process. Use shared messaging infrastructure so events can reach clients connected to different instances.
Configure health checks for the API and background workers. Monitor failed publications and make sure the outbox or queue can be recovered after an outage.
Finally, test deployment behavior under realistic conditions. Restart application instances while users are connected, simulate slow clients, and confirm that historical events remain available after reconnecting.
16. Future Enhancements
Once the basic activity log is working, several improvements can make it more useful.
Advanced filtering: Allow users to filter by actor, event category, project, or date range.
Search: Add full-text search for descriptions and selected metadata fields.
Notifications: Turn important activity events into targeted notifications or email alerts.
Export functionality: Allow authorized users to export filtered activity records for reporting.
Analytics: Aggregate event data to understand product usage and team collaboration patterns.
Audit history: Provide detailed before-and-after values for selected changes, with appropriate redaction and access controls.
Live indicators: Show a small notification badge when new events arrive and let users decide when to load them into the current timeline.
Each feature should be built around a clear requirement. Avoid adding complexity that does not improve the experience or meet an operational need.
Conclusion
Building a real-time activity log is more than connecting a WebSocket to a React component. A dependable implementation combines a well-designed database schema, consistent event definitions, secure authorization, reliable persistence, and a frontend that can handle new events and network interruptions.
React provides the interactive timeline, FastAPI manages application operations and real-time connections, and PostgreSQL stores the permanent activity history. WebSockets deliver immediate updates, while pagination, database indexes, and background workers help the architecture scale.
For a small application, begin with a structured activity table, a reusable event service, a historical REST endpoint, and a secured WebSocket connection. As requirements grow, introduce cursor-based pagination, a transactional outbox, shared messaging, retention policies, and stronger audit controls.
The ultimate goal is to make application activity transparent, useful, and trustworthy. When implemented correctly, a real-time activity log helps users understand changes, gives administrators better visibility, and makes complex web applications easier to maintain.