Appearance
Standalone Authentication
Fusion's built-in authentication works well for getting started, but its generics-heavy design adds complexity that many apps don't need. Standalone authentication is an approach where you extract the authentication logic from Fusion packages into your own project, giving you full control and the ability to simplify it for your specific needs.
The Standalone Authentication PR demonstrates this approach using the TodoApp sample. It removes all dependencies on ActualLab.Fusion.Ext.Contracts, ActualLab.Fusion.Ext.Services, and ActualLab.Fusion.Blazor.Authentication, replacing them with local types that are simpler and tailored to the app.
When to Use This Approach
- Your app has matured past the prototype stage and you want to simplify auth
- You need custom fields on
UserorSessionInfo(e.g., tenant ID, roles, avatar URL) - You want to eliminate generic type parameters like
DbUser<TDbUserId> - You want to understand exactly what the auth system does — no black boxes
WARNING
This approach means you take ownership of the auth code. You won't get automatic updates from Fusion NuGet packages for these components. That said, the auth code rarely changes, and owning it gives you the freedom to evolve it with your app.
What Changes
| Before (Fusion packages) | After (embedded) |
|---|---|
IAuth, IAuthBackend from Ext.Contracts / Ext.Services | IUserApi, ISessionBackend, IUserBackend — local interfaces |
DbUser<TDbUserId>, DbSessionInfo<TDbUserId> | Concrete DbUser, DbSessionInfo — no generics |
User from Ext.Contracts (shared across all Fusion apps) | Local User record tailored to your app |
AuthStateProvider from Blazor.Authentication | Local AuthStateProvider you can customize |
ServerAuthHelper from Fusion.Server | Local ServerAuthHelper with only the logic you need |
AuthController from Fusion.Server | Local AuthEndpoints using minimal APIs |
fusionAuth.js from Blazor.Authentication | Local copy in wwwroot/js/ |
The key insight: instead of one monolithic IAuth / IAuthBackend pair doing everything, the standalone approach splits responsibilities into focused services:
IUserApi— client-facing queries (replacesIAuth)ISessionBackend— session lifecycle management (replaces part ofIAuthBackend)IUserBackend— user CRUD (replaces part ofIAuthBackend)
Extracted Components
The standalone auth system is organized into four layers.
Abstractions (shared between client and server)
These types live in your shared/contracts project and define the domain model:
| File | Purpose |
|---|---|
User.cs | Authenticated or guest user with claims and identities. Simplified from Fusion's generic User |
UserId.cs | Value type for user IDs with embedded shard prefix (e.g., "0:abc123") |
UserIdentity.cs | Authentication provider identity (e.g., "GitHub/12345") |
SessionInfo.cs | Session state: timestamps, IP, user agent, auth identity, forced sign-out flag |
IUserApi.cs | Client-facing compute service: GetOwn(), ListOwnSessions(), UpdatePresence(), OnSignOut() |
IUserApi is the only auth service exposed via RPC to the client:
csharp
public interface IUserApi : IComputeService
{
[ComputeMethod(MinCacheDuration = 10)]
Task<User?> GetOwn(Session session, CancellationToken cancellationToken = default);
[ComputeMethod]
Task<ImmutableArray<SessionInfo>> ListOwnSessions(
Session session, CancellationToken cancellationToken = default);
Task UpdatePresence(Session session, CancellationToken cancellationToken = default);
[CommandHandler]
Task OnSignOut(User_SignOut command, CancellationToken cancellationToken = default);
}Backend Services (server-side only)
| File | Purpose |
|---|---|
ISessionBackend.cs | Backend interface for session lifecycle: setup, sign-in, sign-out, presence |
IUserBackend.cs | Backend interface for user CRUD |
SessionBackend.cs | EF Core implementation of ISessionBackend with compute method caching |
UserBackend.cs | EF Core implementation of IUserBackend |
UserApi.cs | Implementation of IUserApi — bridges client calls to backend services |
ServerAuthHelper.cs | Syncs ASP.NET Core auth state to Fusion on each page load |
AuthEndpoints.cs | Minimal API endpoints for /signIn and /signOut (replaces AuthController) |
HttpContextExt.cs | Helpers for reading auth schemas and remote IP from HttpContext |
The backend splits IAuthBackend into two focused interfaces:
csharp
// Session lifecycle
public interface ISessionBackend : IComputeService, IBackendService
{
[ComputeMethod(MinCacheDuration = 10)]
Task<SessionInfo?> GetSessionInfo(Session session, CancellationToken cancellationToken = default);
[ComputeMethod]
Task<ImmutableArray<SessionInfo>> GetUserSessions(
UserId userId, CancellationToken cancellationToken = default);
[CommandHandler]
Task<SessionInfo> OnSetupSession(SessionBackend_SetupSession command, ...);
[CommandHandler]
Task OnSignIn(SessionBackend_SignIn command, ...);
[CommandHandler]
Task OnSignOut(SessionBackend_SignOut command, ...);
Task UpdatePresence(Session session, CancellationToken cancellationToken = default);
}
// User CRUD
public interface IUserBackend : IComputeService, IBackendService
{
[ComputeMethod(MinCacheDuration = 10)]
Task<User?> Get(UserId userId, CancellationToken cancellationToken = default);
[CommandHandler]
Task<User> OnUpsert(UserBackend_Upsert command, ...);
}Database Entities (server-side only)
| File | Purpose |
|---|---|
DbSessionInfo.cs | EF Core entity for sessions — concrete type, no generics |
DbUser.cs | EF Core entity for users with JSON-serialized claims |
DbUserIdentity.cs | EF Core entity for user-identity associations |
These replace Fusion's generic DbUser<TDbUserId>, DbSessionInfo<TDbUserId>, and DbUserIdentity<TDbUserId>. Without generics, the code is straightforward:
csharp
[Table("Users")]
[Index(nameof(Name))]
public class DbUser : IHasId<string>, IHasVersion<long>
{
[Key] public string Id { get; set; } = "";
[ConcurrencyCheck] public long Version { get; set; }
public string Name { get; set; } = "";
public string ClaimsJson { get; set; } = "{}";
public List<DbUserIdentity> Identities { get; set; } = new();
// ToModel() / UpdateFrom() for domain model conversion
}UI Components (Blazor client)
| File | Purpose |
|---|---|
AuthStateProvider.cs | Blazor AuthenticationStateProvider backed by IUserApi with reactive updates |
AuthState.cs | Auth state model with local User and forced sign-out flag |
CascadingAuthState.razor | Cascades auth state to child components; handles forced sign-out |
ClientAuthHelper.cs | Client-side helper for sign-in/sign-out via JS interop |
PresenceReporter.cs | Background worker reporting user presence every 3 minutes |
fusionAuth.js | JavaScript module for popup-based auth flows |
Service Registration
Here's how the embedded auth services are registered in Program.cs:
csharp
var fusion = services.AddFusion();
// Backend services (server-side)
fusion.AddServer<ISessionBackend, SessionBackend>();
fusion.AddServer<IUserBackend, UserBackend>();
// Client-facing API (exposed via RPC)
fusion.AddServer<IUserApi, UserApi>();
// ServerAuthHelper for syncing ASP.NET Core auth to Fusion
services.AddSingleton(new ServerAuthHelper.Options { /* ... */ });
services.AddScoped<ServerAuthHelper>();
// Auth endpoints (replaces AuthController)
services.AddSingleton(new AuthEndpoints.Options { /* ... */ });
services.AddSingleton<AuthEndpoints>();
// Blazor auth integration
services.AddScoped<AuthStateProvider>();
services.AddScoped<AuthenticationStateProvider>(c => c.GetRequiredService<AuthStateProvider>());
services.AddScoped<ClientAuthHelper>();
services.AddScoped<PresenceReporter>();Compare this to the Fusion-package approach:
csharp
// Before: Fusion packages handle everything
fusion.AddDbAuthService<AppDbContext, string>();
fusionServer.AddAuthEndpoints();The standalone version is more verbose, but every service is yours to inspect, modify, and debug.
Migration Steps
To set up standalone authentication in your own app:
Copy the contract types (
User,UserId,UserIdentity,SessionInfo,IUserApi) into your shared/abstractions project. Adjust namespaces to match your app.Copy the backend services (
ISessionBackend,IUserBackend,SessionBackend,UserBackend,UserApi,ServerAuthHelper,AuthEndpoints,HttpContextExt) into your server project.Copy the DB entities (
DbUser,DbSessionInfo,DbUserIdentity) and update yourDbContextto use them instead of Fusion's generic versions.Copy the UI components (
AuthStateProvider,AuthState,CascadingAuthState,ClientAuthHelper,PresenceReporter,fusionAuth.js) into your Blazor project.Remove NuGet references to
ActualLab.Fusion.Ext.Contracts,ActualLab.Fusion.Ext.Services, andActualLab.Fusion.Blazor.Authentication.Update service registration in
Program.csas shown above.Update
_HostPage.razorto use the localServerAuthHelperinstead of the one fromActualLab.Fusion.Server.Update imports in
_Imports.razorand other files to reference your local namespaces instead ofActualLab.Fusion.Authentication.
Use AI to Help
You can use AI tools like Claude to accelerate this migration. Point it at the Standalone Authentication PR and your own codebase, and ask it to adapt the extracted types to your app's needs.
Simplification Opportunities
Once the code is in your project, you can simplify it further:
- Remove unused features: If you don't need multi-session management or presence tracking, delete
ListOwnSessions,UpdatePresence, andPresenceReporter - Flatten the model: Merge
SessionInfofields directly into your session entity if you don't need the domain/entity separation - Simplify user IDs: If you don't need sharding, replace
UserIdwith a plainstring - Add custom fields: Add
TenantId,AvatarUrl,Roles, or any other fields directly toUserandDbUser - Change auth flow: Replace popup-based auth with redirect-based, or add custom flows like magic links or API keys
- Simplify serialization: Remove
MessagePackorMemoryPackattributes if you only use one serialization format
