Gitea — Architecture#

Architectural style#

Layered monolith — a single deployable binary with a strict four-tier horizontal layering enforced by convention rather than the Go toolchain. The layers are:

  1. modules/ — infrastructure and cross-cutting utilities (no imports from upper layers)
  2. models/ — data access layer (xorm ORM; imports modules/, not services/ or routers/)
  3. services/ — business logic orchestration (imports models/ and modules/)
  4. routers/ — HTTP handler layer (imports services/; does not import models/ directly)

This downward-only DAG is the defining architectural constraint. The project also incorporates several async subsystems (queue-based background workers, cron jobs, event source notifications) that operate as background goroutines within the same process.

Component diagram (textual)#

┌──────────────────────────────────────────────────────────────────────────────┐
│                             gitea binary                                     │
│                                                                              │
│  ┌─────────────────────────────── routers/ ──────────────────────────────┐  │
│  │  web/       api/v1/     api/packages/   api/actions/   private/       │  │
│  │  (HTML UI)  (REST API)  (pkg registry)  (runner API)   (hook IPC)     │  │
│  └──────────────────┬──────────────────────────────────────────┬─────────┘  │
│                     │ calls                                     │            │
│  ┌──────────────────▼──────────────────────────────────────────▼─────────┐  │
│  │                          services/                                     │  │
│  │  context/  auth/  repository/  pull/  issue/  actions/  packages/     │  │
│  │  mailer/  webhook/  cron/  migrations/  indexer/  convert/  ...       │  │
│  └──────────────────┬───────────────────────────────────────────────────┘   │
│                     │ calls                                                   │
│  ┌──────────────────▼──────────────────────────────────────────────────┐    │
│  │                          models/                                     │    │
│  │  repo/  user/  issues/  pull/  auth/  org/  actions/                │    │
│  │  packages/  webhook/  git/  perm/  db/  migrations/                 │    │
│  └──────────────────┬───────────────────────────────────────────────────┘   │
│                     │ uses                                                    │
│  ┌──────────────────▼──────────────────────────────────────────────────┐    │
│  │                          modules/                                    │    │
│  │  setting  log  git  queue  cache  storage  graceful  indexer        │    │
│  │  markup  ssh  lfs  web  process  session  templates  public  ...    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌────────────────┐  ┌──────────────────────────────────────────────────┐   │
│  │  SSH Server    │  │  Background Goroutines                           │   │
│  │  (modules/ssh) │  │  mirrors · webhooks · mailer · indexer          │   │
│  │  git-over-SSH  │  │  automerge · archiver · cron · actions          │   │
│  └────────────────┘  └──────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────────┘

External:
  git subprocess ──(private HTTP IPC)──▶ routers/private/
  Gitea Actions runner ────────────────▶ routers/api/actions/
  Package managers ───────────────────▶ routers/api/packages/

Core components#

modules/setting#

  • Package: code.gitea.io/gitea/modules/setting
  • Responsibility: Load and expose all application configuration. Reads app.ini (an INI file via go-ini) with environment variable overrides. Exposes a large set of package-level variables (setting.AppURL, setting.Database, setting.SSH, etc.) consumed globally. Also provides CfgProvider for programmatic access.
  • Key types: Package-level vars; ConfigProvider interface; LoadSettings() / LoadSettingsForInstall()
  • Dependencies: modules/log, go-ini/ini

modules/graceful#

  • Package: code.gitea.io/gitea/modules/graceful
  • Responsibility: Coordinate graceful shutdown across all server components. Tracks a count of active servers (numberOfServersToCreate = 4), provides HammerContext() for hard cancellation, and RunAtShutdown() for cleanup callbacks. Uses OS signal handling (SIGTERM, SIGUSR1 for hot-reload via socket inheritance).
  • Key types: Manager, RunCanceler interface
  • Dependencies: modules/process, modules/log

modules/queue#

  • Package: code.gitea.io/gitea/modules/queue
  • Responsibility: Generic, type-parametrized async job queue. WorkerPoolQueue[T] manages a pool of goroutines processing batches of items. Supports three backends: in-memory channel, LevelDB-backed queue (base_levelqueue), or Redis. All async operations in services (webhook delivery, indexer updates, task scheduling) go through this queue.
  • Key types: WorkerPoolQueue[T any], HandlerFuncT[T], ManagedWorkerPoolQueue, Manager
  • Dependencies: modules/setting, modules/log, modules/process

