Blazor Development Conventions
Apply these practices when writing, reviewing, or refactoring Blazor applications.
Code Style and Structure
- Write idiomatic Blazor and C# code that follows established .NET conventions
- Use Razor Components (
.razor files) for all component-based UI
- For complex components with injected services or significant logic, use the code-behind with
@inherits pattern: define a base class (e.g., DeviceDetailsBase : NexusPageComponentBase) and reference it with @inherits DeviceDetailsBase in the .razor file
- Keep small, presentational components inline with
@code { } blocks
- Create custom base components for shared concerns across pages — e.g., a
PageComponentBase that provides common injections (NavigationManager, AuthenticationState, ISnackbar) and helper methods (IsAuthenticatedAsync(), RedirectToLogin())
- Use
async/await for all non-blocking UI operations — never block on Task.Result or .Wait() inside components, as this deadlocks the synchronization context
Naming Conventions
- PascalCase for components, methods, properties, public members, and component parameters
- camelCase for private fields and local variables
- Prefix private fields with underscore (
_orderService)
- Prefix interfaces with
I (IOrderService)
- Name component files to match the component class (
OrderList.razor for OrderList)
Component Lifecycle
Blazor components follow a well-defined lifecycle. Understanding when each method runs prevents redundant work and subtle bugs.
| Method |
When it runs |
Typical use |
SetParametersAsync |
On every parameter update, before other lifecycle methods |
Low-level parameter interception (rarely overridden directly) |
OnInitialized / OnInitializedAsync |
Once when the component first initializes |
Initial data loading, one-time setup |
OnParametersSet / OnParametersSetAsync |
After parameter values are set (on init and every update) |
Reacting to parameter changes, recomputing derived state |
OnAfterRender / OnAfterRenderAsync |
After each render, with firstRender flag |
JS interop calls, DOM measurements, third-party library init |
Dispose / DisposeAsync |
When the component is removed from the render tree |
Unsubscribe from events, cancel CancellationTokenSource, release resources |
- Implement
IDisposable or IAsyncDisposable to clean up event subscriptions and cancellation tokens — leaked subscriptions cause memory leaks and ghost updates
- Use
firstRender in OnAfterRenderAsync to run one-time JS interop or DOM setup
- Avoid calling
StateHasChanged() inside OnInitializedAsync or OnParametersSetAsync — Blazor calls it automatically after these methods complete
Data Binding
- Use
@bind for two-way binding to input elements: <input @bind="searchText" />
- Use
@bind:event to control when the binding updates (e.g., @bind:event="oninput" for real-time updates vs the default onchange)
- Use
@bind:after to run logic after a bound value changes without writing a manual setter
- For component parameters, expose a
Value / ValueChanged pair to support @bind-Value from parent components:
[Parameter] public string Value { get; set; } = string.Empty;
[Parameter] public EventCallback<string> ValueChanged { get; set; }
Component Communication
- Parent to child: pass data via
[Parameter] properties
- Child to parent: use
EventCallback<T> to notify the parent of changes
- Deep tree sharing: use
[CascadingParameter] with <CascadingValue> to avoid prop-drilling through intermediate components
- Keep
EventCallback over Action/Func for event handling — EventCallback automatically triggers a re-render on the correct component and handles async delegates
Dependency Injection
In code-behind base classes, use [Inject] on properties. In .razor files, use @inject:
// Code-behind pattern (preferred for complex components)
[Inject] protected GatewayService GatewayService { get; set; }
[Inject] protected IDialogService DialogService { get; set; }
[Inject] protected ISnackbar SnackBarService { get; set; }
[Inject] protected NavigationManager Navigation { get; set; }
- Register services with the correct scope:
Scoped for per-circuit (Server) or per-user (WASM) services, Singleton for shared state and hub connections, Transient for stateless utilities
- Avoid injecting
Scoped services into Singleton services — this captures a stale instance (the captive dependency problem)
Separation of Concerns
- Components handle UI rendering and user interaction only
- Extract business logic into injectable services
- Use dedicated model or DTO classes for data transfer — do not pass entity or database models directly to components
- Isolate HTTP calls in typed service classes rather than calling
HttpClient directly from components
Modern C# Features
Leverage current C# language features for clarity and conciseness:
- Records for immutable data models and DTOs (
record OrderDto(int Id, string Name, decimal Total);)
- Pattern matching in event handlers and conditional rendering logic
- Global usings in
_Imports.razor for commonly used namespaces
- File-scoped namespaces in code-behind files
- Nullable reference types — enable project-wide and handle nullability explicitly in component parameters and service returns
Error Handling and Validation
Page and API Error Handling
- Wrap data-loading calls in try-catch within lifecycle methods and present user-friendly feedback via
ISnackbar:
try
{
await LoadDataAsync(cancellationToken);
}
catch (UnauthorizedAccessException)
{
RedirectToLogin();
}
catch (Exception ex)
{
SnackBarService.Add($"Failed to load data: {ex.Message}", Severity.Error);
}
- Catch
NavigationException separately — it is expected in Blazor Server when navigation aborts rendering
- Use
ErrorBoundary for catching unhandled rendering exceptions when appropriate
- Configure a global error handler page with
app.UseExceptionHandler("/Error")
Form and Input Patterns
- Use
EditForm with DataAnnotationsValidator for form-heavy scenarios with validation
- For search/filter UIs, use component library inputs (e.g.,
MudTextField, MudSelect) with debouncing:
<MudTextField DebounceInterval="500"
@bind-Value="SearchQuery"
Placeholder="Search..." />
Performance Optimization
Blazor re-renders components more often than you might expect. These patterns keep the UI responsive.
Reduce Unnecessary Renders
- Override
ShouldRender() and return false when the component output has not changed — this skips the entire diff and DOM patch cycle
- Use
@key on list items so Blazor can match existing elements by identity instead of recreating them on every render
- Mark child components with
@rendermode appropriately (Server vs WebAssembly vs Static SSR) based on interactivity needs
- Avoid allocating new objects (lambdas, collections, anonymous types) inside render logic — each allocation looks like a "change" to the diffing engine
Server vs WebAssembly Considerations
| Concern |
Blazor Server |
Blazor WebAssembly |
| Initial load |
Fast — only a small SignalR connection is established |
Slower — the .NET runtime and app assemblies download to the browser |
| Latency |
Every UI interaction is a SignalR round-trip |
No round-trip for UI logic; HTTP calls only for data |
| Scalability |
Each user holds a server circuit and memory |
Client-side — the server only serves APIs |
| Offline support |
None — requires a persistent connection |
Possible with service workers and local caching |
| Data access |
Direct access to server-side resources (databases, file system) |
Must go through HTTP APIs |
- On Server, minimize SignalR payload by keeping component state small and avoiding large objects in memory per circuit
- On WebAssembly, use lazy loading (
<Router>) to defer assembly downloads and reduce initial payload
- Use
IAsyncEnumerable streaming with Server to progressively render large datasets without loading everything into memory
Async Methods for API Calls
- Always use async methods (
GetFromJsonAsync, PostAsJsonAsync) for HTTP operations
- Provide a loading indicator while awaiting data to avoid a blank or stale UI
Efficient Event Handling
- Prefer
EventCallback<T> over raw delegates for component events — they batch render updates correctly
- Debounce high-frequency events (e.g., search-as-you-type) to avoid flooding the server or triggering excessive re-renders
Caching Strategies
Choose a caching approach based on the hosting model:
Blazor Server
- Use
IMemoryCache for fast, in-process caching of frequently accessed data
- Use a distributed cache (Redis, SQL Server) when running multiple server instances so all circuits share the same cached data
- Register caches as
Singleton so they survive across circuit lifetimes
Blazor WebAssembly
- Use
localStorage for persistent, cross-session data (user preferences, tokens)
- Use
sessionStorage for per-tab, session-scoped data
- Consider
Blazored.LocalStorage or Blazored.SessionStorage packages for a typed, async-friendly API
General Caching
- Cache API call responses when the data does not change frequently, and invalidate on relevant mutations
- Use cache-aside pattern: check cache first, fetch from source on miss, store result
State Management
Choose the right pattern based on complexity:
Global App State (StateContainer Pattern)
Use a scoped StateContainer service for app-wide state like theme, user preferences, or shared UI settings. Expose state via properties and notify components via events:
public class StateContainer
{
public bool IsDarkMode { get; private set; }
public event Action? OnThemeChange;
public void UseDarkMode(bool darkMode)
{
IsDarkMode = darkMode;
OnThemeChange?.Invoke();
}
}
Register as Scoped, inject into components, subscribe to events, call StateHasChanged() in the handler, and unsubscribe in Dispose.
Feature State (DataStateService Pattern)
For feature-specific state (device lists, search filters, real-time updates), create a dedicated scoped service that combines data caching with event notifications:
public class DataStateService : IDataStateService, IDisposable
{
public List<DeviceType> DeviceTypes { get; private set; } = [];
public DeviceSearchState SearchState { get; } = new();
public event Action? DeviceStateUpdated;
// ... fetch, cache, notify
}
Component-Level Persistence
Use [PersistentState] (.NET 10+) to automatically serialize/deserialize component state across navigations:
[PersistentState]
public List<Device>? Devices { get; set; }
Complex State
For larger applications where many unrelated components need coordinated state, consider Fluxor or Blazor-State for Redux-style unidirectional data flow.
WebAssembly Persistence
- Use
localStorage / sessionStorage (via Blazored.LocalStorage or similar) to persist state across page reloads in WASM apps
UI Component Libraries
When using a component library like MudBlazor, follow its conventions consistently:
- Use library layout components (
MudLayout, MudAppBar, MudDrawer, MudMainContent) for the application shell
- Use
IDialogService for modal dialogs and ISnackbar for toast notifications instead of custom implementations
- Use library data components (
MudTable, MudSelect, MudTextField) with their built-in features (sorting, paging, debouncing)
- Override library styles through CSS variables and a centralized override stylesheet rather than per-component hacks
- Wrap the app root with the library's theme provider for consistent theming and dark mode support
Real-Time Communication
For real-time features, use SignalR:
- Create a dedicated hub connection service registered as
Singleton for connection lifecycle management
- Subscribe to hub events in
DataStateService or a similar scoped service, and propagate changes via events that components subscribe to
- Handle connection drops gracefully with reconnection logic
API Integration
- Create a gateway service (API facade) that encapsulates all HTTP calls behind a typed interface — components inject this service rather than calling
HttpClient directly
- Use
IHttpClientFactory for HTTP client management with named or typed clients
- Handle errors with try-catch and surface user-friendly feedback via
ISnackbar rather than letting exceptions propagate to the UI
- Use
CancellationToken with HTTP calls so navigating away cancels in-flight requests
Streaming and Rendering
- Use
@attribute [StreamRendering] on pages for improved perceived performance — content renders progressively as data loads
- Show loading indicators while async data is pending
Testing
- Use bUnit for component-level testing — it renders components in a test context and lets you assert on markup, parameters, and events
- Use xUnit as the test framework and NSubstitute for mocking injected services
- Test component rendering, parameter changes, event callbacks, and lifecycle behavior
- Mock
NavigationManager, HttpClient, and custom services via bUnit's dependency injection
Security and Authentication
- Use Microsoft Entra ID (via
Microsoft.Identity.Web) for enterprise authentication, or ASP.NET Identity / JWT for other scenarios
- Use
AddCascadingAuthenticationState() to make auth state available throughout the component tree
- Access auth state in base components via
[CascadingParameter] protected Task<AuthenticationState>? AuthenticationStateTask
- Protect pages with
[Authorize] attribute and use <AuthorizeView> / <AuthorizeRouteView> for conditional rendering
- Use a
BearerTokenHandler to automatically attach JWT tokens to downstream API calls
- Never store secrets or sensitive tokens in WebAssembly client code — it runs entirely in the browser and is fully inspectable
1---2name: blazor-conventions3description: Best practices and conventions for Blazor application development including Razor component structure, component lifecycle methods (OnInitializedAsync, OnParametersSetAsync), data binding, state management patterns, Server vs WebAssembly optimization, caching strategies, and error handling with ErrorBoundary. Use this skill whenever writing, reviewing, or refactoring Blazor components or pages — including when the user mentions Razor components, Blazor Server, Blazor WebAssembly (WASM), render optimization, cascading parameters, component parameters, or Blazor forms and validation, even if they do not explicitly say "Blazor best practices."4---56# Blazor Development Conventions78Apply these practices when writing, reviewing, or refactoring Blazor applications.910## Code Style and Structure1112- Write idiomatic Blazor and C# code that follows established .NET conventions13- Use Razor Components (`.razor` files) for all component-based UI14- For complex components with injected services or significant logic, use the **code-behind with `@inherits`** pattern: define a base class (e.g., `DeviceDetailsBase : NexusPageComponentBase`) and reference it with `@inherits DeviceDetailsBase` in the `.razor` file15- Keep small, presentational components inline with `@code { }` blocks16- Create **custom base components** for shared concerns across pages — e.g., a `PageComponentBase` that provides common injections (`NavigationManager`, `AuthenticationState`, `ISnackbar`) and helper methods (`IsAuthenticatedAsync()`, `RedirectToLogin()`)17- Use `async`/`await` for all non-blocking UI operations — never block on `Task.Result` or `.Wait()` inside components, as this deadlocks the synchronization context1819## Naming Conventions2021- **PascalCase** for components, methods, properties, public members, and component parameters22- **camelCase** for private fields and local variables23- Prefix private fields with underscore (`_orderService`)24- Prefix interfaces with `I` (`IOrderService`)25- Name component files to match the component class (`OrderList.razor` for `OrderList`)2627## Component Lifecycle2829Blazor components follow a well-defined lifecycle. Understanding when each method runs prevents redundant work and subtle bugs.3031| Method | When it runs | Typical use |32|---|---|---|33| `SetParametersAsync` | On every parameter update, before other lifecycle methods | Low-level parameter interception (rarely overridden directly) |34| `OnInitialized` / `OnInitializedAsync` | Once when the component first initializes | Initial data loading, one-time setup |35| `OnParametersSet` / `OnParametersSetAsync` | After parameter values are set (on init and every update) | Reacting to parameter changes, recomputing derived state |36| `OnAfterRender` / `OnAfterRenderAsync` | After each render, with `firstRender` flag | JS interop calls, DOM measurements, third-party library init |37| `Dispose` / `DisposeAsync` | When the component is removed from the render tree | Unsubscribe from events, cancel `CancellationTokenSource`, release resources |3839- Implement `IDisposable` or `IAsyncDisposable` to clean up event subscriptions and cancellation tokens — leaked subscriptions cause memory leaks and ghost updates40- Use `firstRender` in `OnAfterRenderAsync` to run one-time JS interop or DOM setup41- Avoid calling `StateHasChanged()` inside `OnInitializedAsync` or `OnParametersSetAsync` — Blazor calls it automatically after these methods complete4243## Data Binding4445- Use `@bind` for two-way binding to input elements: `<input @bind="searchText" />`46- Use `@bind:event` to control when the binding updates (e.g., `@bind:event="oninput"` for real-time updates vs the default `onchange`)47- Use `@bind:after` to run logic after a bound value changes without writing a manual setter48- For component parameters, expose a `Value` / `ValueChanged` pair to support `@bind-Value` from parent components:4950```razor51[Parameter] public string Value { get; set; } = string.Empty;52[Parameter] public EventCallback<string> ValueChanged { get; set; }53```5455## Component Communication5657- **Parent to child**: pass data via `[Parameter]` properties58- **Child to parent**: use `EventCallback<T>` to notify the parent of changes59- **Deep tree sharing**: use `[CascadingParameter]` with `<CascadingValue>` to avoid prop-drilling through intermediate components60- Keep `EventCallback` over `Action`/`Func` for event handling — `EventCallback` automatically triggers a re-render on the correct component and handles async delegates6162## Dependency Injection6364In code-behind base classes, use `[Inject]` on properties. In `.razor` files, use `@inject`:6566```csharp67// Code-behind pattern (preferred for complex components)68[Inject] protected GatewayService GatewayService { get; set; }69[Inject] protected IDialogService DialogService { get; set; }70[Inject] protected ISnackbar SnackBarService { get; set; }71[Inject] protected NavigationManager Navigation { get; set; }72```7374- Register services with the correct scope: `Scoped` for per-circuit (Server) or per-user (WASM) services, `Singleton` for shared state and hub connections, `Transient` for stateless utilities75- Avoid injecting `Scoped` services into `Singleton` services — this captures a stale instance (the captive dependency problem)7677## Separation of Concerns7879- Components handle UI rendering and user interaction only80- Extract business logic into injectable services81- Use dedicated model or DTO classes for data transfer — do not pass entity or database models directly to components82- Isolate HTTP calls in typed service classes rather than calling `HttpClient` directly from components8384## Modern C# Features8586Leverage current C# language features for clarity and conciseness:8788- **Records** for immutable data models and DTOs (`record OrderDto(int Id, string Name, decimal Total);`)89- **Pattern matching** in event handlers and conditional rendering logic90- **Global usings** in `_Imports.razor` for commonly used namespaces91- **File-scoped namespaces** in code-behind files92- **Nullable reference types** — enable project-wide and handle nullability explicitly in component parameters and service returns9394## Error Handling and Validation9596### Page and API Error Handling9798- Wrap data-loading calls in try-catch within lifecycle methods and present user-friendly feedback via `ISnackbar`:99100```csharp101try102{103 await LoadDataAsync(cancellationToken);104}105catch (UnauthorizedAccessException)106{107 RedirectToLogin();108}109catch (Exception ex)110{111 SnackBarService.Add($"Failed to load data: {ex.Message}", Severity.Error);112}113```114115- Catch `NavigationException` separately — it is expected in Blazor Server when navigation aborts rendering116- Use `ErrorBoundary` for catching unhandled rendering exceptions when appropriate117- Configure a global error handler page with `app.UseExceptionHandler("/Error")`118119### Form and Input Patterns120121- Use `EditForm` with `DataAnnotationsValidator` for form-heavy scenarios with validation122- For search/filter UIs, use component library inputs (e.g., `MudTextField`, `MudSelect`) with debouncing:123124```razor125<MudTextField DebounceInterval="500"126 OnDebounceIntervalElapsed="ApplyFilters"127 @bind-Value="SearchQuery"128 Placeholder="Search..." />129```130131## Performance Optimization132133Blazor re-renders components more often than you might expect. These patterns keep the UI responsive.134135### Reduce Unnecessary Renders136137- Override `ShouldRender()` and return `false` when the component output has not changed — this skips the entire diff and DOM patch cycle138- Use `@key` on list items so Blazor can match existing elements by identity instead of recreating them on every render139- Mark child components with `@rendermode` appropriately (Server vs WebAssembly vs Static SSR) based on interactivity needs140- Avoid allocating new objects (lambdas, collections, anonymous types) inside render logic — each allocation looks like a "change" to the diffing engine141142### Server vs WebAssembly Considerations143144| Concern | Blazor Server | Blazor WebAssembly |145|---|---|---|146| Initial load | Fast — only a small SignalR connection is established | Slower — the .NET runtime and app assemblies download to the browser |147| Latency | Every UI interaction is a SignalR round-trip | No round-trip for UI logic; HTTP calls only for data |148| Scalability | Each user holds a server circuit and memory | Client-side — the server only serves APIs |149| Offline support | None — requires a persistent connection | Possible with service workers and local caching |150| Data access | Direct access to server-side resources (databases, file system) | Must go through HTTP APIs |151152- On Server, minimize SignalR payload by keeping component state small and avoiding large objects in memory per circuit153- On WebAssembly, use lazy loading (`<Router OnNavigateAsync="OnNavigateAsync">`) to defer assembly downloads and reduce initial payload154- Use `IAsyncEnumerable` streaming with Server to progressively render large datasets without loading everything into memory155156### Async Methods for API Calls157158- Always use async methods (`GetFromJsonAsync`, `PostAsJsonAsync`) for HTTP operations159- Provide a loading indicator while awaiting data to avoid a blank or stale UI160161### Efficient Event Handling162163- Prefer `EventCallback<T>` over raw delegates for component events — they batch render updates correctly164- Debounce high-frequency events (e.g., search-as-you-type) to avoid flooding the server or triggering excessive re-renders165166## Caching Strategies167168Choose a caching approach based on the hosting model:169170### Blazor Server171172- Use `IMemoryCache` for fast, in-process caching of frequently accessed data173- Use a distributed cache (Redis, SQL Server) when running multiple server instances so all circuits share the same cached data174- Register caches as `Singleton` so they survive across circuit lifetimes175176### Blazor WebAssembly177178- Use `localStorage` for persistent, cross-session data (user preferences, tokens)179- Use `sessionStorage` for per-tab, session-scoped data180- Consider `Blazored.LocalStorage` or `Blazored.SessionStorage` packages for a typed, async-friendly API181182### General Caching183184- Cache API call responses when the data does not change frequently, and invalidate on relevant mutations185- Use cache-aside pattern: check cache first, fetch from source on miss, store result186187## State Management188189Choose the right pattern based on complexity:190191### Global App State (StateContainer Pattern)192193Use a scoped `StateContainer` service for app-wide state like theme, user preferences, or shared UI settings. Expose state via properties and notify components via events:194195```csharp196public class StateContainer197{198 public bool IsDarkMode { get; private set; }199 public event Action? OnThemeChange;200201 public void UseDarkMode(bool darkMode)202 {203 IsDarkMode = darkMode;204 OnThemeChange?.Invoke();205 }206}207```208209Register as `Scoped`, inject into components, subscribe to events, call `StateHasChanged()` in the handler, and unsubscribe in `Dispose`.210211### Feature State (DataStateService Pattern)212213For feature-specific state (device lists, search filters, real-time updates), create a dedicated scoped service that combines data caching with event notifications:214215```csharp216public class DataStateService : IDataStateService, IDisposable217{218 public List<DeviceType> DeviceTypes { get; private set; } = [];219 public DeviceSearchState SearchState { get; } = new();220 public event Action? DeviceStateUpdated;221 // ... fetch, cache, notify222}223```224225### Component-Level Persistence226227Use `[PersistentState]` (.NET 10+) to automatically serialize/deserialize component state across navigations:228229```csharp230[PersistentState]231public List<Device>? Devices { get; set; }232```233234### Complex State235236For larger applications where many unrelated components need coordinated state, consider Fluxor or Blazor-State for Redux-style unidirectional data flow.237238### WebAssembly Persistence239240- Use `localStorage` / `sessionStorage` (via `Blazored.LocalStorage` or similar) to persist state across page reloads in WASM apps241242## UI Component Libraries243244When using a component library like **MudBlazor**, follow its conventions consistently:245246- Use library layout components (`MudLayout`, `MudAppBar`, `MudDrawer`, `MudMainContent`) for the application shell247- Use `IDialogService` for modal dialogs and `ISnackbar` for toast notifications instead of custom implementations248- Use library data components (`MudTable`, `MudSelect`, `MudTextField`) with their built-in features (sorting, paging, debouncing)249- Override library styles through **CSS variables** and a centralized override stylesheet rather than per-component hacks250- Wrap the app root with the library's theme provider for consistent theming and dark mode support251252## Real-Time Communication253254For real-time features, use **SignalR**:255256- Create a dedicated hub connection service registered as `Singleton` for connection lifecycle management257- Subscribe to hub events in `DataStateService` or a similar scoped service, and propagate changes via events that components subscribe to258- Handle connection drops gracefully with reconnection logic259260## API Integration261262- Create a **gateway service** (API facade) that encapsulates all HTTP calls behind a typed interface — components inject this service rather than calling `HttpClient` directly263- Use `IHttpClientFactory` for HTTP client management with named or typed clients264- Handle errors with try-catch and surface user-friendly feedback via `ISnackbar` rather than letting exceptions propagate to the UI265- Use `CancellationToken` with HTTP calls so navigating away cancels in-flight requests266267## Streaming and Rendering268269- Use `@attribute [StreamRendering]` on pages for improved perceived performance — content renders progressively as data loads270- Show loading indicators while async data is pending271272## Testing273274- Use **bUnit** for component-level testing — it renders components in a test context and lets you assert on markup, parameters, and events275- Use **xUnit** as the test framework and **NSubstitute** for mocking injected services276- Test component rendering, parameter changes, event callbacks, and lifecycle behavior277- Mock `NavigationManager`, `HttpClient`, and custom services via bUnit's dependency injection278279## Security and Authentication280281- Use **Microsoft Entra ID** (via `Microsoft.Identity.Web`) for enterprise authentication, or ASP.NET Identity / JWT for other scenarios282- Use `AddCascadingAuthenticationState()` to make auth state available throughout the component tree283- Access auth state in base components via `[CascadingParameter] protected Task<AuthenticationState>? AuthenticationStateTask`284- Protect pages with `[Authorize]` attribute and use `<AuthorizeView>` / `<AuthorizeRouteView>` for conditional rendering285- Use a `BearerTokenHandler` to automatically attach JWT tokens to downstream API calls286- Never store secrets or sensitive tokens in WebAssembly client code — it runs entirely in the browser and is fully inspectable