# How Do All the Pieces Fit Together?

> If S-UI were a pizza restaurant, what would the kitchen, the waiter, and the cash register each do? This page walks through the major Go packages — cmd, core, api, web, sub, database, cronjob — and asks "what question does each one answer?" so you build a mental map before diving into details.

- Repository: alireza0/s-ui
- GitHub: https://github.com/alireza0/s-ui
- Human wiki: https://grok-wiki.com/public/wiki/alireza0-s-ui-2ddf594ac444
- Complete Markdown: https://grok-wiki.com/public/wiki/alireza0-s-ui-2ddf594ac444/llms-full.txt

## Source Files

- `cmd/cmd.go`
- `cmd/admin.go`
- `app/app.go`
- `web/web.go`
- `middleware/domainValidator.go`
- `config/config.go`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [cmd/cmd.go](cmd/cmd.go)
- [cmd/admin.go](cmd/admin.go)
- [app/app.go](app/app.go)
- [web/web.go](web/web.go)
- [api/apiHandler.go](api/apiHandler.go)
- [sub/sub.go](sub/sub.go)
- [sub/subHandler.go](sub/subHandler.go)
- [core/main.go](core/main.go)
- [database/db.go](database/db.go)
- [cronjob/cronJob.go](cronjob/cronJob.go)
- [cronjob/statsJob.go](cronjob/statsJob.go)
- [config/config.go](config/config.go)
- [middleware/domainValidator.go](middleware/domainValidator.go)
</details>

# How Do All the Pieces Fit Together?

Imagine S-UI is a pizza restaurant. The **kitchen** (sing-box, the `core` package) actually makes the pizzas — it handles real network traffic. The **waiter** (`web` + `api`) takes orders from customers sitting at the table (you, the admin in your browser). The **takeout counter** (`sub`) lets delivery apps (VPN clients on phones and laptops) pick up orders by themselves. The **manager** (`app`) hired everyone and keeps the restaurant running. The **cashier's ledger** (`database`) writes down every order, every customer, every bill. And the **kitchen timer** (`cronjob`) checks every few seconds that the oven is still on.

This page walks through each room so you understand *what question each package answers* before you start reading individual files.

---

## The Manager's Office: `app`

**Question answered: "Who starts and stops everything, and in what order?"**

When S-UI launches, `app.App.Init()` is the very first code that runs with real work to do. Think of it as the restaurant manager arriving in the morning and unlocking every door in the right sequence.

```go
// app/app.go:32-53
func (a *APP) Init() error {
    a.initLog()
    err := database.InitDB(config.GetDBPath())   // open the ledger
    a.SettingService.GetAllSetting()             // read the restaurant's rules
    a.core = core.NewCore()                      // prep the kitchen
    a.cronJob = cronjob.NewCronJob()             // set the kitchen timers
    a.webServer = web.NewServer()                // hire the waiter
    a.subServer = sub.NewServer()                // open the takeout counter
    a.configService = service.NewConfigService(a.core)
    return nil
}
```

`Start()` then fires them up in dependency order: timers first, then the web UI, then the takeout counter, then the kitchen (`configService.StartCore()`). `Stop()` reverses the order gracefully.

Sources: [app/app.go:32-103]()

---

## The Receptionist: `cmd`

**Question answered: "What did the operator type on the command line?"**

Before `app` does anything, `cmd.ParseCmd()` reads the terminal arguments. It is the front door of the restaurant — a small dispatch table that routes you to the right person.

| Subcommand | What it does |
|---|---|
| `admin` | Show, reset, or set the first admin's username/password |
| `setting` | Show or update panel port, path, sub port, sub path |
| `uri` | Print the panel URL |
| `migrate` | Run database migrations from older versions |
| *(none)* | Start the full application |

```go
// cmd/cmd.go:70-111  (simplified)
switch os.Args[1] {
case "admin":   updateAdmin / showAdmin / resetAdmin(...)
case "uri":     getPanelURI()
case "migrate": migration.MigrateDb()
case "setting": updateSetting / showSetting(...)
}
```

Notice that `admin` commands open the database directly — they do not start a web server. They are maintenance tools, not runtime paths.

Sources: [cmd/cmd.go:13-112](), [cmd/admin.go:11-64]()

---

## The Waiter: `web` + `api`

**Question answered: "How does the browser talk to S-UI, and what can it ask for?"**

`web.Server` is one Gin HTTP server. It does two things at once:

1. **Serves the Vue.js single-page app** from an embedded filesystem (`//go:embed *` in `web/web.go:29`). The entire admin UI lives inside the binary — no separate static-file server needed.
2. **Mounts the API routes** under `/api` and `/apiv2` within the same process.

