A framework for SPT Tarkov mod developers to create web-based UIs using any frontend framework (React, Vue, Svelte, plain HTML/JS) while maintaining type safety with their C# backend.
WebUiModBase and start buildingAdd the NuGet package to your .csproj:
<PackageReference Include="SPTBridgeUI.Core" Version="1.0.4" />
Or via CLI:
dotnet add package SPTBridgeUI.Core
Note: If your project uses <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies> (common for SPT mods), add this target to copy the SDK DLL:
<Target Name="CopyBridgeUI" AfterTargets="Build">
<ItemGroup>
<BridgeUIDll Include="$(NuGetPackageRoot)sptbridgeui.core\**\SPT.BridgeUI.Core.dll" />
</ItemGroup>
<Copy SourceFiles="@(BridgeUIDll)" DestinationFolder="$(OutputPath)" />
</Target>
using System.Reflection;
using SPT.BridgeUI.Core;
using SPT.BridgeUI.Core.Attributes;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Utils;
[Injectable(TypePriority = 0)] // Required for SPT to discover as IHttpListener
public class MyModWebUi : WebUiModBase
{
// URL path for your mod's web UI (e.g., https://127.0.0.1:6969/mymod/)
protected override string BasePath => "/mymod";
public MyModWebUi(ModHelper modHelper, ISptLogger<MyModWebUi> logger)
: base(modHelper.GetAbsolutePathToModFolder(Assembly.GetExecutingAssembly()))
{
//
}
// Define API endpoints with attributes - no boilerplate!
[ApiEndpoint("/mymod/api/data", "GET", Name = "getData", Description = "Yummy data")]
public MyData GetData() => _myService.GetData();
// Providing a `Name` attribute will allow you to automatically generate a type-safe function
// In this example, your frontend will be able to just call `saveData(data)` with full type-safety!
[ApiEndpoint("/mymod/api/data", "POST", Name = "saveData")]
public object SaveData(MyData data)
{
_myService.Save(data);
return new { success = true };
}
}
Place your built frontend in the wwwroot folder of your mod:
YourMod/
āāā YourMod.dll
āāā YourMod.deps.json
āāā SPT.BridgeUI.Core.dll # Included via NuGet
āāā wwwroot/
āāā index.html
āāā styles.css
āāā app.mjs # ā ļø Use .mjs, NOT .js!
ā ļø IMPORTANT: JavaScript files must use
.mjsextension (not.js).
SPT's mod validator rejects mods containing.jsor.tsfiles, treating them as legacy TypeScript mods.
Ensure wwwroot is copied to output:
Add this MSBuild target to your .csproj to copy frontend files during build:
<Target Name="CopyWwwroot" AfterTargets="Build">
<ItemGroup>
<WebAssets Include="$(ProjectDir)wwwroot\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(WebAssets)" DestinationFolder="$(OutputPath)wwwroot\%(RecursiveDir)" />
</Target>
š” This works for both
Microsoft.NET.SdkandMicrosoft.NET.Sdk.Webprojects.
Navigate to https://127.0.0.1:6969/mymod/
Here's a minimal working example:
MyModWebUi.cs:
[Injectable(TypePriority = 0)]
public class MyModWebUi : WebUiModBase
{
protected override string BasePath => "/mymod";
public MyModWebUi(ModHelper modHelper, ISptLogger<MyModWebUi> logger)
: base(modHelper.GetAbsolutePathToModFolder(Assembly.GetExecutingAssembly()))
{ }
[ApiEndpoint("/mymod/api/hello", "GET")]
public object Hello() => new { message = "Hello from my mod!" };
}
wwwroot/index.html:
<!DOCTYPE html>
<html>
<head>
<title>My Mod</title>
</head>
<body>
<h1>My Mod</h1>
<div id="message"></div>
<script type="module" src="app.mjs"></script>
</body>
</html>
wwwroot/app.mjs:
const response = await fetch("/mymod/api/hello");
const data = await response.json();
document.getElementById("message").textContent = data.message;
That's it! ~15 lines of C# + simple HTML/JS = working web UI for your SPT mod.
š” Want type safety? This minimal example uses vanilla JS without type checking. To get end-to-end type safety, use a TypeScript frontend (React, Vue, etc.) with our
spt-bridgeui-typegenCLI to auto-generate typed API clients. See API Client Generation below.
For a better development experience with hot module replacement:
Set an environment variable before starting the SPT server:
# Windows PowerShell
$env:SPT_WEBUI_DEV_URL = "http://localhost:5173"
# Windows CMD
set SPT_WEBUI_DEV_URL=http://localhost:5173
Vite example (vite.config.ts):
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
base: "/mymod/", // Must match your BasePath
build: {
outDir: "../Server/wwwroot",
emptyOutDir: true,
},
server: {
port: 5173,
proxy: {
"/mymod/api": {
target: "https://127.0.0.1:6969",
secure: false, // Accept self-signed cert
changeOrigin: true,
},
},
},
});
# Terminal 1: Frontend dev server
cd frontend
npm run dev
# Terminal 2: SPT Server
cd <SPT_ROOT>
./SPT.Server.exe
Now changes to your frontend update instantly!
The base class for web UI mods. Extend this and configure:
| Property | Type | Default | Description |
|---|---|---|---|
BasePath |
string |
(required) | URL prefix for this mod (e.g., /mymod) |
DistFolder |
string |
"wwwroot" |
Path to frontend files relative to mod folder |
IndexFile |
string |
"index.html" |
Index file for SPA routing |
DevServerUrl |
string? |
env var | Dev server URL for hot reload |
EnableSpaFallback |
bool |
true |
Serve index.html for unknown routes |
CacheDuration |
TimeSpan |
1 hour | Cache duration for static assets |
Mark methods as API endpoints:
[ApiEndpoint(route, method, Name = "functionName", Description = "optional")]
| Parameter | Required | Description |
|---|---|---|
route |
Yes | The full URL path (e.g., /mymod/api/config) |
method |
Yes | HTTP method (GET, POST, PUT, DELETE, PATCH) |
Name |
No | Name for the generated TypeScript function (e.g., getConfig) |
Description |
No | JSDoc comment in generated code; useful for documentation |
š” Tip: Always set
Nameif you want to use the auto-generated API client!
Supported return types:
Task<T> for async operationsvoid / Task (returns { "success": true })Request body:
For POST/PUT/PATCH, the first parameter is deserialized from the request body:
[ApiEndpoint("/mymod/api/save", "POST")]
public object Save(MyData data)
{
// data is automatically deserialized from JSON body
return new { success = true };
}
See the samples/SimpleCounter directory for a complete working example with:
state.jsonSee the demo for yourself:
SimpleCounter/Server/dist to the root of your SPT 4.0 installationSPTBridgeUI/
āāā src/
ā āāā SPT.BridgeUI.Core/ # Core SDK library
ā āāā WebUiModBase.cs # Base class for mods
ā āāā Attributes/
ā ā āāā ApiEndpointAttribute.cs
ā āāā Handlers/
ā ā āāā StaticFileHandler.cs
ā ā āāā DevServerProxy.cs
ā āāā Utils/
ā āāā MimeTypes.cs
āāā samples/
ā āāā SimpleCounter/ # Example mod
ā āāā Server/ # C# backend
ā āāā frontend/ # HTML/JS frontend
āāā README.md
.mjs for JavaScript modules.js or .ts files (SPT rejects these)Each mod using BridgeUI includes its own copy of SPT.BridgeUI.Core.dll (~30KB). This is the simplest and most reliable approach - no separate mod installation required.
Always use [Injectable(TypePriority = 0)] on your WebUiModBase class. This registers it as an IHttpListener with SPT's dependency injection system.
Generate TypeScript types and API clients from your C# code using the CLI tool.
# Install as a global tool
dotnet tool install --global SPTBridgeUI.TypeGen
# Or run from source during development
dotnet run --project src/SPT.BridgeUI.TypeGen -- [options]
# Simple! References are auto-discovered from NuGet cache
spt-bridgeui-typegen --assembly path/to/YourMod.dll --output frontend/src/api
| Option | Alias | Description |
|---|---|---|
--assembly |
-a |
Path to your compiled mod DLL (required) |
--output |
-o |
Output directory for generated files (default: ./types) |
--types-file |
Output filename for types (default: api-types) |
|
--client-file |
Output filename for API client (default: api-client) |
|
--namespace |
-n |
Only export types from this namespace (optional) |
--refs |
-r |
Additional reference paths (usually auto-detected) |
--no-auto-refs |
Disable auto-discovery of NuGet/ASP.NET references | |
--verbose |
-v |
Show detailed output including discovered references |
--watch |
-w |
Watch for assembly changes and auto-regenerate |
Use the [ExportTs] attribute on C# types:
using SPT.BridgeUI.Core.Attributes;
[ExportTs]
public class PlayerStats
{
public int Level { get; set; }
public string Name { get; set; }
public List<string> Skills { get; set; }
}
[ExportTs]
public enum PlayerStatus
{
Online,
Away,
Offline
}
Generates api-types.ts:
export interface PlayerStats {
level: number;
name: string;
skills: string[];
}
export enum PlayerStatus {
Online = 0,
Away = 1,
Offline = 2,
}
The CLI tool automatically generates type-safe API client functions from your [ApiEndpoint] attributes. This means you define your API once in C# and get fully typed TypeScript functions for free!
Step 1: Create request/response models with [ExportTs]:
[ExportTs]
public class CounterState
{
public int Count { get; set; }
public DateTime LastUpdated { get; set; }
}
[ExportTs]
public class AdjustCounterRequest
{
public int Amount { get; set; } = 1;
}
Step 2: Create endpoints with [ApiEndpoint] and Name:
[ApiEndpoint("/counter/api/state", "GET", Name = "getCounterState")]
public CounterState GetState() => _state;
[ApiEndpoint("/counter/api/increment", "POST", Name = "incrementCounter")]
public CounterState Increment(AdjustCounterRequest request)
{
_state.Count += request.Amount;
return _state;
}
[ApiEndpoint("/counter/api/decrement", "POST", Name = "decrementCounter")]
public CounterState Decrement(AdjustCounterRequest request)
{
_state.Count -= request.Amount;
return _state;
}
Step 3: Run the generator:
# References auto-discovered - no --refs needed!
spt-bridgeui-typegen --assembly YourMod.dll --output frontend/src/api
api-types.ts - Your C# models as TypeScript:
export interface CounterState {
count: number;
lastUpdated: string;
}
export interface AdjustCounterRequest {
amount: number;
}
api-client.ts - Type-safe fetch functions:
import type { CounterState, AdjustCounterRequest } from "./api-types";
export async function getCounterState(): Promise<CounterState> {
const response = await fetch("/counter/api/state");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
export async function incrementCounter(
request: AdjustCounterRequest
): Promise<CounterState> {
const response = await fetch("/counter/api/increment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
export async function decrementCounter(
request: AdjustCounterRequest
): Promise<CounterState> {
const response = await fetch("/counter/api/decrement", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
import {
getCounterState,
incrementCounter,
decrementCounter,
} from "./api/api-client";
import type { CounterState } from "./api/api-types";
function Counter() {
const [state, setState] = useState<CounterState | null>(null);
const handleIncrement = async (amount: number) => {
// ā
TypeScript knows `incrementCounter` expects { amount: number }
// ā
TypeScript knows it returns Promise<CounterState>
const newState = await incrementCounter({ amount });
setState(newState);
};
return (
<div>
<span>{state?.count}</span>
<button onClick={() => handleIncrement(1)}>+1</button>
<button onClick={() => handleIncrement(5)}>+5</button>
<button onClick={() => handleDecrement({ amount: 1 })}>-1</button>
</div>
);
}
The magic: Change your C# model, regenerate, and TypeScript immediately catches any mismatches! šÆ
The CLI automatically discovers SPTarkov packages from your NuGet cache and ASP.NET Core from your .NET installation. In most cases, you don't need to specify any --refs:
# That's it! No --refs needed
spt-bridgeui-typegen --assembly dist/MyMod.dll --output frontend/src/api
Use --verbose to see what was auto-discovered:
spt-bridgeui-typegen --assembly dist/MyMod.dll --output frontend/src/api --verbose
# Output:
# š Auto-discovering references...
# š NuGet cache: C:\Users\you\.nuget\packages
# ā sptarkov.server.core (4.0.6)
# ā sptarkov.di (4.0.6)
# ā ASP.NET Core (9.0.11)
# ...
Manual overrides (rarely needed):
# Add additional reference paths if auto-discovery misses something
spt-bridgeui-typegen --assembly dist/MyMod.dll --output frontend/src/api \
--refs "C:/custom/path/to/assemblies"
# Disable auto-discovery entirely
spt-bridgeui-typegen --assembly dist/MyMod.dll --output frontend/src/api \
--no-auto-refs --refs "C:/my/refs"
[ExportTs] attributes[ApiEndpoint]dotnet new templates for React/Vue/VanillaMIT