S-UI Inbound and Outbound Registry Architecture Explained

S-UI manages proxy configurations through a dual-registry system—Inbounds for traffic listeners and Outbounds for destinations—using a layered Go architecture that separates database persistence, service logic, and runtime synchronization with the Sing-Box core.

S-UI is a web-based management interface for the Sing-Box proxy platform. The application implements a robust registry system to handle dynamic proxy configurations without requiring process restarts. This article examines the architecture of S-UI's inbound and outbound registry system based on the actual source code in the alireza0/s-ui repository.

Layered System Architecture

The registry implementation follows a strict separation of concerns across three distinct layers. The Persistence Layer stores definitions in SQLite using GORM models defined in database/model/inbounds.go and database/model/outbounds.go. The Service Layer provides CRUD operations, JSON marshaling for the Sing-Box core, and user-link enrichment through service/inbounds.go and service/outbounds.go. The Runtime Integration Layer maintains live synchronization with the running Sing-Box instance via corePtr methods including AddInbound, RemoveInbound, AddOutbound, and RemoveOutbound.

Inbound Registry Implementation

Database Models

Inbound definitions persist through the Inbound struct in database/model/inbounds.go. The schema captures fixed fields alongside flexible protocol-specific options:

type Inbound struct {
    Id      uint   `gorm:"primaryKey;autoIncrement"`
    Type    string
    Tag     string `gorm:"unique"`
    TlsId   uint   // FK to TLS table
    Tls     *Tls   // optional TLS configuration
    Addrs   json.RawMessage
    OutJson json.RawMessage
    Options json.RawMessage // all other fields
}

The Options field stores dynamic configuration—such as listen addresses and shadowtls versions—allowing the system to support new protocols without database migrations. Both Inbound and Outbound models implement custom UnmarshalJSON and MarshalJSON methods to partition fixed schema fields from dynamic JSON payloads.

Service Layer Operations

The InboundService in service/inbounds.go coordinates between the database and the running core. Read APIs including Get, GetAll, and FromIds retrieve raw database rows or enriched maps containing id, type, tag, tls_id, listen, and listen_port fields.

The Save method handles lifecycle states "new", "edit", and "del" through a specific sequence:

  1. Parse the inbound payload using UnmarshalJSON
  2. For running cores, remove the old tag and invoke corePtr.AddInbound
  3. Persist the record via tx.Save
  4. Update related client tables through UpdateClientsOnInboundAdd and UpdateLinksByInboundChange

User Enrichment Process

When an inbound supports user authentication, the service triggers user enrichment through helper methods addUsers, initUsers, and fetchUsers. The hasUser function determines protocol capability, then queries the clients table to embed user JSON blobs under the users key in the final configuration. This mechanism supports protocols like VLESS, VMESS, and Shadowsocks.

Graceful Restart Mechanism

The RestartInbounds function implements hot-reload functionality without process termination. It removes existing inbounds via corePtr.RemoveInbound, closes active connections using ConnTracker().CloseConnByInbound, regenerates JSON including refreshed user data, and re-adds the configuration through corePtr.AddInbound.

Outbound Registry Structure

Outbound management mirrors the inbound pattern but excludes user-related complexity. The Outbound struct in database/model/outbounds.go maintains a simplified schema:

type Outbound struct {
    Id      uint   `gorm:"primaryKey;autoIncrement"`
    Type    string
    Tag     string `gorm:"unique"`
    Options json.RawMessage // protocol-specific options
}

The OutboundService in service/outbounds.go provides equivalent CRUD functionality. Since outbounds function purely as destination definitions, they never require user enrichment logic. The Save method handles all three operation types—"new", "edit", and "del"—by marshaling the outbound configuration, synchronizing with the core via corePtr.AddOutbound or corePtr.RemoveOutbound, and persisting changes to the database.

Runtime Synchronization Flow

The complete interaction flow demonstrates how changes propagate from API to execution:

  1. API Handling: api/apiHandler.go receives HTTP requests and delegates to InboundService.Save or OutboundService.Save
  2. JSON Generation: Services generate complete Sing-Box compatible JSON through GetAllConfig, including populated TLS objects and user entries
  3. Core Synchronization: The service layer invokes corePtr methods to apply changes to the running Sing-Box instance immediately
  4. Database Persistence: Canonical representations store in SQLite via GORM models

This hot-reload capability ensures runtime state reflects configuration changes without service interruption.

Practical Configuration Examples

Adding an HTTP Inbound with TLS

payload := []byte(`{
    "type":"http",
    "tag":"http_in",
    "tls_id":1,
    "listen":"0.0.0.0",
    "listen_port":8080
}`)
err := inboundService.Save(tx, "new", payload, "1,2", "my.example.com")

This call stores the inbound, associates TLS configuration, registers it with the Sing-Box core, and links client IDs 1 and 2 as authorized users.

Editing an Outbound Configuration

payload := []byte(`{
    "id":10,
    "type":"shadowsocks",
    "tag":"ss_out",
    "method":"aes-256-gcm",
    "address":"1.2.3.4",
    "port":8388
}`)
err := outboundService.Save(tx, "edit", payload)

The service removes the previous tag, applies the new Shadowsocks configuration to the core, and updates the database record atomically.

Bulk Restarting Inbounds

ids := []uint{3, 5, 7}
err := inboundService.RestartInbounds(tx, ids)

This removes specified inbounds from the core, terminates their connections, refreshes user data and TLS configurations, and re-registers them with the Sing-Box runtime.

Summary

  • Layered Architecture: Clear separation between database models (database/model/ files), service logic (service/inbounds.go and service/outbounds.go), and runtime integration (corePtr methods)
  • Dynamic Schemas: Options fields in both registries allow protocol-specific configurations without database migrations
  • Hot-Reload Capability: Direct manipulation of the Sing-Box core via AddInbound, RemoveInbound, AddOutbound, and RemoveOutbound enables runtime updates
  • User Integration: Inbounds automatically enrich configurations with client data from the clients table for protocols requiring authentication
  • Graceful Restarts: RestartInbounds clears connections and refreshes configurations without process termination

Frequently Asked Questions

How does S-UI handle new protocols without changing the database schema?

The registry system stores protocol-specific parameters in the Options field as json.RawMessage. This design allows arbitrary key-value configurations for new protocols while maintaining fixed database columns for common attributes like type, tag, and id. The custom MarshalJSON and UnmarshalJSON methods handle the serialization logic transparently.

What happens when an administrator edits an active inbound?

The Save method in service/inbounds.go implements an atomic update sequence. It first removes the existing inbound from the running Sing-Box core using corePtr.RemoveInbound, then adds the updated configuration via corePtr.AddInbound, and finally commits the transaction to the SQLite database. This ensures consistency between runtime state and persistent storage.

Why do inbounds require user enrichment while outbounds do not?

Inbounds accept incoming client connections and often require user authentication for protocols like VLESS, VMESS, and Shadowsocks. The service layer queries the clients table through fetchUsers and embeds user credentials in the JSON payload. Outbounds represent upstream destinations and typically authenticate using static credentials or certificates stored directly in their Options field, eliminating the need for dynamic user lookups.

How does the registry achieve hot-reload without restarting Sing-Box?

S-UI maintains a global corePtr reference to the running Sing-Box instance. Service methods call AddInbound, RemoveInbound, AddOutbound, and RemoveOutbound directly on this pointer, invoking Sing-Box's internal API for dynamic configuration changes. Combined with connection tracking via ConnTracker().CloseConnByInbound, the system updates routing rules and listeners while maintaining existing connections where possible.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →