How to Extend or Modify Gitea Modules: A Complete Developer's Guide

You can extend Gitea by creating new packages under modules/ for reusable utilities, implementing business logic in services/ with dependency injection, and registering HTTP handlers in routers/web/, while modifying existing behavior requires overriding service implementations in the global container or patching handler registrations.

Gitea’s self-hosted Git service architecture is built around a clean modular design that separates concerns into distinct layers, making it straightforward to add custom functionality or alter core behavior without forking the entire codebase. Understanding how to extend or modify Gitea modules allows developers to integrate proprietary workflows, custom validations, or specialized webhook payloads while maintaining compatibility with upstream updates. The codebase follows strict conventions in the go-gitea/gitea repository, where components communicate through Go interfaces and a simple global registry enables clean dependency injection.

Understanding Gitea's Layered Architecture

Gitea organizes code into four primary layers, each with specific responsibilities and entry points. This separation ensures that low-level utilities remain independent from HTTP handling and database operations.

  • Modules (modules/): Re-usable utilities, helpers, and low-level abstractions. Examples include modules/util for helper functions, modules/web for HTTP routing infrastructure, and modules/validation for input checking. The core router implementation lives in modules/web/router.go.

  • Services (services/): Business-logic components that operate on models and expose higher-level APIs. For instance, services/webhook/webhook.go handles webhook delivery logic, while issue and repository management live in their respective service packages.

  • Routers (routers/web/ and routers/api/): HTTP handlers that map URLs to service calls, enforce middleware chains, and render templates. Handler files like routers/web/user/setting/webhooks.go extract parameters and invoke the appropriate service methods.

  • Models (models/): Database entities and ORM helpers defining structs like User in models/user/user.go and repository metadata in models/repo/.

The application bootstrap occurs in main.go at the repository root, where all modules, services, and routers initialize and wire together.

Request Flow Through the System

When a request hits the Gitea server, it flows through a predictable pipeline that demonstrates how these layers interact:

  1. The router in modules/web/router.go matches the request path and dispatches to a handler function in routers/web/.

  2. The handler executes middleware from modules/web/middleware/ for logging, authentication, CSRF protection, and locale detection.

  3. The handler invokes a service method (e.g., webhook.CreateWebhook), passing a context object.

  4. The service manipulates models and may call other modules for utility functions, completing the business operation.

Because each layer communicates through well-defined Go interfaces, you can inject alternative implementations at any stage without modifying upstream code.

How to Extend Gitea with New Modules

Adding functionality follows a bottom-up approach: build utilities, wrap them in services, expose via HTTP, and register globally.

Create a New Module

For code reusable across multiple features, create a package under modules/yourmodule/. Modules should be pure libraries with no dependency on database models or HTTP contexts. Expose a public API through clearly named functions and structs.

Implement Business Logic as a Service

Encapsulate domain operations in services/yourservice/. Define a Go interface describing the operations (e.g., type YourService interface { DoSomething(ctx context.Context, ...) error }), then create an unexported struct implementing it. This pattern, visible throughout services/webhook/ and services/issue/, enables easy mocking and replacement.

Register Your Service

Gitea uses a simple global container initialized at startup. In services/init.go (or a dedicated init.go within your package), call Register() with your service implementation:

func init() {
    Register(myservice.New())
}

Retrieve the service elsewhere via the global getter, typically service.GetYourService() or similar patterns found in the existing codebase.

Add HTTP Endpoints

Create handler files in routers/web/yourfeature/ that accept *context.Context (Gitea's extended context wrapping http.ResponseWriter and *http.Request). Extract parameters, call your service, and render JSON or HTML. Register routes in routers/web/init.go or the feature's own init() function using the router group pattern:

web.Group("/api/v1").GET("/yourfeature/:id", yourhandler.GetItem)

Wire Custom Middleware

Reuse existing middleware from modules/web/middleware/ or create new middleware functions that accept and return http.Handler. Apply these at the route group level or individual handler level to enforce cross-cutting concerns like custom authentication headers or rate limiting.

How to Modify Existing Gitea Modules

When you need to alter existing behavior rather than add new features, three techniques allow surgical modifications without extensive rewrites.

Override Functions with Wrappers

For changes to utility functions in modules/util/ or validation logic, create a wrapper function in a new package that calls the original and modifies the result. Adjust the call site in the router or service to use your wrapper instead of the original import.

Replace Service Implementations

Gitea's global service container keeps the last registration for any interface type. To replace an existing service, create a new implementation that embeds the original (for method reuse) or implements the interface from scratch, then register it in an init() function that executes after the original registration:

// services/init.go
func init() {
    // Original registration happens first
    Register(webhook.New())
    // Your override wins
    Register(customwebhook.New())
}

This pattern allows you to modify webhook payloads, alter issue processing logic, or change repository initialization sequences while maintaining the existing API contract.

Patch Router Handlers

Copy the existing handler function from routers/web/ to your own file, modify the logic as needed, and update the route registration to point to your version instead of the original. This approach works best for one-off behavioral changes in the presentation layer.

Practical Code Examples

Example: Creating a Utility Module

Create modules/hello/hello.go for simple, stateless utilities:

package hello

import "fmt"

// Greet returns a friendly greeting.
func Greet(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

Reference existing utilities in modules/util/ for patterns on error handling and input validation.

Example: Building a Service Layer

Define the interface and implementation in services/greeting/greeting.go:

package greeting

import (
    "context"
    "github.com/go-gitea/gitea/modules/hello"
)

// Service describes greeting-related operations.
type Service interface {
    SayHello(ctx context.Context, user string) (string, error)
}

type serviceImpl struct{}

func New() Service { return &serviceImpl{} }

func (s *serviceImpl) SayHello(_ context.Context, user string) (string, error) {
    return hello.Greet(user), nil
}

Register in services/init.go:

package services

import (
    "github.com/go-gitea/gitea/services/greeting"
)

func init() {
    Register(greeting.New())
}

Example: Adding an HTTP Endpoint

Implement the handler in routers/web/greeting/greeting.go:

package greeting

import (
    "net/http"
    "github.com/go-gitea/gitea/modules/context"
    "github.com/go-gitea/gitea/services/greeting"
)

// GetGreeting handles GET /api/v1/greeting/:name
func GetGreeting(ctx *context.Context) {
    name := ctx.Params("name")
    msg, err := greeting.GetService().SayHello(ctx, name)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
        return
    }
    ctx.JSON(http.StatusOK, map[string]string{"message": msg})
}

Wire the route in routers/web/api/v1/init.go or equivalent:

func init() {
    web.Group("/api/v1").GET("/greeting/:name", greeting.GetGreeting)
}

Example: Overriding an Existing Service

To customize webhook payloads, create services/webhook/custom.go:

package webhook

import (
    "context"
    "github.com/go-gitea/gitea/models/webhook"
)

type customImpl struct {
    *serviceImpl // Embed original to inherit unmodified methods
}

func NewCustom() Service {
    return &customImpl{serviceImpl: &serviceImpl{}}
}

func (c *customImpl) Deliver(ctx context.Context, hook *webhook.Webhook) error {
    // Modify payload before delegating to original logic
    hook.Payload = []byte(`{"custom":"value"}`)
    return c.serviceImpl.Deliver(ctx, hook)
}

Ensure your registration in services/init.go executes last:

func init() {
    Register(NewCustom()) // Replaces the default webhook service
}

Key Files for Module Development

Understanding these critical entry points accelerates development when you extend or modify Gitea modules:

  • main.go: Application bootstrap that initializes the global service container and starts the HTTP server.
  • modules/web/router.go: Core HTTP routing implementation defining how URLs map to handlers.
  • modules/web/middleware/: Common middleware for authentication, CSRF protection, logging, and locale detection.
  • services/webhook/webhook.go: Exemplary service implementation showing business logic separation from transport concerns.
  • routers/web/user/setting/webhooks.go: Reference for handler patterns that bridge HTTP requests to service calls.
  • models/user/user.go: Database model definition showing ORM struct conventions used throughout services.

Summary

  • Gitea's architecture separates concerns into modules (utilities), services (business logic), routers (HTTP handling), and models (data), enabling clean extension points.
  • Extend functionality by creating packages under modules/, implementing interfaces in services/, and registering routes in routers/web/.
  • Modify existing behavior by leveraging the global service container—later registrations override earlier ones—or by wrapping functions and patching handler registrations.
  • Always follow the interface-based patterns found in services/webhook/webhook.go and modules/web/router.go to ensure your code remains compatible with upstream Gitea updates.

Frequently Asked Questions

How do I ensure my custom module survives Gitea updates?

Place your code in dedicated directories under modules/ and services/ rather than modifying existing files. Use the service override pattern (registering your implementation after the default in services/init.go) so that git merge operations from upstream apply cleanly without conflicts. Avoid editing files in routers/web/ that you don't own; instead, add new handler files and register additional routes.

Can I modify Gitea's database models when extending modules?

While you can add new tables and models in separate packages under models/, avoid modifying existing model files like models/user/user.go or models/repo/repo.go directly, as this breaks database migrations and upstream compatibility. Instead, create join tables or extension tables linked by foreign keys, and access them through your custom service layer.

What is the difference between a module and a service in Gitea?

Modules (modules/) are low-level, reusable libraries with no application state—think of them as internal packages providing utilities, validation, or HTTP abstractions. Services (services/) contain business logic, operate on database models, and typically manage transactions. Services depend on modules, but modules should never import services or models, maintaining a strict dependency direction.

How do I debug my custom service implementation during development?

Add your service initialization to services/init.go with verbose logging, then run Gitea with the debug log level enabled in app.ini. Since services are singletons registered at startup in main.go, you can add log.Info() calls in your service's constructor and methods to trace execution. Use go test in your package directories for unit testing, following the patterns in services/webhook/webhook_test.go.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →