diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 1c8563cebe..93a736630e 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -560,6 +560,8 @@ var migrations = []Migration{
 	NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun),
 	// v286 -> v287
 	NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
+	// v287 -> v288
+	NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go
new file mode 100644
index 0000000000..c8b1593286
--- /dev/null
+++ b/models/migrations/v1_22/v287.go
@@ -0,0 +1,46 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+type BadgeUnique struct {
+	ID   int64  `xorm:"pk autoincr"`
+	Slug string `xorm:"UNIQUE"`
+}
+
+func (BadgeUnique) TableName() string {
+	return "badge"
+}
+
+func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error {
+	type Badge struct {
+		Slug string
+	}
+
+	err := x.Sync(new(Badge))
+	if err != nil {
+		return err
+	}
+
+	sess := x.NewSession()
+	defer sess.Close()
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	_, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL")
+	if err != nil {
+		return err
+	}
+
+	err = sess.Sync(new(BadgeUnique))
+	if err != nil {
+		return err
+	}
+
+	return sess.Commit()
+}