Gogs — Architecture#

Architectural style#

Monolithic layered application (single-binary web service)

Gogs is a classic monolith: one binary, one process, no microservices. All concerns — HTTP serving, SSH serving, background cron jobs, Git operations, email delivery — run inside the same OS process. The internal structure is loosely layered (cmd → route handlers → database → DB), but the layers are not strictly enforced and there is no formal service/domain layer between handlers and the database. This is deliberate: the project’s founding philosophy is simplicity and minimal operational overhead over architectural purity.

Evidence:

  • A single cmd/gogs entry point produces the only binary.
  • GlobalInit in internal/route/install.go starts the SSH server, cron scheduler, mirror sync, and hook delivery — all in the same process.
  • No gRPC, no message queues, no inter-process communication patterns.

Component diagram (textual)#

┌─────────────────────────────────────────────────────────┐
│                    gogs binary                          │
│                                                         │
│  ┌──────────────┐  CLI: urfave/cli v3                  │
│  │  cmd/gogs    │──────────────────────────────────┐   │
│  │  (7 subcmds) │                                  │   │
│  └──────────────┘                                  │   │
│         │ runWeb()                                  │   │
│         ▼                                           │   │
│  ┌──────────────────┐   ┌───────────────────────┐  │   │
│  │  internal/conf   │   │  internal/ssh         │  │   │
│  │  (INI config)    │   │  (built-in SSH server)│  │   │
│  └──────────────────┘   └───────────────────────┘  │   │
│         │                        │                  │   │
│         ▼                        ▼                  │   │
│  ┌──────────────────────────────────────────────┐  │   │
│  │             Macaron HTTP stack               │  │   │
│  │  middleware: session, csrf, cache, i18n,     │  │   │
│  │             gzip, toolbox, static files      │  │   │
│  │                    │                         │  │   │
│  │         context.Contexter (Store)            │  │   │
│  │         (builds *context.Context per request)│  │   │
│  └──────────────────────────────────────────────┘  │   │
│                       │                             │   │
│         ┌─────────────┼──────────────┐              │   │
│         ▼             ▼              ▼              │   │
│  ┌────────────┐ ┌──────────┐ ┌────────────────┐   │   │
│  │ route/user │ │route/repo│ │ route/api/v1   │   │   │
│  │ route/org  │ │route/lfs │ │ route/admin    │   │   │
│  └────────────┘ └──────────┘ └────────────────┘   │   │
│         │             │              │              │   │
│         └─────────────┴──────────────┘              │   │
│                       │                             │   │
│                       ▼                             │   │
│  ┌──────────────────────────────────────────────┐  │   │
│  │         internal/database                    │  │   │
│  │  global Handle (*DB wrapping *gorm.DB)       │  │   │
│  │  Store methods: Users, Repos, Actions,       │  │   │
│  │  AccessTokens, LoginSources, Organizations,  │  │   │
│  │  Permissions, PublicKeys, TwoFactors, LFS,   │  │   │
│  │  Notices                                     │  │   │
│  │                                              │  │   │
│  │  Legacy xorm engine for remaining models     │  │   │
│  └──────────────────────────────────────────────┘  │   │
│                       │                             │   │
│                       ▼                             │   │
│              MySQL / PostgreSQL / SQLite3           │   │
│                                                     │   │
│  ┌──────────────────────────────────────────────┐  │   │
│  │  Background goroutines (cron, mirrors,       │  │   │
│  │  webhook delivery, pull request tests)       │  │   │
│  └──────────────────────────────────────────────┘  │   │
└─────────────────────────────────────────────────────────┘

Core components#

