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/gogsentry point produces the only binary. GlobalInitininternal/route/install.gostarts 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.gofile (700+ lines) also owns the entire route registration tree — the HTTP topology is defined here rather than insideinternal/route. - Key types:
webCommand,servCommand,hookCommand, etc. (allcli.Commandinstances) - Dependencies:
internal/route,internal/conf,internal/context, allinternal/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, embeddedconf/assets
internal/context#
- Package:
gogs.io/gogs/internal/context - Responsibility: Defines the
*Contextstruct that every HTTP handler receives — it embeds*macaron.Contextand adds user session state, CSRF token, flash messages, current repository context (*Repository), and organization context (*Organization). TheContexter(Store)function returns a Macaron middleware that builds this context per request. - Key types:
Context,Repository,Organization,Storeinterface,AuthStoreinterface - 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.Contextand calldatabase.Handledirectly 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.Storeinterface 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()). TheDBstruct 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 togogs servas a subprocess to execute the git command. Optionally used as a replacement for system sshd withauthorized_keysinjection. - 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 inconf/auth.d/. - Key types: Provider-specific
Configstructs;Authenticatefunctions - Dependencies: Provider-specific:
go-ldap/ldap,msteinert/pam, stdlibnet/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 mapGit 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 goroutinesGit 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 deliveryInitialization / 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 socketDI 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/-cto 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.,
--portongogs web). - Feature flags via build tags: Optional features like
pamandcert(self-signed TLS generation) are conditionally compiled viago build -tags. - Auth sources: Authentication backends can also be configured via INI files in
conf/auth.d/(loaded bydatabase.NewConnection()asloginSourceFilesStore).
Key design decisions#
Global database handle instead of dependency injection.
database.Handleis a package-level*DBset 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 newercontext.Storeinterface is a partial refactor toward DI, but it only applies to the auth middleware — not to the ~80 handler files that still calldatabase.Handledirectly.Route registration centralized in
cmd/gogs/web.go. All 700+ lines of route definitions live in the binary’s entry file, not ininternal/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 meansgo test ./internal/route/...cannot test routing behavior without a running Macaron instance.Dual ORM migration (xorm → GORM). The codebase is mid-migration: newer domain stores (
Users,Repositories,AccessTokens, etc.) use GORM v2 through theHandle *DBfacade; older models (Issue,Comment,Milestone,Release,Webhook,Actionrecords) still use xorm directly via theengineglobal. This is an intentional incremental migration rather than a big-bang rewrite. The split is visible indatabase.go(GORM tables) vs. theengine.go/models.goxorm setup.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 spawnsgogs servas a subprocess, which in turn execsgit-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.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.Contextinjection, thebinding.BindIgnErr, and thei18n.Localeinjection 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.