Type-safe web UIs for SPT mods. Use React, Vue, or any frontend with auto-generated TypeScript from C#.
$ dotnet add package SPTBridgeUI.CoreA 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 buildingOption A: NuGet Package (Recommended)
The easiest way to add BridgeUI to your mod:
<ItemGroup>
<PackageReference Include="SPTBridgeUI.Core" Version="1.0.0" PrivateAssets="all" />
</ItemGroup>
At runtime, you'll also need the BridgeUI mod installed in SPT. Add it as a dependency in your mod's metadata:
public override Dictionary<string, SemanticVersioning.Range>? ModDependencies { get; init; } = new()
{
{ "com.spt.bridgeui", new("~1.0.0") }
};
💡
PrivateAssets="all"prevents the DLL from being copied to your output folder, since the BridgeUI mod provides it at runtime.
Option B: Direct DLL Reference
Alternatively, reference the SDK DLL directly:
<ItemGroup>
<Reference Include="SPT.BridgeUI.Core">
<HintPath>path/to/SPT.BridgeUI.Core.dll</HintPath>
</Reference>
</ItemGroup>
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
└── 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:5173Vite 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.exeNow 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 installationSPT.WebUI.SDK/
├── 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)Compile-time: Add the NuGet package to your project:
<PackageReference Include="SPTBridgeUI.Core" Version="1.0.0" PrivateAssets="all" />Runtime: The BridgeUI mod must be installed in SPT. Declare it as a dependency in your mod's metadata so it loads first.
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/apiapi-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/apiUse --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