cmd/gogs#

  • Package: cmd/gogs (main package)
  • Responsibility: Binary entry point. Registers 7 CLI subcommands (web, serv, hook, admin, import, backup, restore). The web.go file (700+ lines) also owns the entire route registration tree — the HTTP topology is defined here rather than inside internal/route.
  • Key types: webCommand, servCommand, hookCommand, etc. (all cli.Command instances)
  • Dependencies: internal/route, internal/conf, internal/context, all internal/route/* sub-packages, gopkg.in/macaron.v1

internal/conf#

  • Package: gogs.io/gogs/internal/conf
  • Responsibility: Loads and validates INI-based configuration. Exports typed config structs (conf.Server, conf.Auth, conf.Database, conf.SSH, etc.) as package-level variables consumed globally. Supports custom config path via CLI flag.
  • Key types: Typed config structs (all package-level vars, not a single config object)
  • Dependencies: gopkg.in/ini.v1, embedded conf/ assets

internal/context#

  • Package: gogs.io/gogs/internal/context
  • Responsibility: Defines the *Context struct that every HTTP handler receives — it embeds *macaron.Context and adds user session state, CSRF token, flash messages, current repository context (*Repository), and organization context (*Organization). The Contexter(Store) function returns a Macaron middleware that builds this context per request.
  • Key types: Context, Repository, Organization, Store interface, AuthStore interface
  • Dependencies: internal/database, internal/conf, internal/template, gopkg.in/macaron.v1, go-macaron/{session,csrf,cache,i18n}

internal/route (and sub-packages)#

  • Package: gogs.io/gogs/internal/route + route/{user,repo,org,admin,api/v1,lfs,dev}
  • Responsibility: HTTP handler functions only — no route registration, which happens in cmd/gogs/web.go. Handlers accept *context.Context and call database.Handle directly for data access. The repo sub-package is the largest with 61 files covering repository browsing, issues, pull requests, releases, wiki, webhooks, settings, and the HTTP Git protocol.
  • Key types: Handler functions (no shared handler structs in most sub-packages); repo.Store interface for HTTP Git handlers; user.SettingsHandler (a newer handler struct pattern)
  • Dependencies: internal/database, internal/context, internal/conf, and domain utility packages

internal/database#

  • Package: gogs.io/gogs/internal/database
  • Responsibility: Central data layer. Contains both model structs and store implementations. Newer stores use GORM v2 via the global Handle *DB; legacy models use an xorm engine (HasEngine, NewEngine()). The DB struct provides factory methods for each domain store (Handle.Users(), Handle.Repositories(), etc.).
  • Key types: DB, *UsersStore, *RepositoriesStore, *AccessTokensStore, *LoginSourcesStore, *PermissionsStore, *OrganizationsStore, *PublicKeysStore, *TwoFactorsStore, *LFSStore, *ActionsStore, *NoticesStore
  • Dependencies: gorm.io/gorm, xorm.io/xorm (legacy), internal/conf, internal/dbx

internal/ssh#

  • Package: gogs.io/gogs/internal/ssh
  • Responsibility: Runs a built-in SSH server that handles Git-over-SSH connections. For authorized keys, it authenticates the user via public key lookup in database, then exec-dispatches to gogs serv as a subprocess to execute the git command. Optionally used as a replacement for system sshd with authorized_keys injection.
  • Key types: Listen(opts, appDataPath) function; SSH connection and channel handlers
  • Dependencies: golang.org/x/crypto/ssh, internal/database, internal/conf

internal/cron#

  • Package: gogs.io/gogs/internal/cron
  • Responsibility: Background scheduled tasks: mirror synchronization triggers, repository stats updates, authorized keys rewrites, and similar housekeeping. Uses gogs.io/gogs/pkg/cron (the project’s own cron library).
  • Key types: Task registrations via NewContext()
  • Dependencies: gogs.io/gogs/pkg/cron, internal/database, internal/conf

internal/auth#

  • Package: gogs.io/gogs/internal/auth/{github,ldap,pam,smtp}
  • Responsibility: Pluggable authentication backends. Each sub-package implements an authentication provider. Login sources are stored in the database (LoginSource) and can also be loaded from INI files in conf/auth.d/.
  • Key types: Provider-specific Config structs; Authenticate functions
  • Dependencies: Provider-specific: go-ldap/ldap, msteinert/pam, stdlib net/smtp, HTTP client for GitHub OAuth

Data flow#

Typical web UI request (e.g., view a repository):

1. HTTP GET /:username/:reponame
2. → Macaron middleware stack:
     session.Sessioner  — loads/creates session
     csrf.Csrfer        — injects CSRF token
     context.Contexter  — builds *context.Context:
                           • authenticatedUser() called → database.Handle.AccessTokens().GetBySHA1()
                             or database.Handle.Users().GetByID() from session
3. → context.RepoAssignment() middleware
       → database.Handle.Repositories().GetByName() to load *database.Repository
       → Permissions check via database.Handle.Permissions()
4. → repo.Home handler (internal/route/repo/view.go)
       → direct calls to database functions (xorm-based legacy):
         GetBranches(), GetCommits(), etc. via gogs.io/git-module
5. → c.HTML(200, "repo/home") renders template with c.Data map

Git push via SSH:

1. SSH connection → internal/ssh SSH server → public key lookup in database
2. → exec: gogs serv <key_id>
3. → cmd/gogs/serv.go resolves repo from git-receive-pack command
4. → permission check via database.Handle.Permissions()
5. → exec: git-receive-pack on repo path
6. → on completion: gogs hook post-receive fires webhook delivery goroutines

Git push via HTTP:

1. POST /:username/:reponame/git-receive-pack (or /info/refs?service=git-receive-pack)
2. → repo.HTTPContexter(repo.NewStore()) middleware — authenticates HTTP git requests
3. → repo.HTTP handler — proxies to git http-backend subprocess
4. → post-receive hook fires webhook delivery

Initialization / Bootstrap#

The bootstrap sequence for gogs web is entirely manual (no DI framework):

runWeb(cmd)
├── route.GlobalInit(customConf)              // internal/route/install.go
│   ├── conf.Init(customConf)                 // loads INI config into package-level vars
│   ├── conf.InitLogging(false)               // sets up clog writer
│   ├── email.NewContext()                    // initializes SMTP mailer
│   ├── highlight.NewContext()                // syntax highlighter init
│   ├── markup.NewSanitizer()                 // HTML sanitizer init
│   ├── database.NewEngine()                  // starts xorm engine (legacy models)
│   ├── database.NewConnection()              // starts GORM connection → sets database.Handle
│   │   └── loadLoginSourceFiles()            // loads auth.d INI auth sources
│   ├── database.LoadRepoConfig()             // loads repo config (gitignore templates etc.)
│   ├── database.NewRepoContext()             // loads repository-level config
│   ├── cron.NewContext()                     // registers cron tasks
│   ├── database.InitSyncMirrors()            // starts mirror sync goroutine
│   ├── database.InitDeliverHooks()           // starts webhook delivery goroutine
│   ├── database.InitTestPullRequests()       // starts PR test goroutine
│   └── ssh.Listen(conf.SSH, ...)             // starts built-in SSH server goroutine
├── newMacaron()                              // builds Macaron instance + middleware stack
│   ├── macaron.Logger(), macaron.Recovery()
│   ├── gzip.Gziper()
│   ├── macaron.Static() (×4 for public/avatar/repo-avatar/custom)
│   ├── macaron.Renderer() (HTML templates)
│   ├── i18n.I18n() (31 locales)
│   ├── cache.Cacher()
│   ├── captcha.Captchaer()
│   └── toolbox.Toolboxer() (health check)
├── route registration (700+ lines in web.go)
│   └── m.Group(...) for all UI, API, Git HTTP, internal routes
│       └── session.Sessioner, csrf.Csrfer, context.Contexter(NewStore())  // per-group middleware
└── http.ListenAndServe / TLS / fcgi / unix socket

DI pattern: Manual wiring only. No Wire, Dig, or Fx. The main DI mechanism is the global database.Handle variable set at startup. Component initialization relies on package-level NewContext() functions — a common Go-of-era-2014 pattern. The newer context.Store interface (used by context.Contexter) is a limited step toward dependency injection for the auth middleware, but handlers still access database.Handle directly.


Configuration#

  • Format: INI files via gopkg.in/ini.v1
  • File locations:
    • conf/app.ini — embedded defaults (shipped in binary)
    • <custom_dir>/conf/app.ini — operator overrides (not embedded)
    • CLI flag --config / -c to specify a non-default custom config path
  • Access pattern: Config values are loaded into typed package-level variables in internal/conf (e.g., conf.Server.HTTPPort, conf.Auth.RequireSigninView). These are accessed globally throughout the codebase — there is no config injection.
  • Runtime override: A limited set of server settings can be overridden by CLI flags (e.g., --port on gogs web).
  • Feature flags via build tags: Optional features like pam and cert (self-signed TLS generation) are conditionally compiled via go build -tags.
  • Auth sources: Authentication backends can also be configured via INI files in conf/auth.d/ (loaded by database.NewConnection() as loginSourceFilesStore).

Key design decisions#

  1. Global database handle instead of dependency injection. database.Handle is a package-level *DB set once at startup. Route handlers call it directly without it being passed in. This was idiomatic circa 2014 and keeps code concise but makes unit testing without a real database difficult. The newer context.Store interface is a partial refactor toward DI, but it only applies to the auth middleware — not to the ~80 handler files that still call database.Handle directly.

  2. Route registration centralized in cmd/gogs/web.go. All 700+ lines of route definitions live in the binary’s entry file, not in internal/route. This gives a single-file overview of the entire HTTP topology but creates a very large file and a tight coupling between the binary and the handler packages. It also means go test ./internal/route/... cannot test routing behavior without a running Macaron instance.

  3. Dual ORM migration (xorm → GORM). The codebase is mid-migration: newer domain stores (Users, Repositories, AccessTokens, etc.) use GORM v2 through the Handle *DB facade; older models (Issue, Comment, Milestone, Release, Webhook, Action records) still use xorm directly via the engine global. This is an intentional incremental migration rather than a big-bang rewrite. The split is visible in database.go (GORM tables) vs. the engine.go / models.go xorm setup.

  4. Built-in SSH server as process supervisor. Rather than requiring users to configure a system sshd with authorized_keys, Gogs ships its own SSH server (internal/ssh). When a git push arrives over SSH, the built-in server authenticates via public key lookup in the database, then spawns gogs serv as a subprocess, which in turn execs git-receive-pack. This design avoids sshd configuration but means Gogs must manage its own authorized keys file and SSH host keys — a significant operational surface for a “simple” tool.

  5. Macaron middleware framework (legacy choice). Gogs was built on Macaron at a time when it was the leading Go web framework (it predates the dominance of Gin/Echo). Macaron uses a reflection-based DI middleware model where middleware functions are matched by their return types. This is deeply embedded throughout the codebase — the *context.Context injection, the binding.BindIgnErr, and the i18n.Locale injection all depend on Macaron’s type-matching. Migrating away from Macaron would require touching every handler. The project is effectively locked into Macaron unless a complete handler rewrite is undertaken.