services/context#

  • Package: code.gitea.io/gitea/services/context
  • Responsibility: Per-request context carrier. Context struct holds the current *user_model.User (Doer), *Repository, *Organization, *Package, session, flash, render helper, and template data. This is the primary integration point between the HTTP handler layer and domain logic. Stored in the chi request context by middleware; retrieved by handlers via ctx.GetWebContext(req).
  • Key types: Context, Base, Repository, Organization, APIContext
  • Dependencies: models/user, modules/session, modules/cache, modules/templates, modules/web

models/db#

  • Package: code.gitea.io/gitea/models/db
  • Responsibility: xorm engine lifecycle, transaction helpers, generic Find/Count/Exist/Insert/Update utilities, and the ListOptions / FindPageAndCount paging primitives used throughout models/. Owns the global xorm engine singleton.
  • Key types: Engine, Context (ctx with engine embedded), ListOptions, DefaultContext
  • Dependencies: xorm.io/xorm, modules/setting, modules/log

routers (init.go + NormalRoutes)#

  • Package: code.gitea.io/gitea/routers
  • Responsibility: Top-level bootstrap and route mount point. InitWebInstalled() is the ordered initialization sequence for all services. NormalRoutes() composes the chi root router by mounting sub-routers at canonical prefixes (/, /api/v1, /api/internal, /api/packages, /v2, /api/actions).
  • Key types: *web.Router (wraps chi.Router)
  • Dependencies: All service packages, all router packages, modules/web

routers/private#

  • Package: code.gitea.io/gitea/routers/private
  • Responsibility: Internal HTTP IPC between the git hook subprocess (gitea hook) and the running web server. Routes like /api/internal/hook/pre-receive, /api/internal/hook/post-receive, and /api/internal/repo/... are only bound to a Unix socket or localhost. This avoids filesystem IPC and keeps hook→server communication typed and authenticated.
  • Key types: Routes() function; handler functions for hook events
  • Dependencies: services/repository, services/git

Data flow#

HTTP web request (e.g., viewing a repository)#

Browser HTTP GET /owner/repo
  → modules/web.Router (chi)
    → common.ProtocolMiddlewares() (rate limit, proxy headers, CORS)
    → web_routers.Routes() mount at "/"
      → auth middleware (services/auth pipeline):
          session → basic auth → token → OAuth2 → SSPI
          sets services/context.Context.Doer
      → repo middleware (services/context repo.go):
          loads models/repo.Repository, models/perm permissions
          sets context.Repo
      → handler func (e.g., routers/web/repo/code.go):
          calls services/repository.* or models/repo.*
          populates context.Data (template vars)
          calls ctx.HTML(200, "repo/home")
            → services/context.Context.Render.HTML()
              → modules/templates.PageRenderer
                → html/template execution
  ← HTTP response (rendered HTML)

Async operation (e.g., webhook delivery after push)#

git push over SSH
  → modules/ssh SSH server
    → gitea serv subprocess (cmd/serv.go)
      → git-receive-pack subprocess
        → gitea hook post-receive (cmd/hook.go)
          → POST /api/internal/hook/post-receive (routers/private)
            → services/repository.PushUpdates()
              → queue.Push(webhook.HookTask)  ← enqueued
                → WorkerPoolQueue goroutine pops task
                  → services/webhook.Deliver()
                    → HTTP POST to webhook URL

REST API request (e.g., creating an issue)#

POST /api/v1/repos/owner/repo/issues
  → routers/api/v1.Routes() (chi)
    → auth middleware: token / OAuth2 / basic
    → APIContext (services/context.APIContext)
    → routers/api/v1/repo/issue.go CreateIssue()
      → services/issue.NewIssue()
        → models/issues.CreateIssue() (xorm insert)
        → services/mailer.SendIssueCommentMail()  → queue
        → services/webhook.PrepareWebhooks()      → queue
        → services/indexer.UpdateRepoIndexer()    → queue
  ← JSON response

Initialization / Bootstrap#

