Blazor ServerVSAAugust 2026 · 6 min read

Inside the Blazor Server Architecture of a Production CRM System

TL;DR

Indotalent CRM is a production Blazor Server application: a Vertical Slice Architecture monolith where each SignalR circuit acts as a user session and scoped DI services hold the user and tenant context. Grid-heavy UI is tamed with server-side paging, MudTable virtualization, and ShouldRender guards.

The Blazor Server Architecture of a production CRM system is not a demo. It handles thousands of users, long-lived sessions, tenant isolation, and a module set — contacts, deals, tasks, reports — that keeps growing. This article opens the hood on how that architecture holds up in production, using Indotalent CRM as the reference codebase.

Everything described here is running code: the circuit model, scoped contexts, the rendering strategy, and the operational lessons that only show up under real workloads.

The Blazor Server Architecture Behind a Production CRM

Indotalent CRM is built as a Vertical Slice Architecture monolith with Blazor Server on the front end, a REST API for integrations, and MudBlazor for the component library. The server project owns the SignalR circuits, dependency injection scoping, authorization, and the database. There is no client-side .NET runtime anywhere: every line of business logic runs on the server.

That choice shapes everything. Because the code runs server-side, features like inline editing, drag-and-drop kanban boards, and live notifications are just C#. The team never writes JavaScript to bridge a gap, and secrets never leave the server.

The Circuit as a User Session

In the CRM, a circuit is treated as a user session, not as a page load. When a user logs in, the circuit opens and a scoped UserContext is initialized from the authentication ticket. Every subsequent interaction — opening a deal, saving a note, changing a filter — reuses that same context.

Circuit lifecycle handlers track who is online. A scoped CircuitHandler flips a presence flag when the circuit opens and clears it when it closes, giving the dashboard a live online count without any polling:

public class TrackUsersCircuitHandler : CircuitHandler
{
    private readonly UserContext _user;

    public TrackUsersCircuitHandler(UserContext user) => _user = user;

    public override Task OnCircuitOpenedAsync(
        Circuit circuit, CancellationToken ct)
    {
        _user.MarkOnline();
        return Task.CompletedTask;
    }

    public override Task OnCircuitClosedAsync(
        Circuit circuit, CancellationToken ct)
    {
        _user.MarkOffline();
        return Task.CompletedTask;
    }
}

builder.Services.AddScoped<UserContext>();
builder.Services.AddScoped<CircuitHandler, TrackUsersCircuitHandler>();

OnCircuitClosedAsync is also the safety net for circuit-level cleanup. If a user closes the tab mid-edit, the scoped services are disposed and the handler marks them offline. One practical detail: the reconnect window means the flag flips a few seconds after the tab closes, which is fine for presence but too slow for any kind of security decision.

Scoped DI Inside the Blazor Server Architecture: User and Tenant Context in One Lifetime

The DI scope is the heart of the CRM's multi-tenant design. UserContext, TenantContext, and the DbContext are all registered as Scoped, so every component in a circuit resolves the same three instances. The tenant is resolved once at login, and from then on every query is filtered by it.

This removes a whole class of bugs. There is no global tenant, no thread-static trick, and no parameter drilling through a component tree that is hundreds of nodes deep. If a component needs the current user or tenant, it injects the context and reads it. New slices can be added without touching existing screens, and a MediatR handler can resolve the same context to stamp audit rows with the actor and the tenant without being told who is calling.

Rendering Strategy Under Real Workloads

A CRM is a grid-heavy application, and grids are where Blazor Server rendering can stall. The strategy is a combination of constraints and tooling:

  • Every grid is server-filtered, sorted, and paged at the data layer
  • MudTable virtualization keeps the DOM small even with tens of thousands of rows
  • ShouldRender guards stop child components from re-rendering on unrelated updates
  • Long operations — imports, exports, report generation — run in a BackgroundService, not in the circuit

The result is that SignalR messages stay small and the server CPU stays dominated by real work rather than wasted rendering.

What Breaks in Production and How We Fixed It

Three lessons stand out from running a real CRM. First, captive dependencies: a Singleton that grabbed a scoped DbContext caused intermittent connection errors until the DI graph was audited and fixed. Second, circuit lifetime: browser tabs left open overnight accumulated memory, so the reconnect window and circuit timeout were tuned to close idle circuits aggressively. Third, data volume: unfiltered queries against a growing deals table got slow, which pushed the team to make every grid server-side by default.

None of these were framework failures. They were architecture failures — a wrong lifetime, a wrong boundary, a wrong assumption — and they were all fixed by applying the principles in this article.

Key Takeaways

  • A production CRM treats the circuit as a user session and the DI scope as its state container
  • CircuitHandler gives you presence tracking and cleanup without polling
  • Scoped UserContext and TenantContext eliminate parameter drilling and tenant bugs
  • Server-side grids with MudTable virtualization keep SignalR traffic low
  • Audit the DI graph — captive scoped services are the number one production bug

FAQ

How does the CRM know a user closed their browser?

The circuit closes and OnCircuitClosedAsync runs, flipping the presence flag. The signal is slightly delayed by the reconnect window, which is acceptable for presence display.

Why not run the CRM as Blazor WebAssembly?

The CRM needs server-side data access, tenant isolation, and protected business logic. Keeping everything server-side with Blazor Server is simpler and more secure for this workload.

Can two users share the same scoped context?

No. A scope belongs to one circuit and one user. Sharing would require a Singleton, which must be stateless or tenant-keyed to avoid cross-user leaks.

How is the REST API involved if Blazor Server does everything?

The Blazor Server app consumes the same REST API that integrations and external clients use, keeping one contract for all consumers of the CRM.

Ready to open the hood of a production CRM?

Every Indotalent product is a complete .NET 10 application built with Blazor Server, Vertical Slice Architecture, and MudBlazor. Complete .NET 10 source code — $21 each.

Explore Products