diff --git a/bridge/bridges.go b/bridge/bridges.go
index d74a58fa7b2967c6e4aa834567f8c8e550c86ec4..b72abb32a4c4207a76554c592cd4257a45d188ab 100644
--- a/bridge/bridges.go
+++ b/bridge/bridges.go
@@ -3,6 +3,7 @@ package bridge
 
 import (
 	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/gitea"
 	"github.com/MichaelMure/git-bug/bridge/github"
 	"github.com/MichaelMure/git-bug/bridge/gitlab"
 	"github.com/MichaelMure/git-bug/bridge/jira"
@@ -12,6 +13,7 @@ import (
 )
 
 func init() {
+	core.Register(&gitea.Gitea{})
 	core.Register(&github.Github{})
 	core.Register(&gitlab.Gitlab{})
 	core.Register(&launchpad.Launchpad{})
diff --git a/bridge/core/params.go b/bridge/core/params.go
index e398b81a517cc77725f0c535cea4d1d840b62408..d1d378570a48d1058a8dcbaa8a9fc26d5237b2f7 100644
--- a/bridge/core/params.go
+++ b/bridge/core/params.go
@@ -5,13 +5,13 @@ import "fmt"
 // BridgeParams holds parameters to simplify the bridge configuration without
 // having to make terminal prompts.
 type BridgeParams struct {
-	URL        string // complete URL of a repo               (Github, Gitlab,     , Launchpad)
-	BaseURL    string // base URL for self-hosted instance    (        Gitlab, Jira,          )
-	Login      string // username for the passed credential   (Github, Gitlab, Jira,          )
-	CredPrefix string // ID prefix of the credential to use   (Github, Gitlab, Jira,          )
-	TokenRaw   string // pre-existing token to use            (Github, Gitlab,     ,          )
-	Owner      string // owner of the repo                    (Github,       ,     ,          )
-	Project    string // name of the repo or project key      (Github,       , Jira, Launchpad)
+	URL        string // complete URL of a repo               (Gitea, Github, Gitlab,     , Launchpad)
+	BaseURL    string // base URL for self-hosted instance    (     ,       , Gitlab, Jira,          )
+	Login      string // username for the passed credential   (Gitea, Github, Gitlab, Jira,          )
+	CredPrefix string // ID prefix of the credential to use   (Gitea, Github, Gitlab, Jira,          )
+	TokenRaw   string // pre-existing token to use            (Gitea, Github, Gitlab,     ,          )
+	Owner      string // owner of the repo                    (     , Github,       ,     ,          )
+	Project    string // name of the repo or project key      (     , Github,       , Jira, Launchpad)
 }
 
 func (BridgeParams) fieldWarning(field string, target string) string {
diff --git a/bridge/gitea/config.go b/bridge/gitea/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..7a9f581b2fee2c1072c0d5930270dd811c3eef25
--- /dev/null
+++ b/bridge/gitea/config.go
@@ -0,0 +1,305 @@
+package gitea
+
+import (
+	"context"
+	"fmt"
+	"path"
+	"regexp"
+	"sort"
+	"strings"
+
+	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/commands/input"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+var (
+	ErrBadProjectURL = errors.New("bad project url")
+)
+
+func (g *Gitea) ValidParams() map[string]interface{} {
+	return map[string]interface{}{
+		"URL":        nil,
+		"Login":      nil,
+		"CredPrefix": nil,
+		"TokenRaw":   nil,
+	}
+}
+
+func (g *Gitea) Configure(repo *cache.RepoCache, params core.BridgeParams, interactive bool) (core.Configuration, error) {
+	var err error
+	var baseURL, owner, project string
+
+	// get project url
+	switch {
+	case params.URL != "":
+		baseURL, owner, project, err = splitURL(params.URL)
+		if err != nil {
+			return nil, err
+		}
+	default:
+		// terminal prompt
+		if !interactive {
+			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the gitea project URL via the --url option.")
+		}
+		baseURL, owner, project, err = promptURL(repo)
+		if err != nil {
+			return nil, errors.Wrap(err, "url prompt")
+		}
+	}
+
+	var login string
+	var cred auth.Credential
+
+	switch {
+	case params.CredPrefix != "":
+		cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
+		if err != nil {
+			return nil, err
+		}
+		l, ok := cred.GetMetadata(auth.MetaKeyLogin)
+		if !ok {
+			return nil, fmt.Errorf("credential doesn't have a login")
+		}
+		login = l
+	case params.TokenRaw != "":
+		token := auth.NewToken(target, params.TokenRaw)
+		login, err = getLoginFromToken(baseURL, token)
+		if err != nil {
+			return nil, err
+		}
+		token.SetMetadata(auth.MetaKeyLogin, login)
+		token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
+		cred = token
+	default:
+		if params.Login == "" {
+			if !interactive {
+				return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the login name via the --login option.")
+			}
+			// TODO: validate username
+			login, err = input.Prompt("Gitea login", "login", input.Required)
+		} else {
+			// TODO: validate username
+			login = params.Login
+		}
+		if err != nil {
+			return nil, err
+		}
+		if !interactive {
+			return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the access token via the --token option.")
+		}
+		cred, err = promptTokenOptions(repo, login, baseURL)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	token, ok := cred.(*auth.Token)
+	if !ok {
+		return nil, fmt.Errorf("the Gitea bridge only handle token credentials")
+	}
+
+	// verify access to the repository with token
+	_, err = validateProject(baseURL, owner, project, token)
+	if err != nil {
+		return nil, errors.Wrap(err, "project validation")
+	}
+
+	conf := make(core.Configuration)
+	conf[core.ConfigKeyTarget] = target
+	conf[confKeyBaseURL] = baseURL
+	conf[confKeyOwner] = owner
+	conf[confKeyProject] = project
+	conf[confKeyDefaultLogin] = login
+
+	err = g.ValidateConfig(conf)
+	if err != nil {
+		return nil, err
+	}
+
+	// don't forget to store the now known valid token
+	if !auth.IdExist(repo, cred.ID()) {
+		err = auth.Store(repo, cred)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return conf, core.FinishConfig(repo, metaKeyGiteaLogin, login)
+}
+
+func (g *Gitea) ValidateConfig(conf core.Configuration) error {
+	if v, ok := conf[core.ConfigKeyTarget]; !ok {
+		return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
+	} else if v != target {
+		return fmt.Errorf("unexpected target name: %v", v)
+	}
+	if _, ok := conf[confKeyBaseURL]; !ok {
+		return fmt.Errorf("missing %s key", confKeyBaseURL)
+	}
+	if _, ok := conf[confKeyOwner]; !ok {
+		return fmt.Errorf("missing %s key", confKeyOwner)
+	}
+	if _, ok := conf[confKeyProject]; !ok {
+		return fmt.Errorf("missing %s key", confKeyProject)
+	}
+	if _, ok := conf[confKeyDefaultLogin]; !ok {
+		return fmt.Errorf("missing %s key", confKeyDefaultLogin)
+	}
+
+	return nil
+}
+
+func promptTokenOptions(repo repository.RepoKeyring, login, baseURL string) (auth.Credential, error) {
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithKind(auth.KindToken),
+		auth.WithMeta(auth.MetaKeyLogin, login),
+		auth.WithMeta(auth.MetaKeyBaseURL, baseURL),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	cred, index, err := input.PromptCredential(target, "token", creds, []string{
+		"enter my token",
+	})
+	switch {
+	case err != nil:
+		return nil, err
+	case cred != nil:
+		return cred, nil
+	case index == 0:
+		return promptToken(baseURL)
+	default:
+		panic("missed case")
+	}
+}
+
+func promptToken(baseURL string) (*auth.Token, error) {
+	fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseURL, "user/settings/applications"))
+	fmt.Println()
+
+	re := regexp.MustCompile(`^[a-z0-9]{40}$`)
+
+	var login string
+
+	validator := func(name string, value string) (complaint string, err error) {
+		if !re.MatchString(value) {
+			return "token has incorrect format", nil
+		}
+		login, err = getLoginFromToken(baseURL, auth.NewToken(target, value))
+		if err != nil {
+			return fmt.Sprintf("token is invalid: %v", err), nil
+		}
+		return "", nil
+	}
+
+	rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
+	if err != nil {
+		return nil, err
+	}
+
+	token := auth.NewToken(target, rawToken)
+	token.SetMetadata(auth.MetaKeyLogin, login)
+	token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
+
+	return token, nil
+}
+
+func promptURL(repo repository.RepoCommon) (string, string, string, error) {
+	validRemotes, err := getRemoteURLs(repo)
+	if err != nil {
+		return "", "", "", err
+	}
+
+	validator := func(name, value string) (string, error) {
+		_, _, _, err := splitURL(value)
+		if err != nil {
+			return err.Error(), nil
+		}
+		return "", nil
+	}
+
+	url, err := input.PromptURLWithRemote("Gitea project URL", "URL", validRemotes, input.Required, input.IsURL, validator)
+	if err != nil {
+		return "", "", "", err
+	}
+
+	return splitURL(url)
+}
+
+func splitURL(url string) (baseURL, owner, project string, err error) {
+	cleanURL := strings.TrimSuffix(url, ".git")
+
+	re := regexp.MustCompile(`(.*)/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+)$`)
+
+	res := re.FindStringSubmatch(cleanURL)
+	if res == nil {
+		return "", "", "", ErrBadProjectURL
+	}
+
+	baseURL = res[1]
+	owner = res[2]
+	project = res[3]
+	return
+}
+
+func getRemoteURLs(repo repository.RepoCommon) ([]string, error) {
+	remotes, err := repo.GetRemotes()
+	if err != nil {
+		return nil, err
+	}
+
+	urls := make([]string, 0, len(remotes))
+	for _, url := range remotes {
+		urls = append(urls, url)
+	}
+
+	sort.Strings(urls)
+
+	return urls, nil
+}
+
+func validateProject(baseURL, owner, project string, token *auth.Token) (bool, error) {
+	client, err := buildClient(baseURL, token)
+	if err != nil {
+		return false, err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+	defer cancel()
+	client.SetContext(ctx)
+
+	_, _, err = client.GetRepo(owner, project)
+	if err != nil {
+		return false, errors.Wrap(err, "wrong token scope or non-existent project")
+	}
+
+	return true, nil
+}
+
+func getLoginFromToken(baseURL string, token *auth.Token) (string, error) {
+	client, err := buildClient(baseURL, token)
+	if err != nil {
+		return "", err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+	defer cancel()
+	client.SetContext(ctx)
+
+	user, _, err := client.GetMyUserInfo()
+	if err != nil {
+		return "", err
+	}
+	if user.UserName == "" {
+		return "", fmt.Errorf("gitea say username is empty")
+	}
+
+	return user.UserName, nil
+}
diff --git a/bridge/gitea/export.go b/bridge/gitea/export.go
new file mode 100644
index 0000000000000000000000000000000000000000..75bc2a2bd071863336f0d879625e24d0bd868ee7
--- /dev/null
+++ b/bridge/gitea/export.go
@@ -0,0 +1,29 @@
+package gitea
+
+import (
+	"context"
+	"syscall"
+	"time"
+
+	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/cache"
+)
+
+var (
+	ErrMissingIdentityToken = errors.New("missing identity token")
+)
+
+// giteaExporter implement the Exporter interface
+type giteaExporter struct {
+}
+
+// Init .
+func (ge *giteaExporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
+	return syscall.ENOSYS
+}
+
+func (ge *giteaExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
+	return nil, syscall.ENOSYS
+}
diff --git a/bridge/gitea/gitea.go b/bridge/gitea/gitea.go
new file mode 100644
index 0000000000000000000000000000000000000000..d64e1d919172021bd9bec599dc8a2db7c02c021d
--- /dev/null
+++ b/bridge/gitea/gitea.go
@@ -0,0 +1,57 @@
+package gitea
+
+import (
+	"time"
+
+	"code.gitea.io/sdk/gitea"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
+)
+
+const (
+	target = "gitea-preview"
+
+	metaKeyGiteaID      = "gitea-id"
+	metaKeyGiteaLogin   = "gitea-login"
+	metaKeyGiteaOwner   = "gitea-owner"
+	metaKeyGiteaProject = "gitea-project"
+	metaKeyGiteaBaseURL = "gitea-base-url"
+
+	confKeyOwner        = "owner"
+	confKeyProject      = "project"
+	confKeyBaseURL      = "base-url"
+	confKeyDefaultLogin = "default-login"
+
+	defaultTimeout = 60 * time.Second
+)
+
+var _ core.BridgeImpl = &Gitea{}
+
+type Gitea struct{}
+
+func (Gitea) Target() string {
+	return target
+}
+
+func (g *Gitea) LoginMetaKey() string {
+	return metaKeyGiteaLogin
+}
+
+func (Gitea) NewImporter() core.Importer {
+	return &giteaImporter{}
+}
+
+func (Gitea) NewExporter() core.Exporter {
+	return nil
+	// return &giteaExporter{}
+}
+
+func buildClient(baseURL string, token *auth.Token) (*gitea.Client, error) {
+	giteaClient, err := gitea.NewClient(baseURL, gitea.SetToken(token.Value))
+	if err != nil {
+		return nil, err
+	}
+
+	return giteaClient, nil
+}
diff --git a/bridge/gitea/import.go b/bridge/gitea/import.go
new file mode 100644
index 0000000000000000000000000000000000000000..ceb1b73a15d34982b1477a0ba80c370022f5e10b
--- /dev/null
+++ b/bridge/gitea/import.go
@@ -0,0 +1,189 @@
+package gitea
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"time"
+
+	"code.gitea.io/sdk/gitea"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/bridge/gitea/iterator"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entities/bug"
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/util/text"
+)
+
+// giteaImporter implement the Importer interface
+type giteaImporter struct {
+	conf core.Configuration
+
+	// default client
+	client *gitea.Client
+
+	// iterator
+	iterator *iterator.Iterator
+
+	// send only channel
+	out chan<- core.ImportResult
+}
+
+func (gi *giteaImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
+	gi.conf = conf
+
+	creds, err := auth.List(repo,
+		auth.WithTarget(target),
+		auth.WithKind(auth.KindToken),
+		auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyBaseURL]),
+		auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
+	)
+	if err != nil {
+		return err
+	}
+
+	if len(creds) == 0 {
+		return ErrMissingIdentityToken
+	}
+
+	gi.client, err = buildClient(conf[confKeyBaseURL], creds[0].(*auth.Token))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ImportAll iterate over all the configured repository issues (comments) and ensure the creation
+// of the missing issues / comments / label events / title changes ...
+func (gi *giteaImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
+	gi.iterator = iterator.NewIterator(ctx, gi.client, 10, gi.conf[confKeyOwner], gi.conf[confKeyProject], defaultTimeout, since)
+	out := make(chan core.ImportResult)
+	gi.out = out
+
+	go func() {
+		defer close(gi.out)
+
+		// Loop over all matching issues
+		for gi.iterator.NextIssue() {
+			issue := gi.iterator.IssueValue()
+
+			// create issue
+			b, err := gi.ensureIssue(repo, issue)
+			if err != nil {
+				err := fmt.Errorf("issue creation: %v", err)
+				out <- core.NewImportError(err, "")
+				return
+			}
+
+			// Loop over all comments
+
+			// Loop over all label events
+
+			if !b.NeedCommit() {
+				out <- core.NewImportNothing(b.Id(), "no imported operation")
+			} else if err := b.Commit(); err != nil {
+				// commit bug state
+				err := fmt.Errorf("bug commit: %v", err)
+				out <- core.NewImportError(err, "")
+				return
+			}
+		}
+
+		if err := gi.iterator.Error(); err != nil {
+			out <- core.NewImportError(err, "")
+		}
+	}()
+
+	return out, nil
+}
+
+func (gi *giteaImporter) ensureIssue(repo *cache.RepoCache, issue *gitea.Issue) (*cache.BugCache, error) {
+	// ensure issue author
+	author, err := gi.ensurePerson(repo, issue.Poster.UserName)
+	if err != nil {
+		return nil, err
+	}
+
+	giteaID := strconv.FormatInt(issue.Index, 10)
+
+	// resolve bug
+	b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
+		return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
+			excerpt.CreateMetadata[metaKeyGiteaID] == giteaID &&
+			excerpt.CreateMetadata[metaKeyGiteaBaseURL] == gi.conf[confKeyBaseURL] &&
+			excerpt.CreateMetadata[metaKeyGiteaOwner] == gi.conf[confKeyOwner] &&
+			excerpt.CreateMetadata[metaKeyGiteaProject] == gi.conf[confKeyProject]
+	})
+	if err == nil {
+		return b, nil
+	}
+	if err != bug.ErrBugNotExist {
+		return nil, err
+	}
+
+	// if bug was never imported, create bug
+	b, _, err = repo.NewBugRaw(
+		author,
+		issue.Created.Unix(),
+		text.CleanupOneLine(issue.Title),
+		text.Cleanup(issue.Body),
+		nil,
+		map[string]string{
+			core.MetaKeyOrigin:  target,
+			metaKeyGiteaID:      giteaID,
+			metaKeyGiteaOwner:   gi.conf[confKeyOwner],
+			metaKeyGiteaProject: gi.conf[confKeyProject],
+			metaKeyGiteaBaseURL: gi.conf[confKeyBaseURL],
+		},
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// importing a new bug
+	gi.out <- core.NewImportBug(b.Id())
+
+	return b, nil
+}
+
+func (gi *giteaImporter) ensurePerson(repo *cache.RepoCache, loginName string) (*cache.IdentityCache, error) {
+	// Look first in the cache
+	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGiteaLogin, loginName)
+	if err == nil {
+		return i, nil
+	}
+	if entity.IsErrMultipleMatch(err) {
+		return nil, err
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+	defer cancel()
+	gi.client.SetContext(ctx)
+
+	user, _, err := gi.client.GetUserInfo(loginName)
+	if err != nil {
+		return nil, err
+	}
+
+	i, err = repo.NewIdentityRaw(
+		user.FullName,
+		user.Email,
+		user.UserName,
+		user.AvatarURL,
+		nil,
+		map[string]string{
+			// because Gitea
+			metaKeyGiteaLogin: user.UserName,
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	gi.out <- core.NewImportIdentity(i.Id())
+	return i, nil
+}
diff --git a/bridge/gitea/iterator/comment.go b/bridge/gitea/iterator/comment.go
new file mode 100644
index 0000000000000000000000000000000000000000..afdcf0d53bb09c3a18e0ef93aa659f30ff70f0d6
--- /dev/null
+++ b/bridge/gitea/iterator/comment.go
@@ -0,0 +1,87 @@
+package iterator
+
+import (
+	"context"
+
+	"code.gitea.io/sdk/gitea"
+)
+
+type commentIterator struct {
+	issue    int64
+	page     int
+	lastPage bool
+	index    int
+	cache    []*gitea.Comment
+}
+
+func newCommentIterator() *commentIterator {
+	ci := &commentIterator{}
+	ci.Reset(-1)
+	return ci
+}
+
+func (ci *commentIterator) Next(ctx context.Context, conf config) (bool, error) {
+	// first query
+	if ci.cache == nil {
+		return ci.getNext(ctx, conf)
+	}
+
+	// move cursor index
+	if ci.index < len(ci.cache)-1 {
+		ci.index++
+		return true, nil
+	}
+
+	return ci.getNext(ctx, conf)
+}
+
+func (ci *commentIterator) Value() *gitea.Comment {
+	return ci.cache[ci.index]
+}
+
+func (ci *commentIterator) getNext(ctx context.Context, conf config) (bool, error) {
+	if ci.lastPage {
+		return false, nil
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, conf.timeout)
+	defer cancel()
+	conf.gc.SetContext(ctx)
+
+	comments, _, err := conf.gc.ListIssueComments(
+		conf.owner,
+		conf.project,
+		ci.issue,
+		gitea.ListIssueCommentOptions{
+			ListOptions: gitea.ListOptions{
+				Page:     ci.page,
+				PageSize: conf.capacity,
+			},
+		},
+	)
+
+	if err != nil {
+		ci.Reset(-1)
+		return false, err
+	}
+
+	ci.lastPage = true
+
+	if len(comments) == 0 {
+		return false, nil
+	}
+
+	ci.cache = comments
+	ci.index = 0
+	ci.page++
+
+	return true, nil
+}
+
+func (ci *commentIterator) Reset(issue int64) {
+	ci.issue = issue
+	ci.index = -1
+	ci.page = 1
+	ci.lastPage = false
+	ci.cache = nil
+}
diff --git a/bridge/gitea/iterator/issue.go b/bridge/gitea/iterator/issue.go
new file mode 100644
index 0000000000000000000000000000000000000000..5937baaa7d096081a42bc59a1fd3b2224e2da288
--- /dev/null
+++ b/bridge/gitea/iterator/issue.go
@@ -0,0 +1,96 @@
+package iterator
+
+import (
+	"context"
+	"strconv"
+
+	"code.gitea.io/sdk/gitea"
+)
+
+type issueIterator struct {
+	page     int
+	lastPage bool
+	index    int
+	cache    []*gitea.Issue
+}
+
+func newIssueIterator() *issueIterator {
+	ii := &issueIterator{}
+	ii.Reset()
+	return ii
+}
+
+func (ii *issueIterator) Next(ctx context.Context, conf config) (bool, error) {
+	// first query
+	if ii.cache == nil {
+		return ii.getNext(ctx, conf)
+	}
+
+	// move cursor index
+	if ii.index < len(ii.cache)-1 {
+		ii.index++
+		return true, nil
+	}
+
+	return ii.getNext(ctx, conf)
+}
+
+func (ii *issueIterator) Value() *gitea.Issue {
+	return ii.cache[ii.index]
+}
+
+func (ii *issueIterator) getNext(ctx context.Context, conf config) (bool, error) {
+	if ii.lastPage {
+		return false, nil
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, conf.timeout)
+	defer cancel()
+	conf.gc.SetContext(ctx)
+
+	issues, resp, err := conf.gc.ListRepoIssues(
+		conf.owner,
+		conf.project,
+		gitea.ListIssueOption{
+			ListOptions: gitea.ListOptions{
+				Page:     ii.page,
+				PageSize: conf.capacity,
+			},
+			State: gitea.StateAll,
+			Type:  gitea.IssueTypeIssue,
+		},
+	)
+
+	if err != nil {
+		ii.Reset()
+		return false, err
+	}
+
+	total, err := strconv.Atoi(resp.Header.Get("X-Total-Count"))
+	if err != nil {
+		ii.Reset()
+		return false, err
+	}
+
+	if total <= ii.page*conf.capacity {
+		ii.lastPage = true
+	}
+
+	// if repository doesn't have any issues
+	if len(issues) == 0 {
+		return false, nil
+	}
+
+	ii.cache = issues
+	ii.index = 0
+	ii.page++
+
+	return true, nil
+}
+
+func (ii *issueIterator) Reset() {
+	ii.index = -1
+	ii.page = 1
+	ii.lastPage = false
+	ii.cache = nil
+}
diff --git a/bridge/gitea/iterator/iterator.go b/bridge/gitea/iterator/iterator.go
new file mode 100644
index 0000000000000000000000000000000000000000..e3c701fa2934a73abd375a7e40474a6d3aff75f5
--- /dev/null
+++ b/bridge/gitea/iterator/iterator.go
@@ -0,0 +1,142 @@
+package iterator
+
+import (
+	"context"
+	"time"
+
+	"code.gitea.io/sdk/gitea"
+)
+
+type Iterator struct {
+	// shared context
+	ctx context.Context
+
+	// to pass to sub-iterators
+	conf config
+
+	// sticky error
+	err error
+
+	// issues iterator
+	issue *issueIterator
+
+	// comments iterator
+	comment *commentIterator
+
+	// labels iterator
+	label *labelIterator
+}
+
+type config struct {
+	// gitea api v1 client
+	gc *gitea.Client
+
+	timeout time.Duration
+
+	// if since is given the iterator will query only the issues
+	// updated after this date
+	since time.Time
+
+	// name of the repository owner on Gitea
+	owner string
+
+	// name of the Gitea repository
+	project string
+
+	// number of issues and notes to query at once
+	capacity int
+}
+
+// NewIterator create a new iterator
+func NewIterator(ctx context.Context, client *gitea.Client, capacity int, owner, project string, timeout time.Duration, since time.Time) *Iterator {
+	return &Iterator{
+		ctx: ctx,
+		conf: config{
+			gc:       client,
+			timeout:  timeout,
+			since:    since,
+			owner:    owner,
+			project:  project,
+			capacity: capacity,
+		},
+		issue:   newIssueIterator(),
+		comment: newCommentIterator(),
+		label:   newLabelIterator(),
+	}
+}
+
+// Error return last encountered error
+func (i *Iterator) Error() error {
+	return i.err
+}
+
+func (i *Iterator) NextIssue() bool {
+	if i.err != nil {
+		return false
+	}
+
+	if i.ctx.Err() != nil {
+		return false
+	}
+
+	more, err := i.issue.Next(i.ctx, i.conf)
+	if err != nil {
+		i.err = err
+		return false
+	}
+
+	// Also reset the other sub iterators as they would
+	// no longer be valid
+	i.comment.Reset(i.issue.Value().Index)
+	i.label.Reset(i.issue.Value().Index)
+
+	return more
+}
+
+func (i *Iterator) IssueValue() *gitea.Issue {
+	return i.issue.Value()
+}
+
+func (i *Iterator) NextComment() bool {
+	if i.err != nil {
+		return false
+	}
+
+	if i.ctx.Err() != nil {
+		return false
+	}
+
+	more, err := i.comment.Next(i.ctx, i.conf)
+	if err != nil {
+		i.err = err
+		return false
+	}
+
+	return more
+}
+
+func (i *Iterator) CommentValue() *gitea.Comment {
+	return i.comment.Value()
+}
+
+func (i *Iterator) NextLabel() bool {
+	if i.err != nil {
+		return false
+	}
+
+	if i.ctx.Err() != nil {
+		return false
+	}
+
+	more, err := i.label.Next(i.ctx, i.conf)
+	if err != nil {
+		i.err = err
+		return false
+	}
+
+	return more
+}
+
+func (i *Iterator) LabelValue() *gitea.Label {
+	return i.label.Value()
+}
diff --git a/bridge/gitea/iterator/label.go b/bridge/gitea/iterator/label.go
new file mode 100644
index 0000000000000000000000000000000000000000..f0ad21840c46348f20dec5cbe3e6d5528b4d3ae7
--- /dev/null
+++ b/bridge/gitea/iterator/label.go
@@ -0,0 +1,86 @@
+package iterator
+
+import (
+	"context"
+
+	"code.gitea.io/sdk/gitea"
+)
+
+type labelIterator struct {
+	issue    int64
+	page     int
+	lastPage bool
+	index    int
+	cache    []*gitea.Label
+}
+
+func newLabelIterator() *labelIterator {
+	li := &labelIterator{}
+	li.Reset(-1)
+	return li
+}
+
+func (li *labelIterator) Next(ctx context.Context, conf config) (bool, error) {
+	// first query
+	if li.cache == nil {
+		return li.getNext(ctx, conf)
+	}
+
+	// move cursor index
+	if li.index < len(li.cache)-1 {
+		li.index++
+		return true, nil
+	}
+
+	return li.getNext(ctx, conf)
+}
+
+func (li *labelIterator) Value() *gitea.Label {
+	return li.cache[li.index]
+}
+
+func (li *labelIterator) getNext(ctx context.Context, conf config) (bool, error) {
+	if li.lastPage {
+		return false, nil
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, conf.timeout)
+	defer cancel()
+	conf.gc.SetContext(ctx)
+
+	labels, _, err := conf.gc.GetIssueLabels(
+		conf.owner,
+		conf.project,
+		li.issue,
+		gitea.ListLabelsOptions{
+			ListOptions: gitea.ListOptions{
+				Page:     li.page,
+				PageSize: conf.capacity,
+			},
+		},
+	)
+	if err != nil {
+		li.Reset(-1)
+		return false, err
+	}
+
+	li.lastPage = true
+
+	if len(labels) == 0 {
+		return false, nil
+	}
+
+	li.cache = labels
+	li.index = 0
+	li.page++
+
+	return true, nil
+}
+
+func (li *labelIterator) Reset(issue int64) {
+	li.issue = issue
+	li.index = -1
+	li.page = 1
+	li.lastPage = false
+	li.cache = nil
+}
diff --git a/doc/man/git-bug-bridge-auth-add-token.1 b/doc/man/git-bug-bridge-auth-add-token.1
index dda8839fdc709783d5ed059db9dcd96c7322d2d8..2dbf741d74732c628e1624a31d72cfd5fe5cbaab 100644
--- a/doc/man/git-bug-bridge-auth-add-token.1
+++ b/doc/man/git-bug-bridge-auth-add-token.1
@@ -19,7 +19,7 @@ Store a new token
 .SH OPTIONS
 .PP
 \fB-t\fP, \fB--target\fP=""
-	The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+	The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
 
 .PP
 \fB-l\fP, \fB--login\fP=""
diff --git a/doc/man/git-bug-bridge-configure.1 b/doc/man/git-bug-bridge-configure.1
index ae01bf14e630cf1607ea70428e0930b8464099a7..ae34170f7d123d597df9131f0593f7f11130953e 100644
--- a/doc/man/git-bug-bridge-configure.1
+++ b/doc/man/git-bug-bridge-configure.1
@@ -29,7 +29,7 @@ Configure a new bridge by passing flags or/and using interactive terminal prompt
 
 .PP
 \fB-t\fP, \fB--target\fP=""
-	The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+	The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
 
 .PP
 \fB-u\fP, \fB--url\fP=""
diff --git a/doc/md/git-bug_bridge_auth_add-token.md b/doc/md/git-bug_bridge_auth_add-token.md
index faafaf61cdec4572deea09702594a2f5bcaab2d5..ded286a3eb5b19eaa3bca7317b2ba484642d5aa7 100644
--- a/doc/md/git-bug_bridge_auth_add-token.md
+++ b/doc/md/git-bug_bridge_auth_add-token.md
@@ -9,7 +9,7 @@ git-bug bridge auth add-token [TOKEN] [flags]
 ### Options
 
 ```
-  -t, --target string   The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+  -t, --target string   The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
   -l, --login string    The login in the remote bug-tracker
   -u, --user string     The user to add the token to. Default is the current user
   -h, --help            help for add-token
diff --git a/doc/md/git-bug_bridge_configure.md b/doc/md/git-bug_bridge_configure.md
index f71e294d5d158cf421f0be1e216e0930f2483434..18ac8ae2e7c190cf69cc92b4bd56fd4779010300 100644
--- a/doc/md/git-bug_bridge_configure.md
+++ b/doc/md/git-bug_bridge_configure.md
@@ -72,7 +72,7 @@ git bug bridge configure \
 
 ```
   -n, --name string         A distinctive name to identify the bridge
-  -t, --target string       The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+  -t, --target string       The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
   -u, --url string          The URL of the remote repository
   -b, --base-url string     The base URL of your remote issue tracker
   -l, --login string        The login on your remote issue tracker
diff --git a/go.mod b/go.mod
index 09782bee7cf8afc7c0400ececc8432a31d3de350..06f214bcd91daa910eea8cad62076b00b4aceb1c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/MichaelMure/git-bug
 go 1.18
 
 require (
+	code.gitea.io/sdk/gitea v0.14.0
 	github.com/99designs/gqlgen v0.17.17
 	github.com/99designs/keyring v1.2.1
 	github.com/MichaelMure/go-term-text v0.3.1
@@ -35,6 +36,7 @@ require (
 )
 
 require (
+	github.com/hashicorp/go-version v1.2.1 // indirect
 	github.com/lithammer/dedent v1.1.0 // indirect
 	github.com/owenrumney/go-sarif v1.0.11 // indirect
 	github.com/segmentio/fasthash v1.0.3 // indirect
diff --git a/go.sum b/go.sum
index f49a999d0ad6197c9560b57f731b15857aac9e68..165325ea083653b64bbb6eca2b2bb0dc921c8b2b 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+code.gitea.io/sdk/gitea v0.14.0 h1:m4J352I3p9+bmJUfS+g0odeQzBY/5OXP91Gv6D4fnJ0=
+code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
 github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
@@ -295,6 +297,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=