```
Browser request
    │
    ├─► /assets/…         → embedded JS/CSS (1-year cache header)
    ├─► /api/:action      → APIHandler  (session-cookie auth)
    ├─► /apiv2/…          → APIv2Handler (token auth)
    └─► everything else   → index.html  (Vue SPA)
```

`api.APIHandler` speaks a simple action-based protocol. A POST to `/api/save` saves configuration; a GET to `/api/load` fetches it all. There is also a second handler, `APIv2Handler`, which uses bearer tokens instead of session cookies — useful for programmatic access or API clients.

```go
// api/apiHandler.go:34-63
case "login":       a.ApiService.Login(c)
case "save":        a.ApiService.Save(c, loginUser)
case "restartApp":  a.ApiService.RestartApp(c)
case "restartSb":   a.ApiService.RestartSb(c)
case "addToken":    a.ApiService.AddToken(c); a.apiv2.ReloadTokens()
```

If the `webDomain` setting is set, the `middleware.DomainValidator` middleware rejects any request whose `Host` header doesn't match exactly — a basic vhost-style guard.

```go
// middleware/domainValidator.go:11-25
func DomainValidator(domain string) gin.HandlerFunc {
    return func(c *gin.Context) {
        host := c.Request.Host
        if host != domain {
            c.AbortWithStatus(http.StatusForbidden)
            return
        }
        c.Next()
    }
}
```

Sources: [web/web.go:29-135](), [api/apiHandler.go:23-80](), [middleware/domainValidator.go:11-25]()

---

## The Takeout Counter: `sub`

**Question answered: "How do VPN client apps fetch their configuration automatically?"**

`sub.Server` is a *completely separate* HTTP server, listening on its own port (configurable via `subPort`). VPN apps like Clash, sing-box, or any link-based client call this endpoint to get their configuration — they never touch the admin UI port.

```
VPN Client App
    │
    └─► GET /sub/<subid>              → plain link list (base64 encoded)
        GET /sub/<subid>?format=json  → sing-box JSON config
        GET /sub/<subid>?format=clash → Clash YAML config
        HEAD /sub/<subid>             → quota/expiry headers only
```

The handler reads a `subid` from the URL path, looks up the matching client record, and delegates to one of three service implementations depending on the `format` query parameter.

```go
// sub/subHandler.go:27-56
subId := c.Param("subid")
format, isFormat := c.GetQuery("format")
switch format {
case "json":  result, headers, err = s.JsonService.GetJson(subId, format)
case "clash": result, headers, err = s.ClashService.GetClash(subId)
default:      result, headers, err = s.SubService.GetSubs(subId)   // link list
}
```

The response always includes standard subscription headers (`Subscription-Userinfo`, `Profile-Update-Interval`, `Profile-Title`) so client apps know when the subscription expires and how much data has been used.

Just like the web server, `sub.Server` can optionally be locked to a specific domain via `subDomain` using the same `DomainValidator` middleware.

Sources: [sub/sub.go:38-67](), [sub/subHandler.go:17-78]()

---

## The Kitchen: `core`

**Question answered: "Who actually moves network packets?"**

`core.Core` wraps **sing-box** (`github.com/sagernet/sing-box`), the underlying proxy engine. S-UI itself does not move traffic — it tells sing-box what to do and sing-box does the real work.

```go
// core/main.go:28-80
type Core struct {
    isRunning bool
    instance  *Box
}

func (c *Core) Start(sbConfig []byte) error {
    var opt option.Options
    opt.UnmarshalJSONContext(globalCtx, sbConfig)   // parse JSON config
    c.instance, err = NewBox(Options{...})           // create sing-box instance
    c.instance.Start()                               // start proxy engine
    inbound_manager  = service.FromContext[adapter.InboundManager](globalCtx)
    outbound_manager = service.FromContext[adapter.OutboundManager](globalCtx)
    ...
}
```

After start, `core` holds references to sing-box's internal managers (inbounds, outbounds, endpoints, router). The `service` layer uses these to query live traffic stats and connection state without restarting sing-box.

Sources: [core/main.go:28-95]()

---

## The Ledger: `database`

**Question answered: "Where is everything stored, and what are the tables?"**

S-UI uses **SQLite** via GORM, stored at a path derived from the binary's own directory (or `SUI_DB_FOLDER` env var). The `InitDB` function runs GORM `AutoMigrate` on startup to create or update all tables.

