diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 943168061c..1982681aed 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -968,6 +968,10 @@ _role:
     isRemote: "リモートユーザー"
     createdLessThan: "アカウント作成から~以内"
     createdMoreThan: "アカウント作成から~経過"
+    followersLessThanOrEq: "フォロワー数が~以下"
+    followersMoreThanOrEq: "フォロワー数が~以上"
+    followingLessThanOrEq: "フォロー数が~以下"
+    followingMoreThanOrEq: "フォロー数が~以上"
     and: "~かつ~"
     or: "~または~"
     not: "~ではない"
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 1b5abce29a..be755f7dab 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -16,6 +16,7 @@ import { DI } from '@/di-symbols.js';
 import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { bindThis } from '@/decorators.js';
+import { StreamMessages } from '@/server/api/stream/types.js';
 import type { OnApplicationShutdown } from '@nestjs/common';
 
 @Injectable()
@@ -73,7 +74,7 @@ export class AntennaService implements OnApplicationShutdown {
 		const obj = JSON.parse(data);
 
 		if (obj.channel === 'internal') {
-			const { type, body } = obj.message;
+			const { type, body } = obj.message as StreamMessages['internal']['payload'];
 			switch (type) {
 				case 'antennaCreated':
 					this.antennas.push(body);
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index ff05779aee..4b792c083d 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -4,8 +4,9 @@ import Redis from 'ioredis';
 import { DI } from '@/di-symbols.js';
 import { Meta } from '@/models/entities/Meta.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import { StreamMessages } from '@/server/api/stream/types.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
 
 @Injectable()
 export class MetaService implements OnApplicationShutdown {
@@ -40,7 +41,7 @@ export class MetaService implements OnApplicationShutdown {
 		const obj = JSON.parse(data);
 
 		if (obj.channel === 'internal') {
-			const { type, body } = obj.message;
+			const { type, body } = obj.message as StreamMessages['internal']['payload'];
 			switch (type) {
 				case 'metaUpdated': {
 					this.cache = body;
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index d2056709e1..e7821ebd78 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -10,6 +10,7 @@ import { MetaService } from '@/core/MetaService.js';
 import { UserCacheService } from '@/core/UserCacheService.js';
 import { RoleCondFormulaValue } from '@/models/entities/Role.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { StreamMessages } from '@/server/api/stream/types.js';
 import type { OnApplicationShutdown } from '@nestjs/common';
 
 export type RoleOptions = {
@@ -69,7 +70,7 @@ export class RoleService implements OnApplicationShutdown {
 		const obj = JSON.parse(data);
 
 		if (obj.channel === 'internal') {
-			const { type, body } = obj.message;
+			const { type, body } = obj.message as StreamMessages['internal']['payload'];
 			switch (type) {
 				case 'roleCreated': {
 					const cached = this.rolesCache.get(null);
@@ -147,6 +148,18 @@ export class RoleService implements OnApplicationShutdown {
 				case 'createdMoreThan': {
 					return user.createdAt.getTime() < (Date.now() - (value.sec * 1000));
 				}
+				case 'followersLessThanOrEq': {
+					return user.followersCount <= value.value;
+				}
+				case 'followersMoreThanOrEq': {
+					return user.followersCount >= value.value;
+				}
+				case 'followingLessThanOrEq': {
+					return user.followingCount <= value.value;
+				}
+				case 'followingMoreThanOrEq': {
+					return user.followingCount >= value.value;
+				}
 				default:
 					return false;
 			}
diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts
index 4d9ee7366d..29a64f5848 100644
--- a/packages/backend/src/core/UserCacheService.ts
+++ b/packages/backend/src/core/UserCacheService.ts
@@ -6,6 +6,7 @@ import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/mode
 import { DI } from '@/di-symbols.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
+import { StreamMessages } from '@/server/api/stream/types.js';
 import type { OnApplicationShutdown } from '@nestjs/common';
 
 @Injectable()
@@ -39,7 +40,7 @@ export class UserCacheService implements OnApplicationShutdown {
 		const obj = JSON.parse(data);
 
 		if (obj.channel === 'internal') {
-			const { type, body } = obj.message;
+			const { type, body } = obj.message as StreamMessages['internal']['payload'];
 			switch (type) {
 				case 'userChangeSuspendedState':
 				case 'remoteUserUpdated': {
@@ -62,6 +63,13 @@ export class UserCacheService implements OnApplicationShutdown {
 					this.localUserByNativeTokenCache.set(body.newToken, user);
 					break;
 				}
+				case 'follow': {
+					const follower = this.userByIdCache.get(body.followerId);
+					if (follower) follower.followingCount++;
+					const followee = this.userByIdCache.get(body.followeeId);
+					if (followee) followee.followersCount++;
+					break;
+				}
 				default:
 					break;
 			}
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 52834c375e..f1ce311cea 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -62,6 +62,7 @@ export class UserFollowingService {
 		private federatedInstanceService: FederatedInstanceService,
 		private webhookService: WebhookService,
 		private apRendererService: ApRendererService,
+		private globalEventService: GlobalEventService,
 		private perUserFollowingChart: PerUserFollowingChart,
 		private instanceChart: InstanceChart,
 	) {
@@ -195,6 +196,8 @@ export class UserFollowingService {
 		}
 	
 		if (alreadyFollowed) return;
+
+		this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
 	
 		//#region Increment counts
 		await Promise.all([
@@ -314,6 +317,8 @@ export class UserFollowingService {
 		follower: {id: User['id']; host: User['host']; },
 		followee: { id: User['id']; host: User['host']; },
 	): Promise<void> {
+		this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
+	
 		//#region Decrement following / followers counts
 		await Promise.all([
 			this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
index 91a39f1359..36110490a0 100644
--- a/packages/backend/src/core/WebhookService.ts
+++ b/packages/backend/src/core/WebhookService.ts
@@ -3,8 +3,9 @@ import Redis from 'ioredis';
 import type { WebhooksRepository } from '@/models/index.js';
 import type { Webhook } from '@/models/entities/Webhook.js';
 import { DI } from '@/di-symbols.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
 import { bindThis } from '@/decorators.js';
+import { StreamMessages } from '@/server/api/stream/types.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
 
 @Injectable()
 export class WebhookService implements OnApplicationShutdown {
@@ -39,7 +40,7 @@ export class WebhookService implements OnApplicationShutdown {
 		const obj = JSON.parse(data);
 
 		if (obj.channel === 'internal') {
-			const { type, body } = obj.message;
+			const { type, body } = obj.message as StreamMessages['internal']['payload'];
 			switch (type) {
 				case 'webhookCreated':
 					if (body.active) {
diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts
index f7b4edc9e7..a18df40d0c 100644
--- a/packages/backend/src/models/entities/Role.ts
+++ b/packages/backend/src/models/entities/Role.ts
@@ -34,6 +34,26 @@ type CondFormulaValueCreatedMoreThan = {
 	sec: number;
 };
 
+type CondFormulaValueFollowersLessThanOrEq = {
+	type: 'followersLessThanOrEq';
+	value: number;
+};
+
+type CondFormulaValueFollowersMoreThanOrEq = {
+	type: 'followersMoreThanOrEq';
+	value: number;
+};
+
+type CondFormulaValueFollowingLessThanOrEq = {
+	type: 'followingLessThanOrEq';
+	value: number;
+};
+
+type CondFormulaValueFollowingMoreThanOrEq = {
+	type: 'followingMoreThanOrEq';
+	value: number;
+};
+
 export type RoleCondFormulaValue =
 	CondFormulaValueAnd |
 	CondFormulaValueOr |
@@ -41,7 +61,11 @@ export type RoleCondFormulaValue =
 	CondFormulaValueIsLocal |
 	CondFormulaValueIsRemote |
 	CondFormulaValueCreatedLessThan |
-	CondFormulaValueCreatedMoreThan;
+	CondFormulaValueCreatedMoreThan |
+	CondFormulaValueFollowersLessThanOrEq |
+	CondFormulaValueFollowersMoreThanOrEq |
+	CondFormulaValueFollowingLessThanOrEq |
+	CondFormulaValueFollowingMoreThanOrEq;
 
 @Entity()
 export class Role {
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index 3bc844f949..03837baefb 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -14,7 +14,7 @@ import type { Page } from '@/models/entities/Page.js';
 import type { Packed } from '@/misc/schema.js';
 import type { Webhook } from '@/models/entities/Webhook.js';
 import type { Meta } from '@/models/entities/Meta.js';
-import { Role, RoleAssignment } from '@/models';
+import { Following, Role, RoleAssignment } from '@/models';
 import type Emitter from 'strict-event-emitter-types';
 import type { EventEmitter } from 'events';
 
@@ -28,6 +28,8 @@ export interface InternalStreamTypes {
 	userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>;
 	userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>;
 	remoteUserUpdated: Serialized<{ id: User['id']; }>;
+	follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
+	unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
 	defaultRoleOverrideUpdated: Serialized<Role['options']>;
 	roleCreated: Serialized<Role>;
 	roleDeleted: Serialized<Role>;
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 1cce5e58e8..5bd3803486 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -6,6 +6,10 @@
 			<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
 			<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
 			<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
+			<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
+			<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
+			<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
+			<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
 			<option value="and">{{ i18n.ts._role._condition.and }}</option>
 			<option value="or">{{ i18n.ts._role._condition.or }}</option>
 			<option value="not">{{ i18n.ts._role._condition.not }}</option>
@@ -37,6 +41,9 @@
 	<MkInput v-else-if="type === 'createdLessThan' || type === 'createdMoreThan'" v-model="v.sec" type="number">
 		<template #suffix>sec</template>
 	</MkInput>
+
+	<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
+	</MkInput>
 </div>
 </template>
 
@@ -85,6 +92,10 @@ const type = computed({
 		if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
 		if (t === 'createdLessThan') v.value.sec = 86400;
 		if (t === 'createdMoreThan') v.value.sec = 86400;
+		if (t === 'followersLessThanOrEq') v.value.value = 10;
+		if (t === 'followersMoreThanOrEq') v.value.value = 10;
+		if (t === 'followingLessThanOrEq') v.value.value = 10;
+		if (t === 'followingMoreThanOrEq') v.value.value = 10;
 		v.value.type = t;
 	},
 });