SPT-BridgeUI Svelte Themes

Spt Bridgeui

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.

BridgeUI

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.

Features

  • šŸŽØ Any Frontend - Use React, Vue, Svelte, or vanilla HTML/JS
  • šŸ”’ End-to-End Type Safety - Auto-generate TypeScript types and API clients from C#
  • šŸ”Œ Simple API Definition - Attribute-based API endpoints with automatic routing
  • šŸ“ Static File Serving - Built-in serving of bundled frontend with SPA support
  • ⚔ Minimal Boilerplate - Extend WebUiModBase and start building
  • šŸ”„ Hot Reload (Experimental) - Dev server proxy for instant updates during development

Quick Start

1. Add the SDK to Your Project

Add 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>

2. Create Your Web UI Class

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 };
    }
}

3. Add Your Frontend

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 .mjs extension (not .js).
SPT's mod validator rejects mods containing .js or .ts files, 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.Sdk and Microsoft.NET.Sdk.Web projects.

4. Access Your UI

Navigate to https://127.0.0.1:6969/mymod/

Complete Example

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-typegen CLI to auto-generate typed API clients. See API Client Generation below.

Development with Hot Reload

For a better development experience with hot module replacement:

1. Set the Dev Server URL

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

2. Configure Your Frontend Dev Server

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,
      },
    },
  },
});

3. Run Both Servers

# 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!

API Reference

WebUiModBase

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

ApiEndpointAttribute

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 Name if you want to use the auto-generated API client!

Supported return types:

  • Any object (serialized as JSON)
  • Task<T> for async operations
  • void / 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 };
}

Sample Project

See the samples/SimpleCounter directory for a complete working example with:

  • C# backend with API endpoints
  • State persistence to state.json
  • React + TypeScript frontend (bundled with Vite)
  • Auto-generated TypeScript types and API client
  • Tarkov-inspired dark theme styling

See the demo for yourself:

  • Install the server mod by copying the contents of SimpleCounter/Server/dist to the root of your SPT 4.0 installation
  • Start your server
  • Go to https://127.0.0.1:6969/counter

Project Structure

SPTBridgeUI/
ā”œā”€ā”€ 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

Important Notes

File Extensions

  • āœ… Use .mjs for JavaScript modules
  • āŒ Do NOT use .js or .ts files (SPT rejects these)

SDK Distribution

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.

Required Attribute

Always use [Injectable(TypePriority = 0)] on your WebUiModBase class. This registers it as an IHttpListener with SPT's dependency injection system.

Type Generation

Generate TypeScript types and API clients from your C# code using the CLI tool.

Installation

# 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]

Usage

# Simple! References are auto-discovered from NuGet cache
spt-bridgeui-typegen --assembly path/to/YourMod.dll --output frontend/src/api

Options

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

Mark Types for Export

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,
}

API Client Generation

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!

Define Your Models and Endpoints

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

Generated Output

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();
}

Use in React with Full Type Safety

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! šŸŽÆ

Reference Paths (Auto-Discovered!)

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"

Roadmap

  • Phase 1: Core Framework - Static serving, SPA fallback, API attributes
  • Phase 2: Type Generation - TypeScript types from [ExportTs] attributes
  • Phase 3: API Client Generation - Typed fetch functions from [ApiEndpoint]
  • Phase 4: Templates - dotnet new templates for React/Vue/Vanilla

License

MIT

Top categories

Loading Svelte Themes