diff --git a/locales/index.d.ts b/locales/index.d.ts
index 363032eaa258134b14ba72a01c779f686d98fa32..11be41235a4e33747815194a2b8dec83873f8da1 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1142,6 +1142,7 @@ export interface Locale {
     "privacyPolicy": string;
     "privacyPolicyUrl": string;
     "tosAndPrivacyPolicy": string;
+    "avatarDecorations": string;
     "_announcement": {
         "forExistingUsers": string;
         "forExistingUsersDescription": string;
@@ -2295,6 +2296,9 @@ export interface Locale {
         "createAd": string;
         "deleteAd": string;
         "updateAd": string;
+        "createAvatarDecoration": string;
+        "updateAvatarDecoration": string;
+        "deleteAvatarDecoration": string;
     };
     "_fileViewer": {
         "title": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index f1b57f8bdea4bf59d488c4facb98a4041e4c8cbb..11b083392832fe76fc18f80abf911ab75affb541 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1139,6 +1139,7 @@ impressumDescription: "ドイツなどの一部の国と地域では表示が義
 privacyPolicy: "プライバシーポリシー"
 privacyPolicyUrl: "プライバシーポリシーURL"
 tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
+avatarDecorations: "アイコンデコレーション"
 
 _announcement:
   forExistingUsers: "既存ユーザーのみ"
@@ -2208,6 +2209,9 @@ _moderationLogTypes:
   createAd: "広告を作成"
   deleteAd: "広告を削除"
   updateAd: "広告を更新"
+  createAvatarDecoration: "アイコンデコレーションを作成"
+  updateAvatarDecoration: "アイコンデコレーションを更新"
+  deleteAvatarDecoration: "アイコンデコレーションを削除"
 
 _fileViewer:
   title: "ファイルの詳細"
diff --git a/packages/backend/migration/1697847397844-avatar-decoration.js b/packages/backend/migration/1697847397844-avatar-decoration.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f221397466637a341fc1c703b3594b6d9a86a48
--- /dev/null
+++ b/packages/backend/migration/1697847397844-avatar-decoration.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AvatarDecoration1697847397844 {
+    name = 'AvatarDecoration1697847397844'
+
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "avatar_decoration" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "url" character varying(1024) NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(2048) NOT NULL, "roleIdsThatCanBeUsedThisDecoration" character varying(128) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_b6de9296f6097078e1dc53f7603" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
+        await queryRunner.query(`DROP TABLE "avatar_decoration"`);
+    }
+}
diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e97946f9dc0e97b7ba1cf511620e66d2dbc5a66f
--- /dev/null
+++ b/packages/backend/src/core/AvatarDecorationService.ts
@@ -0,0 +1,129 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { MemorySingleCache } from '@/misc/cache.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+@Injectable()
+export class AvatarDecorationService implements OnApplicationShutdown {
+	public cache: MemorySingleCache<MiAvatarDecoration[]>;
+
+	constructor(
+		@Inject(DI.redisForSub)
+		private redisForSub: Redis.Redis,
+
+		@Inject(DI.avatarDecorationsRepository)
+		private avatarDecorationsRepository: AvatarDecorationsRepository,
+
+		private idService: IdService,
+		private moderationLogService: ModerationLogService,
+		private globalEventService: GlobalEventService,
+	) {
+		this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
+
+		this.redisForSub.on('message', this.onMessage);
+	}
+
+	@bindThis
+	private async onMessage(_: string, data: string): Promise<void> {
+		const obj = JSON.parse(data);
+
+		if (obj.channel === 'internal') {
+			const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+			switch (type) {
+				case 'avatarDecorationCreated':
+				case 'avatarDecorationUpdated':
+				case 'avatarDecorationDeleted': {
+					this.cache.delete();
+					break;
+				}
+				default:
+					break;
+			}
+		}
+	}
+
+	@bindThis
+	public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
+		const created = await this.avatarDecorationsRepository.insert({
+			id: this.idService.gen(),
+			...options,
+		}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
+
+		this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
+
+		if (moderator) {
+			this.moderationLogService.log(moderator, 'createAvatarDecoration', {
+				avatarDecorationId: created.id,
+				avatarDecoration: created,
+			});
+		}
+
+		return created;
+	}
+
+	@bindThis
+	public async update(id: MiAvatarDecoration['id'], params: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<void> {
+		const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
+
+		const date = new Date();
+		await this.avatarDecorationsRepository.update(avatarDecoration.id, {
+			updatedAt: date,
+			...params,
+		});
+
+		const updated = await this.avatarDecorationsRepository.findOneByOrFail({ id: avatarDecoration.id });
+		this.globalEventService.publishInternalEvent('avatarDecorationUpdated', updated);
+
+		if (moderator) {
+			this.moderationLogService.log(moderator, 'updateAvatarDecoration', {
+				avatarDecorationId: avatarDecoration.id,
+				before: avatarDecoration,
+				after: updated,
+			});
+		}
+	}
+
+	@bindThis
+	public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise<void> {
+		const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
+
+		await this.avatarDecorationsRepository.delete({ id: avatarDecoration.id });
+		this.globalEventService.publishInternalEvent('avatarDecorationDeleted', avatarDecoration);
+
+		if (moderator) {
+			this.moderationLogService.log(moderator, 'deleteAvatarDecoration', {
+				avatarDecorationId: avatarDecoration.id,
+				avatarDecoration: avatarDecoration,
+			});
+		}
+	}
+
+	@bindThis
+	public async getAll(noCache = false): Promise<MiAvatarDecoration[]> {
+		if (noCache) {
+			this.cache.delete();
+		}
+		return this.cache.fetch(() => this.avatarDecorationsRepository.find());
+	}
+
+	@bindThis
+	public dispose(): void {
+		this.redisForSub.off('message', this.onMessage);
+	}
+
+	@bindThis
+	public onApplicationShutdown(signal?: string | undefined): void {
+		this.dispose();
+	}
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index e7e66646fc54ff0b4fd9899297dab8a5f0d46395..b46afb1909a32589b50a3b545d1fcc9dc1c5a4b1 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -11,6 +11,7 @@ import { AnnouncementService } from './AnnouncementService.js';
 import { AntennaService } from './AntennaService.js';
 import { AppLockService } from './AppLockService.js';
 import { AchievementService } from './AchievementService.js';
+import { AvatarDecorationService } from './AvatarDecorationService.js';
 import { CaptchaService } from './CaptchaService.js';
 import { CreateSystemUserService } from './CreateSystemUserService.js';
 import { CustomEmojiService } from './CustomEmojiService.js';
@@ -140,6 +141,7 @@ const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExis
 const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
 const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
 const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
+const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
 const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
 const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
 const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
@@ -273,6 +275,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		AntennaService,
 		AppLockService,
 		AchievementService,
+		AvatarDecorationService,
 		CaptchaService,
 		CreateSystemUserService,
 		CustomEmojiService,
@@ -399,6 +402,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$AntennaService,
 		$AppLockService,
 		$AchievementService,
+		$AvatarDecorationService,
 		$CaptchaService,
 		$CreateSystemUserService,
 		$CustomEmojiService,
@@ -526,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		AntennaService,
 		AppLockService,
 		AchievementService,
+		AvatarDecorationService,
 		CaptchaService,
 		CreateSystemUserService,
 		CustomEmojiService,
@@ -651,6 +656,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$AntennaService,
 		$AppLockService,
 		$AchievementService,
+		$AvatarDecorationService,
 		$CaptchaService,
 		$CreateSystemUserService,
 		$CustomEmojiService,
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index b74fbbe584a910facad40780ec32f7ca99cac639..bfbdecf688d2c0baf71bc626cf2baa446ff4d04f 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js';
 import type { MiPage } from '@/models/Page.js';
 import type { MiWebhook } from '@/models/Webhook.js';
 import type { MiMeta } from '@/models/Meta.js';
-import { MiRole, MiRoleAssignment } from '@/models/_.js';
+import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js';
 import type { Packed } from '@/misc/json-schema.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
@@ -188,6 +188,9 @@ export interface InternalEventTypes {
 	antennaCreated: MiAntenna;
 	antennaDeleted: MiAntenna;
 	antennaUpdated: MiAntenna;
+	avatarDecorationCreated: MiAvatarDecoration;
+	avatarDecorationDeleted: MiAvatarDecoration;
+	avatarDecorationUpdated: MiAvatarDecoration;
 	metaUpdated: MiMeta;
 	followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
 	unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 2c2ff7af1dcce3472354c36a74dfadfae9d747d1..ef05920d501840dccfd426658fd291f699823042 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -227,6 +227,12 @@ export class RoleService implements OnApplicationShutdown {
 		}
 	}
 
+	@bindThis
+	public async getRoles() {
+		const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
+		return roles;
+	}
+
 	@bindThis
 	public async getUserAssigns(userId: MiUser['id']) {
 		const now = Date.now();
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b0577fc1a098270224683aadbd42f5a7efabfd7b..66facce4c26f4b5fb2b6e95c988e8f3c5102da9e 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -21,9 +21,10 @@ import { RoleService } from '@/core/RoleService.js';
 import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { IdService } from '@/core/IdService.js';
+import type { AnnouncementService } from '@/core/AnnouncementService.js';
+import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
 import type { OnModuleInit } from '@nestjs/common';
-import type { AnnouncementService } from '../AnnouncementService.js';
-import type { CustomEmojiService } from '../CustomEmojiService.js';
 import type { NoteEntityService } from './NoteEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 import type { PageEntityService } from './PageEntityService.js';
@@ -62,6 +63,7 @@ export class UserEntityService implements OnModuleInit {
 	private roleService: RoleService;
 	private federatedInstanceService: FederatedInstanceService;
 	private idService: IdService;
+	private avatarDecorationService: AvatarDecorationService;
 
 	constructor(
 		private moduleRef: ModuleRef,
@@ -126,6 +128,7 @@ export class UserEntityService implements OnModuleInit {
 		this.roleService = this.moduleRef.get('RoleService');
 		this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
 		this.idService = this.moduleRef.get('IdService');
+		this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
 	}
 
 	//#region Validators
@@ -328,8 +331,6 @@ export class UserEntityService implements OnModuleInit {
 				...announcement,
 			})) : null;
 
-		const falsy = opts.detail ? false : undefined;
-
 		const packed = {
 			id: user.id,
 			name: user.name,
@@ -337,6 +338,10 @@ export class UserEntityService implements OnModuleInit {
 			host: user.host,
 			avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
 			avatarBlurhash: user.avatarBlurhash,
+			avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => decorations.filter(decoration => user.avatarDecorations.includes(decoration.id)).map(decoration => ({
+				id: decoration.id,
+				url: decoration.url,
+			}))) : [],
 			isBot: user.isBot,
 			isCat: user.isCat,
 			instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index edcdd21d606ac748d68793e0f1559c869cf7dbf9..8411cb8229042d5310f7aa24de2060ff07a38145 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -18,6 +18,7 @@ export const DI = {
 	announcementsRepository: Symbol('announcementsRepository'),
 	announcementReadsRepository: Symbol('announcementReadsRepository'),
 	appsRepository: Symbol('appsRepository'),
+	avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
 	noteFavoritesRepository: Symbol('noteFavoritesRepository'),
 	noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
 	noteReactionsRepository: Symbol('noteReactionsRepository'),
diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08ebbdeac1449c67d651c633d980afa956109ed1
--- /dev/null
+++ b/packages/backend/src/models/AvatarDecoration.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { id } from './util/id.js';
+
+@Entity('avatar_decoration')
+export class MiAvatarDecoration {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public updatedAt: Date | null;
+
+	@Column('varchar', {
+		length: 1024,
+	})
+	public url: string;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public name: string;
+
+	@Column('varchar', {
+		length: 2048,
+	})
+	public description: string;
+
+	// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
+	@Column('varchar', {
+		array: true, length: 128, default: '{}',
+	})
+	public roleIdsThatCanBeUsedThisDecoration: string[];
+}
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 9efd6841b180e3674c6b4e95e1cb88be1a8ab47e..866fdfe6d423cc70a7af583adb03cfcdd2578649 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -5,7 +5,7 @@
 
 import { Module } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
+import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
 import type { DataSource } from 'typeorm';
 import type { Provider } from '@nestjs/common';
 
@@ -39,6 +39,12 @@ const $appsRepository: Provider = {
 	inject: [DI.db],
 };
 
+const $avatarDecorationsRepository: Provider = {
+	provide: DI.avatarDecorationsRepository,
+	useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
+	inject: [DI.db],
+};
+
 const $noteFavoritesRepository: Provider = {
 	provide: DI.noteFavoritesRepository,
 	useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
@@ -402,6 +408,7 @@ const $userMemosRepository: Provider = {
 		$announcementsRepository,
 		$announcementReadsRepository,
 		$appsRepository,
+		$avatarDecorationsRepository,
 		$noteFavoritesRepository,
 		$noteThreadMutingsRepository,
 		$noteReactionsRepository,
@@ -468,6 +475,7 @@ const $userMemosRepository: Provider = {
 		$announcementsRepository,
 		$announcementReadsRepository,
 		$appsRepository,
+		$avatarDecorationsRepository,
 		$noteFavoritesRepository,
 		$noteThreadMutingsRepository,
 		$noteReactionsRepository,
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 796d7c8356d7b16d2827eb7294477bdb0ce86ac1..c98426a7b6bacb253c5f097f8850b7c718d4b52c 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -138,6 +138,11 @@ export class MiUser {
 	})
 	public bannerBlurhash: string | null;
 
+	@Column('varchar', {
+		length: 512, array: true, default: '{}',
+	})
+	public avatarDecorations: string[];
+
 	@Index()
 	@Column('varchar', {
 		length: 128, array: true, default: '{}',
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index f974f95ed89fcc1c8f667da2699b6a611222847c..d7c327f164728c21c92fa115fa070519d097d14f 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -10,6 +10,7 @@ import { MiAnnouncement } from '@/models/Announcement.js';
 import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
 import { MiAntenna } from '@/models/Antenna.js';
 import { MiApp } from '@/models/App.js';
+import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
 import { MiAuthSession } from '@/models/AuthSession.js';
 import { MiBlocking } from '@/models/Blocking.js';
 import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
@@ -77,6 +78,7 @@ export {
 	MiAnnouncementRead,
 	MiAntenna,
 	MiApp,
+	MiAvatarDecoration,
 	MiAuthSession,
 	MiBlocking,
 	MiChannelFollowing,
@@ -143,6 +145,7 @@ export type AnnouncementsRepository = Repository<MiAnnouncement>;
 export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>;
 export type AntennasRepository = Repository<MiAntenna>;
 export type AppsRepository = Repository<MiApp>;
+export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>;
 export type AuthSessionsRepository = Repository<MiAuthSession>;
 export type BlockingsRepository = Repository<MiBlocking>;
 export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 57d2d976ff7a5727d5d2bb2e338a8dc2ed60ff6c..bf283fbeb2d06cf48208faa7297837bb3b47763c 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -37,6 +37,26 @@ export const packedUserLiteSchema = {
 			type: 'string',
 			nullable: true, optional: false,
 		},
+		avatarDecorations: {
+			type: 'array',
+			nullable: false, optional: false,
+			items: {
+				type: 'object',
+				nullable: false, optional: false,
+				properties: {
+					id: {
+						type: 'string',
+						nullable: false, optional: false,
+						format: 'id',
+					},
+					url: {
+						type: 'string',
+						format: 'url',
+						nullable: false, optional: false,
+					},
+				},
+			},
+		},
 		isAdmin: {
 			type: 'boolean',
 			nullable: false, optional: true,
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index d4c6ad82ce48857f5f1be6fea7ea2a988a740524..cd611839a430f69ffe63ea1067842f3f96b17d27 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -18,6 +18,7 @@ import { MiAnnouncement } from '@/models/Announcement.js';
 import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
 import { MiAntenna } from '@/models/Antenna.js';
 import { MiApp } from '@/models/App.js';
+import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
 import { MiAuthSession } from '@/models/AuthSession.js';
 import { MiBlocking } from '@/models/Blocking.js';
 import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
@@ -129,6 +130,7 @@ export const entities = [
 	MiMeta,
 	MiInstance,
 	MiApp,
+	MiAvatarDecoration,
 	MiAuthSession,
 	MiAccessToken,
 	MiUser,
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index f834561456e17d6777527a2bc6d7d12456e784d9..f234a2637dc1b6d5cccae799285142541e1217bd 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -18,6 +18,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
 import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
 import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
 import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
+import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
+import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
+import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
+import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@@ -176,6 +180,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
 import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
 import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
 import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
+import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
 import * as ep___hashtags_list from './endpoints/hashtags/list.js';
 import * as ep___hashtags_search from './endpoints/hashtags/search.js';
 import * as ep___hashtags_show from './endpoints/hashtags/show.js';
@@ -368,6 +373,10 @@ const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements
 const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default };
 const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default };
 const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default };
+const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default };
+const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
+const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
+const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
 const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
 const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
 const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
@@ -526,6 +535,7 @@ const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useCla
 const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default };
 const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default };
 const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default };
+const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default };
 const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default };
 const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default };
 const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default };
@@ -722,6 +732,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_announcements_delete,
 		$admin_announcements_list,
 		$admin_announcements_update,
+		$admin_avatarDecorations_create,
+		$admin_avatarDecorations_delete,
+		$admin_avatarDecorations_list,
+		$admin_avatarDecorations_update,
 		$admin_deleteAllFilesOfAUser,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
@@ -880,6 +894,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$gallery_posts_unlike,
 		$gallery_posts_update,
 		$getOnlineUsersCount,
+		$getAvatarDecorations,
 		$hashtags_list,
 		$hashtags_search,
 		$hashtags_show,
@@ -1070,6 +1085,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_announcements_delete,
 		$admin_announcements_list,
 		$admin_announcements_update,
+		$admin_avatarDecorations_create,
+		$admin_avatarDecorations_delete,
+		$admin_avatarDecorations_list,
+		$admin_avatarDecorations_update,
 		$admin_deleteAllFilesOfAUser,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
@@ -1228,6 +1247,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$gallery_posts_unlike,
 		$gallery_posts_update,
 		$getOnlineUsersCount,
+		$getAvatarDecorations,
 		$hashtags_list,
 		$hashtags_search,
 		$hashtags_show,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index d12a035afac2f80725e4447c257424c1adb3976e..8d34edca9d70105668f366b1f6280f38be71b11c 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -18,6 +18,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
 import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
 import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
 import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
+import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
+import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
+import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
+import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@@ -176,6 +180,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
 import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
 import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
 import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
+import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
 import * as ep___hashtags_list from './endpoints/hashtags/list.js';
 import * as ep___hashtags_search from './endpoints/hashtags/search.js';
 import * as ep___hashtags_show from './endpoints/hashtags/show.js';
@@ -366,6 +371,10 @@ const eps = [
 	['admin/announcements/delete', ep___admin_announcements_delete],
 	['admin/announcements/list', ep___admin_announcements_list],
 	['admin/announcements/update', ep___admin_announcements_update],
+	['admin/avatar-decorations/create', ep___admin_avatarDecorations_create],
+	['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
+	['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
+	['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
 	['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
 	['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
 	['admin/drive/cleanup', ep___admin_drive_cleanup],
@@ -524,6 +533,7 @@ const eps = [
 	['gallery/posts/unlike', ep___gallery_posts_unlike],
 	['gallery/posts/update', ep___gallery_posts_update],
 	['get-online-users-count', ep___getOnlineUsersCount],
+	['get-avatar-decorations', ep___getAvatarDecorations],
 	['hashtags/list', ep___hashtags_list],
 	['hashtags/search', ep___hashtags_search],
 	['hashtags/show', ep___hashtags_show],
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c1869b141ab1cddd8cd34309bec931af9e116378
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		name: { type: 'string', minLength: 1 },
+		description: { type: 'string' },
+		url: { type: 'string', minLength: 1 },
+		roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
+			type: 'string',
+		} },
+	},
+	required: ['name', 'description', 'url'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private avatarDecorationService: AvatarDecorationService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			await this.avatarDecorationService.create({
+				name: ps.name,
+				description: ps.description,
+				url: ps.url,
+				roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
+			}, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5aba24b4267392c85c420c521a6ddf89cf85c5e5
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	errors: {
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['id'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private avatarDecorationService: AvatarDecorationService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			await this.avatarDecorationService.delete(ps.id, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9a32a590818b9631816f522d2278bde41cd43f36
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -0,0 +1,101 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
+import type { MiAnnouncement } from '@/models/Announcement.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { DI } from '@/di-symbols.js';
+import { IdService } from '@/core/IdService.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			optional: false, nullable: false,
+			properties: {
+				id: {
+					type: 'string',
+					optional: false, nullable: false,
+					format: 'id',
+					example: 'xxxxxxxxxx',
+				},
+				createdAt: {
+					type: 'string',
+					optional: false, nullable: false,
+					format: 'date-time',
+				},
+				updatedAt: {
+					type: 'string',
+					optional: false, nullable: true,
+					format: 'date-time',
+				},
+				name: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+				description: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+				url: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+				roleIdsThatCanBeUsedThisDecoration: {
+					type: 'array',
+					optional: false, nullable: false,
+					items: {
+						type: 'string',
+						optional: false, nullable: false,
+						format: 'id',
+					},
+				},
+			},
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+		sinceId: { type: 'string', format: 'misskey:id' },
+		untilId: { type: 'string', format: 'misskey:id' },
+		userId: { type: 'string', format: 'misskey:id', nullable: true },
+	},
+	required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private avatarDecorationService: AvatarDecorationService,
+		private idService: IdService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const avatarDecorations = await this.avatarDecorationService.getAll(true);
+
+			return avatarDecorations.map(avatarDecoration => ({
+				id: avatarDecoration.id,
+				createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(),
+				updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null,
+				name: avatarDecoration.name,
+				description: avatarDecoration.description,
+				url: avatarDecoration.url,
+				roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
+			}));
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
new file mode 100644
index 0000000000000000000000000000000000000000..564014a3df5b76b7deb4aad11c640424c50d16ca
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	errors: {
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		id: { type: 'string', format: 'misskey:id' },
+		name: { type: 'string', minLength: 1 },
+		description: { type: 'string' },
+		url: { type: 'string', minLength: 1 },
+		roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
+			type: 'string',
+		} },
+	},
+	required: ['id'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private avatarDecorationService: AvatarDecorationService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			await this.avatarDecorationService.update(ps.id, {
+				name: ps.name,
+				description: ps.description,
+				url: ps.url,
+				roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
+			}, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ec602a0dc5a1158642d9e40a1ca2302fef07b4ea
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts
@@ -0,0 +1,79 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { IsNull } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false,
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			optional: false, nullable: false,
+			properties: {
+				id: {
+					type: 'string',
+					optional: false, nullable: false,
+					format: 'id',
+					example: 'xxxxxxxxxx',
+				},
+				name: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+				description: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+				url: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+				roleIdsThatCanBeUsedThisDecoration: {
+					type: 'array',
+					optional: false, nullable: false,
+					items: {
+						type: 'string',
+						optional: false, nullable: false,
+						format: 'id',
+					},
+				},
+			},
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {},
+	required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private avatarDecorationService: AvatarDecorationService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const decorations = await this.avatarDecorationService.getAll(true);
+
+			return decorations.map(decoration => ({
+				id: decoration.id,
+				name: decoration.name,
+				description: decoration.description,
+				url: decoration.url,
+				roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration,
+			}));
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 431bb4c60a7e9ab8577067f15b6df05564af332f..f1837e708221bc6557547730b5ca643fac94e67f 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -32,6 +32,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
 import { HttpRequestService } from '@/core/HttpRequestService.js';
 import type { Config } from '@/config.js';
 import { safeForSql } from '@/misc/safe-for-sql.js';
+import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
 import { ApiLoggerService } from '../../ApiLoggerService.js';
 import { ApiError } from '../../error.js';
 
@@ -131,6 +132,9 @@ export const paramDef = {
 		birthday: { ...birthdaySchema, nullable: true },
 		lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
 		avatarId: { type: 'string', format: 'misskey:id', nullable: true },
+		avatarDecorations: { type: 'array', maxItems: 1, items: {
+			type: 'string',
+		} },
 		bannerId: { type: 'string', format: 'misskey:id', nullable: true },
 		fields: {
 			type: 'array',
@@ -207,6 +211,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private roleService: RoleService,
 		private cacheService: CacheService,
 		private httpRequestService: HttpRequestService,
+		private avatarDecorationService: AvatarDecorationService,
 	) {
 		super(meta, paramDef, async (ps, _user, token) => {
 			const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@@ -296,6 +301,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				updates.bannerBlurhash = null;
 			}
 
+			if (ps.avatarDecorations) {
+				const decorations = await this.avatarDecorationService.getAll(true);
+				const myRoles = await this.roleService.getUserRoles(user.id);
+				const allRoles = await this.roleService.getRoles();
+				const decorationIds = decorations
+					.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
+					.map(d => d.id);
+
+				updates.avatarDecorations = ps.avatarDecorations.filter(id => decorationIds.includes(id));
+			}
+
 			if (ps.pinnedPageId) {
 				const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
 
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 316073c992b10a1d0ef616394fa7f0bfb2d4ba56..69224360b3c683d4f3dc04edc414a47b82719c82 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -60,6 +60,9 @@ export const moderationLogTypes = [
 	'createAd',
 	'updateAd',
 	'deleteAd',
+	'createAvatarDecoration',
+	'updateAvatarDecoration',
+	'deleteAvatarDecoration',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -221,6 +224,19 @@ export type ModerationLogPayloads = {
 		adId: string;
 		ad: any;
 	};
+	createAvatarDecoration: {
+		avatarDecorationId: string;
+		avatarDecoration: any;
+	};
+	updateAvatarDecoration: {
+		avatarDecorationId: string;
+		before: any;
+		after: any;
+	};
+	deleteAvatarDecoration: {
+		avatarDecorationId: string;
+		avatarDecoration: any;
+	};
 };
 
 export type Serialized<T> = {
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 53db1ac28a006a9dfed96bd7745acc4ce42afbb7..520d9b14e4037cb4d42c8b4bb7e9e4b1ebae4200 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -68,6 +68,7 @@ describe('ユーザー', () => {
 			host: user.host,
 			avatarUrl: user.avatarUrl,
 			avatarBlurhash: user.avatarBlurhash,
+			avatarDecorations: user.avatarDecorations,
 			isBot: user.isBot,
 			isCat: user.isCat,
 			instance: user.instance,
@@ -349,6 +350,7 @@ describe('ユーザー', () => {
 		assert.strictEqual(response.host, null);
 		assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
 		assert.strictEqual(response.avatarBlurhash, null);
+		assert.deepStrictEqual(response.avatarDecorations, []);
 		assert.strictEqual(response.isBot, false);
 		assert.strictEqual(response.isCat, false);
 		assert.strictEqual(response.instance, undefined);
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 811c243926c9acadcbee8d3441e7b9e3206d9a46..c2e6ee52f3f11fca8bc8aa3d30ac5ea49c364b11 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -74,6 +74,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
 		onlineStatus: 'unknown',
 		avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
 		avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
+		avatarDecorations: [],
 		emojis: [],
 		bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
 		bannerColor: '#000000',
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 27c25b949020c8bd53997f0f516e0b9105bbcba0..de684425a2e2c13a528ece28d3871abe6e7c6a8b 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
-	<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
+	<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
 	<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
 	<div v-if="user.isCat" :class="[$style.ears]">
 		<div :class="$style.earLeft">
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</div>
 	</div>
+	<img v-if="decoration || user.avatarDecorations.length > 0" :class="[$style.decoration]" :src="decoration ?? user.avatarDecorations[0].url" alt="">
 </component>
 </template>
 
@@ -47,6 +48,7 @@ const props = withDefaults(defineProps<{
 	link?: boolean;
 	preview?: boolean;
 	indicator?: boolean;
+	decoration?: string;
 }>(), {
 	target: null,
 	link: false,
@@ -134,7 +136,7 @@ watch(() => props.user.avatarBlurhash, () => {
 
 .indicator {
 	position: absolute;
-	z-index: 1;
+	z-index: 2;
 	bottom: 0;
 	left: 0;
 	width: 20%;
@@ -278,4 +280,13 @@ watch(() => props.user.avatarBlurhash, () => {
 		}
 	}
 }
+
+.decoration {
+	position: absolute;
+	z-index: 1;
+	top: -50%;
+	left: -50%;
+	width: 200%;
+	pointer-events: none;
+}
 </style>
diff --git a/packages/frontend/src/pages/admin/avatar-decorations.vue b/packages/frontend/src/pages/admin/avatar-decorations.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b4007e6d207e8a96b838343037df66bf63870cb3
--- /dev/null
+++ b/packages/frontend/src/pages/admin/avatar-decorations.vue
@@ -0,0 +1,103 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+	<MkSpacer :contentMax="900">
+		<div class="_gaps">
+			<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
+				<template #label>{{ avatarDecoration.name }}</template>
+				<template #caption>{{ avatarDecoration.description }}</template>
+
+				<div class="_gaps_m">
+					<MkInput v-model="avatarDecoration.name">
+						<template #label>{{ i18n.ts.name }}</template>
+					</MkInput>
+					<MkTextarea v-model="avatarDecoration.description">
+						<template #label>{{ i18n.ts.description }}</template>
+					</MkTextarea>
+					<MkInput v-model="avatarDecoration.url">
+						<template #label>{{ i18n.ts.imageUrl }}</template>
+					</MkInput>
+					<div class="buttons _buttons">
+						<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+						<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+					</div>
+				</div>
+			</MkFolder>
+		</div>
+	</MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import XHeader from './_header_.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkRadios from '@/components/MkRadios.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkFolder from '@/components/MkFolder.vue';
+
+let avatarDecorations: any[] = $ref([]);
+
+function add() {
+	avatarDecorations.unshift({
+		_id: Math.random().toString(36),
+		id: null,
+		name: '',
+		description: '',
+		url: '',
+	});
+}
+
+function del(avatarDecoration) {
+	os.confirm({
+		type: 'warning',
+		text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
+	}).then(({ canceled }) => {
+		if (canceled) return;
+		avatarDecorations = avatarDecorations.filter(x => x !== avatarDecoration);
+		os.api('admin/avatar-decorations/delete', avatarDecoration);
+	});
+}
+
+async function save(avatarDecoration) {
+	if (avatarDecoration.id == null) {
+		await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
+		load();
+	} else {
+		os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
+	}
+}
+
+function load() {
+	os.api('admin/avatar-decorations/list').then(_avatarDecorations => {
+		avatarDecorations = _avatarDecorations;
+	});
+}
+
+load();
+
+const headerActions = $computed(() => [{
+	asFullButton: true,
+	icon: 'ti ti-plus',
+	text: i18n.ts.add,
+	handler: add,
+}]);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+	title: i18n.ts.avatarDecorations,
+	icon: 'ti ti-sparkles',
+});
+</script>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index a508c20cf317c0a2d82e16756d2ff85852c8695b..b304edbf57658260289f26b47715691f7db1af9f 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -115,6 +115,11 @@ const menuDef = $computed(() => [{
 		text: i18n.ts.customEmojis,
 		to: '/admin/emojis',
 		active: currentPage?.route.name === 'emojis',
+	}, {
+		icon: 'ti ti-sparkles',
+		text: i18n.ts.avatarDecorations,
+		to: '/admin/avatar-decorations',
+		active: currentPage?.route.name === 'avatarDecorations',
 	}, {
 		icon: 'ti ti-whirl',
 		text: i18n.ts.federation,
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 0af226f02e3ffb8b9d8f3a5c29ff0f144410efc6..bceefcf6c80cc894c4882fbba74f79c5291ab1e8 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #label>
 		<b
 			:class="{
-				[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
+				[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type),
 				[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
-				[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
+				[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type)
 			}"
 		>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
 		<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
@@ -37,6 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span v-else-if="log.type === 'deleteUserAnnouncement'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
 		<span v-else-if="log.type === 'deleteNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
 		<span v-else-if="log.type === 'deleteDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
+		<span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
+		<span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span>
+		<span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span>
 	</template>
 	<template #icon>
 		<MkAvatar :user="log.user" :class="$style.avatar"/>
@@ -102,6 +105,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
 			</div>
 		</template>
+		<template v-else-if="log.type === 'updateAvatarDecoration'">
+			<div :class="$style.diff">
+				<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
+			</div>
+		</template>
 
 		<details>
 			<summary>raw</summary>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 5e4889f61c3b91912f521d6b175364524160b7e2..c44a58d04a2a91e52de9311877ca42185e66cca3 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -83,6 +83,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
 	</FormSlot>
 
+	<MkFolder>
+		<template #icon><i class="ti ti-sparkles"></i></template>
+		<template #label>{{ i18n.ts.avatarDecorations }}</template>
+
+		<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-gap: 12px;">
+			<div
+				v-for="avatarDecoration in avatarDecorations"
+				:key="avatarDecoration.id"
+				:class="[$style.avatarDecoration, { [$style.avatarDecorationActive]: $i.avatarDecorations.some(x => x.id === avatarDecoration.id) }]"
+				@click="toggleDecoration(avatarDecoration)"
+			>
+				<div :class="$style.avatarDecorationName">{{ avatarDecoration.name }}</div>
+				<MkAvatar style="width: 64px; height: 64px;" :user="$i" :decoration="avatarDecoration.url"/>
+			</div>
+		</div>
+	</MkFolder>
+
 	<MkFolder>
 		<template #label>{{ i18n.ts.advancedSettings }}</template>
 
@@ -126,6 +143,7 @@ import MkInfo from '@/components/MkInfo.vue';
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
 const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
+let avatarDecorations: any[] = $ref([]);
 
 const profile = reactive({
 	name: $i.name,
@@ -146,6 +164,10 @@ watch(() => profile, () => {
 const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
 const fieldEditMode = ref(false);
 
+os.api('get-avatar-decorations').then(_avatarDecorations => {
+	avatarDecorations = _avatarDecorations;
+});
+
 function addField() {
 	fields.value.push({
 		id: Math.random().toString(),
@@ -244,6 +266,20 @@ function changeBanner(ev) {
 	});
 }
 
+function toggleDecoration(avatarDecoration) {
+	if ($i.avatarDecorations.some(x => x.id === avatarDecoration.id)) {
+		os.apiWithDialog('i/update', {
+			avatarDecorations: [],
+		});
+		$i.avatarDecorations = [];
+	} else {
+		os.apiWithDialog('i/update', {
+			avatarDecorations: [avatarDecoration.id],
+		});
+		$i.avatarDecorations.push(avatarDecoration);
+	}
+}
+
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => []);
@@ -338,4 +374,23 @@ definePageMetadata({
 .dragItemForm {
 	flex-grow: 1;
 }
+
+.avatarDecoration {
+	cursor: pointer;
+	padding: 16px 16px 24px 16px;
+	border: solid 2px var(--divider);
+	border-radius: 8px;
+	text-align: center;
+}
+
+.avatarDecorationActive {
+	border-color: var(--accent);
+}
+
+.avatarDecorationName {
+	position: relative;
+	z-index: 10;
+	font-weight: bold;
+	margin-bottom: 16px;
+}
 </style>
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index 6c33d0d8ee91acbe2a207dc291420e23aa76c2d6..2258edebbbcdc73131c6f86c3defa2ac4f55f1e0 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -343,6 +343,10 @@ export const routes = [{
 		path: '/emojis',
 		name: 'emojis',
 		component: page(() => import('./pages/custom-emojis-manager.vue')),
+	}, {
+		path: '/avatar-decorations',
+		name: 'avatarDecorations',
+		component: page(() => import('./pages/admin/avatar-decorations.vue')),
 	}, {
 		path: '/queue',
 		name: 'queue',
diff --git a/packages/frontend/test/home.test.ts b/packages/frontend/test/home.test.ts
index 80b26c081a48f3b042bbb74e1bb31626d7b708c2..6d38b7e5268308d9ddc5c11a974385243a293cc6 100644
--- a/packages/frontend/test/home.test.ts
+++ b/packages/frontend/test/home.test.ts
@@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest';
 import { render, cleanup, type RenderResult } from '@testing-library/vue';
 import './init';
 import type * as Misskey from 'misskey-js';
-import { directives } from '@/directives';
-import { components } from '@/components/index';
+import { directives } from '@/directives/index.js';
+import { components } from '@/components/index.js';
 import XHome from '@/pages/user/home.vue';
 
 describe('XHome', () => {
@@ -34,6 +34,8 @@ describe('XHome', () => {
 			createdAt: '1970-01-01T00:00:00.000Z',
 			fields: [],
 			pinnedNotes: [],
+			avatarUrl: 'https://example.com',
+			avatarDecorations: [],
 		});
 
 		const anchor = home.container.querySelector<HTMLAnchorElement>('a[href^="https://example.com/"]');
@@ -54,6 +56,8 @@ describe('XHome', () => {
 			createdAt: '1970-01-01T00:00:00.000Z',
 			fields: [],
 			pinnedNotes: [],
+			avatarUrl: 'https://example.com',
+			avatarDecorations: [],
 		});
 
 		const anchor = home.container.querySelector<HTMLAnchorElement>('a[href^="https://example.com/"]');
diff --git a/packages/frontend/test/note.test.ts b/packages/frontend/test/note.test.ts
index 3e4faad28792fa8b1bbb565e34fbe30676215f17..8ccc05ff3e745bb6ce725d5a9a849e3511632c90 100644
--- a/packages/frontend/test/note.test.ts
+++ b/packages/frontend/test/note.test.ts
@@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest';
 import { render, cleanup, type RenderResult } from '@testing-library/vue';
 import './init';
 import type * as Misskey from 'misskey-js';
-import { components } from '@/components';
-import { directives } from '@/directives';
+import { components } from '@/components/index.js';
+import { directives } from '@/directives/index.js';
 import MkMediaImage from '@/components/MkMediaImage.vue';
 
 describe('MkMediaImage', () => {
diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts
index 0cf3a417e2b974e3ef957f6d83ab9b60f5654c89..811f07d9c7ca71bc989c9a50be2e7580b755310d 100644
--- a/packages/frontend/test/url-preview.test.ts
+++ b/packages/frontend/test/url-preview.test.ts
@@ -7,8 +7,8 @@ import { describe, test, assert, afterEach } from 'vitest';
 import { render, cleanup, type RenderResult } from '@testing-library/vue';
 import './init';
 import type { summaly } from 'summaly';
-import { components } from '@/components';
-import { directives } from '@/directives';
+import { components } from '@/components/index.js';
+import { directives } from '@/directives/index.js';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 
 type SummalyResult = Awaited<ReturnType<typeof summaly>>;
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 0136df20303d4cf1a904a046d607cc9a15e93e85..4fabc195de477dac44b8bc951089e7b8ff388dfe 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2634,10 +2634,22 @@ type ModerationLog = {
 } | {
     type: 'deleteAd';
     info: ModerationLogPayloads['deleteAd'];
+} | {
+    type: 'createAvatarDecoration';
+    info: ModerationLogPayloads['createAvatarDecoration'];
+} | {
+    type: 'updateAvatarDecoration';
+    info: ModerationLogPayloads['updateAvatarDecoration'];
+} | {
+    type: 'deleteAvatarDecoration';
+    info: ModerationLogPayloads['deleteAvatarDecoration'];
+} | {
+    type: 'resolveAbuseReport';
+    info: ModerationLogPayloads['resolveAbuseReport'];
 });
 
 // @public (undocumented)
-export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd"];
+export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration"];
 
 // @public (undocumented)
 export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
@@ -2965,6 +2977,10 @@ type UserLite = {
     onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
     avatarUrl: string;
     avatarBlurhash: string;
+    avatarDecorations: {
+        id: ID;
+        url: string;
+    }[];
     emojis: {
         name: string;
         url: string;
@@ -2989,8 +3005,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
 // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
 // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
 // src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
-// src/entities.ts:109:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
-// src/entities.ts:605:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/entities.ts:113:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
+// src/entities.ts:609:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
 // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index c4ddead823aed2570f097de7ff959877a8952d46..48a36a31d68abf66d7bc84191680d2e3eeb57b79 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -78,6 +78,9 @@ export const moderationLogTypes = [
 	'createAd',
 	'updateAd',
 	'deleteAd',
+	'createAvatarDecoration',
+	'updateAvatarDecoration',
+	'deleteAvatarDecoration',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -239,4 +242,17 @@ export type ModerationLogPayloads = {
 		adId: string;
 		ad: any;
 	};
+	createAvatarDecoration: {
+		avatarDecorationId: string;
+		avatarDecoration: any;
+	};
+	updateAvatarDecoration: {
+		avatarDecorationId: string;
+		before: any;
+		after: any;
+	};
+	deleteAvatarDecoration: {
+		avatarDecorationId: string;
+		avatarDecoration: any;
+	};
 };
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 50b4a40c49f2b96de81879be8c541a71bf362b54..a2a283d23405226b5979b9dc9a4970b37c7c3921 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -16,6 +16,10 @@ export type UserLite = {
 	onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
 	avatarUrl: string;
 	avatarBlurhash: string;
+	avatarDecorations: {
+		id: ID;
+		url: string;
+	}[];
 	emojis: {
 		name: string;
 		url: string;
@@ -693,4 +697,16 @@ export type ModerationLog = {
 } | {
 	type: 'deleteAd';
 	info: ModerationLogPayloads['deleteAd'];
+} | {
+	type: 'createAvatarDecoration';
+	info: ModerationLogPayloads['createAvatarDecoration'];
+} | {
+	type: 'updateAvatarDecoration';
+	info: ModerationLogPayloads['updateAvatarDecoration'];
+} | {
+	type: 'deleteAvatarDecoration';
+	info: ModerationLogPayloads['deleteAvatarDecoration'];
+} | {
+	type: 'resolveAbuseReport';
+	info: ModerationLogPayloads['resolveAbuseReport'];
 });