The startup sequence in routers.InitWebInstalled() (called from cmd/web.go serveInstalled) is strictly ordered:

  1. git.InitFull — verify git binary, detect git version and feature flags
  2. translation.InitLocales — load i18n locale files (from bindata or disk)
  3. setting.LoadSettings — parse app.ini, populate all setting vars
  4. storage.Init — initialize pluggable file storage backends (local/S3/MinIO)
  5. mailer.NewContext — set up SMTP/sendmail mailer
  6. cache.Init — connect to Redis/in-memory cache
  7. feed_service.Init, uinotification.Init, archiver.Init — minor services
  8. markup / external renderers — register Markdown, orgmode, asciidoc renderers
  9. common.InitDBEngine — open xorm DB connection, run pending migrations
  10. system.Init — load runtime state (AppPath tracking)
  11. oauth2.Init, oauth2_provider.Init — OAuth2 client + OIDC provider
  12. release_service.Init — release-related initialization
  13. models.Init, authmodel.Init, repo_service.Init — domain model init
  14. indexer_service.Init — start background indexer (Bleve/ES/Meilisearch)
  15. Background goroutines: mirror sync, webhook dispatcher, pull-request automerge, task runner, migration runner
  16. eventsource.GetManager().Init — SSE (Server-Sent Events) for live UI updates
  17. mailer_incoming.Init — incoming mail polling (if configured)
  18. syncAppConfForGit — re-sync git hooks if AppPath changed
  19. ssh.Init — start built-in SSH server (if enabled)
  20. auth.Init — register auth source providers (LDAP, SAML, PAM, SSPI)
  21. svg.Init — load SVG icon cache
  22. actions_service.Init — initialize Gitea Actions runner bridge
  23. repo_service.InitLicenseClassifier — load license detection data
  24. cron.Init — start all periodic cron tasks (mirrors, GC, cleanup)

After this sequence, routers.NormalRoutes() builds the chi router tree and cmd/web.go listen() binds it to the configured protocol (HTTP/HTTPS/FCGI/Unix socket).

No dependency injection framework is used. All wiring is manual, via package-level init() functions and explicit calls to mustInit(fn) / mustInitCtx(ctx, fn). The mustInit wrapper uses reflect+runtime.FuncForPC to print the function name on failure before calling log.Fatal.

Configuration#

  • Format: INI file (app.ini) located at CustomPath/conf/app.ini, parsed by go-ini/ini via modules/setting.
  • Custom path: Gitea respects GITEA_CUSTOM env, --custom-path flag, or a compiled-in default. Everything under CustomPath/ (templates, locales, public files) overrides the embedded defaults.
  • Environment overrides: Any app.ini key can be overridden with GITEA__SECTION__KEY=value environment variables.
  • Install wizard: On first run (InstallLock = false), serveInstall() runs a limited HTTP server serving only the install page (routers/install). On form submit, it writes app.ini and restarts.
  • Runtime reload: gitea manager reload-templates and gitea manager flush-queues interact with the running server via routers/private IPC. Full configuration reload is done via a graceful restart (SIGUSR1 + socket inheritance).
  • Feature flags: Build tags (sqlite, bindata, timetzdata) enable/disable features at compile time.

Key design decisions#

  1. Strict layer DAG without internal/. The four-layer architecture (modulesmodelsservicesrouters) is Gitea’s core structural discipline. Unlike most large Go projects, there is no internal/ boundary — all packages are importable by any Go code. Layering discipline is enforced by convention and CI linting. This is a pragmatic trade-off: it lowers the barrier for contributors and allows test packages to reach any layer, at the cost of relying on team discipline for import direction.

  2. Single binary, multiple roles, urfave/cli dispatch. The same gitea binary handles HTTP serving, SSH git service, hook callbacks, admin operations, and health checks. Subcommand dispatch via urfave/cli v3 (not cobra) routes execution. The gitea serv and gitea hook subcommands are invoked by SSH authorized_keys and git hook scripts respectively, then communicate back to the running web process via the private HTTP IPC channel.

  3. WorkerPoolQueue[T] for all async operations. Webhook delivery, search indexing, email sending, automerge scheduling, and Actions task dispatch all use the same generic WorkerPoolQueue[T any] type. The backend is swappable at configuration time (in-memory channel, LevelDB, Redis) without changing call sites. This provides persistence and flow control while keeping service code simple.

  4. services/context.Context as the HTTP boundary object. Rather than passing individual domain objects to handlers, Gitea loads a rich Context struct (user, repo, org, permissions, flash, session, cache) in middleware and passes it to every handler. This is the central seam between the routing and service layers. It avoids long parameter lists but makes the context struct itself a heavy object that must be carefully initialized.

  5. Private HTTP IPC instead of filesystem or pipe IPC. Git hook processes (pre-receive, post-receive, update) communicate with the running server over a Unix socket HTTP API (routers/private). This avoids the fragility of parsing shell output or using named pipes, gives the server full type-safe access to hook events, and allows the server to enforce authorization and quota checks synchronously during the push.