diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go
new file mode 100644
index 0000000000..88bbef70c7
--- /dev/null
+++ b/models/forgejo_migrations/migrate.go
@@ -0,0 +1,139 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forgejo_migrations //nolint:revive
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"xorm.io/xorm"
+	"xorm.io/xorm/names"
+)
+
+// ForgejoVersion describes the Forgejo version table. Should have only one row with id = 1.
+type ForgejoVersion struct {
+	ID      int64 `xorm:"pk autoincr"`
+	Version int64
+}
+
+type Migration struct {
+	description string
+	migrate     func(*xorm.Engine) error
+}
+
+// NewMigration creates a new migration.
+func NewMigration(desc string, fn func(*xorm.Engine) error) *Migration {
+	return &Migration{desc, fn}
+}
+
+// This is a sequence of additional Forgejo migrations.
+// Add new migrations to the bottom of the list.
+var migrations = []*Migration{}
+
+// GetCurrentDBVersion returns the current Forgejo database version.
+func GetCurrentDBVersion(x *xorm.Engine) (int64, error) {
+	if err := x.Sync(new(ForgejoVersion)); err != nil {
+		return -1, fmt.Errorf("sync: %w", err)
+	}
+
+	currentVersion := &ForgejoVersion{ID: 1}
+	has, err := x.Get(currentVersion)
+	if err != nil {
+		return -1, fmt.Errorf("get: %w", err)
+	}
+	if !has {
+		return -1, nil
+	}
+	return currentVersion.Version, nil
+}
+
+// ExpectedVersion returns the expected Forgejo database version.
+func ExpectedVersion() int64 {
+	return int64(len(migrations))
+}
+
+// EnsureUpToDate will check if the Forgejo database is at the correct version.
+func EnsureUpToDate(x *xorm.Engine) error {
+	currentDB, err := GetCurrentDBVersion(x)
+	if err != nil {
+		return err
+	}
+
+	if currentDB < 0 {
+		return fmt.Errorf("database has not been initialized")
+	}
+
+	expected := ExpectedVersion()
+
+	if currentDB != expected {
+		return fmt.Errorf(`current Forgejo database version %d is not equal to the expected version %d. Please run "forgejo [--config /path/to/app.ini] migrate" to update the database version`, currentDB, expected)
+	}
+
+	return nil
+}
+
+// Migrate Forgejo database to current version.
+func Migrate(x *xorm.Engine) error {
+	// Set a new clean the default mapper to GonicMapper as that is the default for .
+	x.SetMapper(names.GonicMapper{})
+	if err := x.Sync(new(ForgejoVersion)); err != nil {
+		return fmt.Errorf("sync: %w", err)
+	}
+
+	currentVersion := &ForgejoVersion{ID: 1}
+	has, err := x.Get(currentVersion)
+	if err != nil {
+		return fmt.Errorf("get: %w", err)
+	} else if !has {
+		// If the version record does not exist we think
+		// it is a fresh installation and we can skip all migrations.
+		currentVersion.ID = 0
+		currentVersion.Version = ExpectedVersion()
+
+		if _, err = x.InsertOne(currentVersion); err != nil {
+			return fmt.Errorf("insert: %w", err)
+		}
+	}
+
+	v := currentVersion.Version
+
+	// Downgrading Forgejo's database version not supported
+	if v > ExpectedVersion() {
+		msg := fmt.Sprintf("Your Forgejo database (migration version: %d) is for a newer version of Forgejo, you cannot use the newer database for this old Forgejo release (%d).", v, ExpectedVersion())
+		msg += "\nForgejo will exit to keep your database safe and unchanged. Please use the correct Forgejo release, do not change the migration version manually (incorrect manual operation may cause data loss)."
+		if !setting.IsProd {
+			msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE forgejo_version SET version=%d WHERE id=1;", ExpectedVersion())
+		}
+		_, _ = fmt.Fprintln(os.Stderr, msg)
+		log.Fatal(msg)
+		return nil
+	}
+
+	// Some migration tasks depend on the git command
+	if git.DefaultContext == nil {
+		if err = git.InitSimple(context.Background()); err != nil {
+			return err
+		}
+	}
+
+	// Migrate
+	for i, m := range migrations[v:] {
+		log.Info("Migration[%d]: %s", v+int64(i), m.description)
+		// Reset the mapper between each migration - migrations are not supposed to depend on each other
+		x.SetMapper(names.GonicMapper{})
+		if err = m.migrate(x); err != nil {
+			return fmt.Errorf("migration[%d]: %s failed: %w", v+int64(i), m.description, err)
+		}
+		currentVersion.Version = v + int64(i) + 1
+		if _, err = x.ID(1).Update(currentVersion); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 020043cfc3..a9bec5e4ac 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"fmt"
 
+	"code.gitea.io/gitea/models/forgejo_migrations"
 	"code.gitea.io/gitea/models/migrations/v1_10"
 	"code.gitea.io/gitea/models/migrations/v1_11"
 	"code.gitea.io/gitea/models/migrations/v1_12"
@@ -649,5 +650,7 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
 			return err
 		}
 	}
-	return nil
+
+	// Execute Forgejo specific migrations.
+	return forgejo_migrations.Migrate(x)
 }