```go
// database/db.go:97-110
db.AutoMigrate(
    &model.Setting{}, &model.Tls{},
    &model.Inbound{}, &model.Outbound{},
    &model.Service{}, &model.Endpoint{},
    &model.User{},    &model.Tokens{},
    &model.Stats{},   &model.Client{},
    &model.Changes{},
)
```

The database is opened in WAL mode with a 10-second busy timeout, allowing the web server and cronjobs to read concurrently without blocking each other.

| Table | Stores |
|---|---|
| `settings` | Panel port, path, TLS files, time zone, etc. |
| `inbounds` / `outbounds` / `endpoints` | sing-box protocol config blocks |
| `clients` | VPN users with their sub IDs and quotas |
| `stats` | Per-client traffic snapshots |
| `tokens` | API v2 bearer tokens |
| `changes` | Audit log of config changes |

If no user exists on first run, `initUser()` seeds the default `admin`/`admin` credential.

Sources: [database/db.go:36-119]()

---

## The Kitchen Timers: `cronjob`

**Question answered: "What happens automatically while nobody is clicking anything?"**

`cronjob.CronJob` registers five recurring jobs using `github.com/robfig/cron`:

| Schedule | Job | Purpose |
|---|---|---|
| Every 10 s | `StatsJob` | Pull traffic counters from sing-box, save to DB |
| Every 1 m | `DepleteJob` | Mark clients as expired if quota is exhausted |
| Daily | `DelStatsJob` | Delete stats older than `trafficAge` days |
| Every 5 s | `CheckCoreJob` | Restart sing-box if it crashed |
| Every 10 m | `WALCheckpointJob` | Flush the SQLite WAL file |

```go
// cronjob/cronJob.go:22-33
c.cron.AddJob("@every 10s", NewStatsJob(trafficAge > 0))
c.cron.AddJob("@every 1m",  NewDepleteJob())
c.cron.AddJob("@every 5s",  NewCheckCoreJob())
c.cron.AddJob("@every 10m", NewWALCheckpointJob())
```

`StatsJob` is the most important: every 10 seconds it calls `StatsService.SaveStats()`, which reads live counters from sing-box and writes them to the `stats` table so the UI and subscription headers always show current data.

Sources: [cronjob/cronJob.go:17-43](), [cronjob/statsJob.go:19-25]()

---

## The Settings Sheet: `config`

**Question answered: "Where do environment-level defaults come from?"**

`config` is the simplest package. It reads a handful of environment variables and two embedded text files (`version`, `name`) compiled into the binary at build time.

| Env var | Purpose |
|---|---|
| `SUI_DEBUG` | Enable debug logging and Gin debug mode |
| `SUI_LOG_LEVEL` | `debug`, `info`, `warn`, `error` |
| `SUI_DB_FOLDER` | Override the SQLite database directory |

Everything else (port, TLS, paths) lives in the `settings` database table and is read through `SettingService` at runtime — not at compile time.

Sources: [config/config.go:36-68]()

---

## The Mental Map

```text
┌─────────────────────────────────────────────────────────┐
│  cmd  — command-line dispatch (admin, setting, migrate)  │
└────────────────────┬────────────────────────────────────┘
                     │ starts
┌────────────────────▼────────────────────────────────────┐
│  app  — orchestrator (Init → Start → Stop)               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │   web    │  │   sub    │  │  core    │  │ cronjob │ │
│  │ (admin   │  │ (client  │  │ (sing-   │  │ (timers │ │
│  │  UI +    │  │  sub     │  │  box     │  │  stats  │ │
│  │  api)    │  │  feeds)  │  │  engine) │  │  expiry)│ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬────┘ │
│       └──────────────┴─────────────┴─────────────┘      │
│                          │                               │
│                   ┌──────▼──────┐                        │
│                   │  database   │  (SQLite / GORM)        │
│                   └─────────────┘                        │
└─────────────────────────────────────────────────────────┘
```

---

## Putting It All Together

When you run `s-ui` with no arguments, `cmd` sees no subcommand and hands control to `app`. The manager opens the database, loads settings, and starts every server concurrently: the admin UI on one port, the subscription feed on another, and sing-box proxying traffic underneath. Every 10 seconds the kitchen timer reads live traffic numbers and saves them so the subscription headers stay fresh. When you click "Save" in the browser, the waiter (`api`) writes changes to the database and optionally tells the kitchen (`core`) to reload its configuration. When a phone app refreshes its VPN profile, it hits the takeout counter (`sub`) on the separate port and gets back a link list, JSON, or Clash YAML — whichever format it asked for.

Sources: [app/app.go:56-103]()
