# S-UI Inbound and Outbound Registry Architecture Explained

> Explore the S-UI registry architecture. Understand how Inbounds and Outbounds manage proxy configurations in a layered Go system, syncing seamlessly with the Sing-Box core.

- Repository: [Alireza Ahmadi/s-ui](https://github.com/alireza0/s-ui)
- Tags: architecture
- Published: 2026-05-22

---

**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`](https://github.com/alireza0/s-ui/blob/main/database/model/inbounds.go) and [`database/model/outbounds.go`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/service/inbounds.go) and [`service/outbounds.go`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/database/model/inbounds.go). The schema captures fixed fields alongside flexible protocol-specific options:

```go
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`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/database/model/outbounds.go) maintains a simplified schema:

```go
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`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/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

```go
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

```go
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

```go
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`](https://github.com/alireza0/s-ui/blob/main/service/inbounds.go) and [`service/outbounds.go`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/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.