diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index b7ef578c52..390bfc9f31 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -96,6 +96,9 @@ common:
     specified: "ダイレクト"
     specified-desc: "指定したユーザーにのみ公開"
     private: "非公開"
+    local-public: "公開(ローカルのみ)"
+    local-home: "ホーム(ローカルのみ)"
+    local-followers: "フォロワー(ローカルのみ)"
 
   note-placeholders:
     a: "今どうしてる?"
@@ -471,6 +474,9 @@ common/views/components/visibility-chooser.vue:
   specified: "ダイレクト"
   specified-desc: "指定したユーザーにのみ公開"
   private: "非公開"
+  local-public: "公開(ローカルのみ)"
+  local-home: "ホーム(ローカルのみ)"
+  local-followers: "フォロワー(ローカルのみ)"
 
 common/views/components/trends.vue:
   count: "{}人が投稿"
@@ -761,6 +767,7 @@ desktop/views/components/post-form.vue:
   create-poll: "アンケートを作成"
   text-remain: "残り{}文字"
   recent-tags: "最近"
+  local-only-message: "この投稿はローカルにのみ公開されます"
   click-to-tagging: "クリックでタグ付け"
   visibility: "公開範囲"
   geolocation-alert: "お使いの端末は位置情報に対応していません"
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
index 2c7ae0194c..012b678ab3 100644
--- a/src/client/app/common/views/components/note-header.vue
+++ b/src/client/app/common/views/components/note-header.vue
@@ -19,6 +19,9 @@
 			<template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template>
 			<template v-if="note.visibility == 'private'"><fa icon="lock"/></template>
 		</span>
+		<span class="localOnly" v-if="note.localOnly == true">
+			<template><fa icon="heart"/></template>
+		</span>
 	</div>
 </header>
 </template>
@@ -115,4 +118,7 @@ export default Vue.extend({
 		> .visibility
 			margin-left 8px
 
+		> .localOnly
+			margin-left 4px
+
 </style>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 896be039b3..0335fba0ee 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -35,6 +35,24 @@
 				<span>{{ $t('private') }}</span>
 			</div>
 		</div>
+		<div @click="choose('local-public')" :class="{ active: v == 'local-public' }">
+			<div><fa icon="globe"/></div>
+			<div>
+				<span>{{ $t('local-public') }}</span>
+			</div>
+		</div>
+		<div @click="choose('local-home')" :class="{ active: v == 'local-home' }">
+			<div><fa icon="home"/></div>
+			<div>
+				<span>{{ $t('local-home') }}</span>
+			</div>
+		</div>
+		<div @click="choose('local-followers')" :class="{ active: v == 'local-followers' }">
+			<div><fa icon="unlock"/></div>
+			<div>
+				<span>{{ $t('local-followers') }}</span>
+			</div>
+		</div>
 	</div>
 </div>
 </template>
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index e2b67c150f..6bd4674269 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -20,6 +20,15 @@
 		<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
 		<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
 		<mk-time :time="note.createdAt"/>
+		<span class="visibility" v-if="note.visibility != 'public'">
+			<template v-if="note.visibility == 'home'"><fa icon="home"/></template>
+			<template v-if="note.visibility == 'followers'"><fa icon="unlock"/></template>
+			<template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template>
+			<template v-if="note.visibility == 'private'"><fa icon="lock"/></template>
+		</span>
+		<span class="localOnly" v-if="note.localOnly == true">
+			<template><fa icon="heart"/></template>
+		</span>
 	</div>
 	<article>
 		<mk-avatar class="avatar" :user="appearNote.user"/>
@@ -199,9 +208,6 @@ export default Vue.extend({
 		> span
 			flex-shrink 0
 
-			&:last-of-type
-				margin-right 8px
-
 		.name
 			overflow hidden
 			flex-shrink 1
@@ -215,6 +221,18 @@ export default Vue.extend({
 			flex-shrink 0
 			font-size 0.9em
 
+		> .visibility
+			margin-left 8px
+
+			[data-icon]
+				margin-right 0
+
+		> .localOnly
+			margin-left 4px
+
+			[data-icon]
+				margin-right 0
+
 		& + article
 			padding-top 8px
 
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index e05fab168c..02478b4eb3 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -14,6 +14,7 @@
 			<b>{{ $t('recent-tags') }}:</b>
 			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
 		</div>
+		<div class="local-only" v-if="this.localOnly == true">{{ $t('local-only-message') }}</div>
 		<input v-show="useCw" v-model="cw" :placeholder="$t('annotations')">
 		<div class="textarea">
 			<textarea :class="{ with: (files.length != 0 || poll) }"
@@ -112,6 +113,7 @@ export default Vue.extend({
 			geo: null,
 			visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
 			visibleUsers: [],
+			localOnly: false,
 			autocomplete: null,
 			draghover: false,
 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
@@ -363,7 +365,14 @@ export default Vue.extend({
 				source: this.$refs.visibilityButton
 			});
 			w.$once('chosen', v => {
-				this.visibility = v;
+				const m = v.match(/^local-(.+)/);
+				if (m) {
+					this.localOnly = true;
+					this.visibility = m[1];
+				} else {
+					this.localOnly = false;
+					this.visibility = v;
+				}
 			});
 		},
 
@@ -407,6 +416,7 @@ export default Vue.extend({
 				cw: this.useCw ? this.cw || '' : undefined,
 				visibility: this.visibility,
 				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
+				localOnly: this.localOnly,
 				geo: this.geo ? {
 					coordinates: [this.geo.longitude, this.geo.latitude],
 					altitude: this.geo.altitude,
@@ -640,6 +650,10 @@ export default Vue.extend({
 				margin-right 8px
 				white-space nowrap
 
+		> .local-only
+			margin 0 0 8px 0
+			color var(--primary)
+
 	> .mk-uploader
 		margin 8px 0 0 0
 		padding 8px
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index d42efbf344..38fab0e5a1 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -16,6 +16,15 @@
 		<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 		<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
 		<mk-time :time="note.createdAt"/>
+		<span class="visibility" v-if="note.visibility != 'public'">
+			<template v-if="note.visibility == 'home'"><fa icon="home"/></template>
+			<template v-if="note.visibility == 'followers'"><fa icon="unlock"/></template>
+			<template v-if="note.visibility == 'specified'"><fa icon="envelope"/></template>
+			<template v-if="note.visibility == 'private'"><fa icon="lock"/></template>
+		</span>
+		<span class="localOnly" v-if="note.localOnly == true">
+			<template><fa icon="heart"/></template>
+		</span>
 	</div>
 	<article>
 		<mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/>
@@ -163,9 +172,6 @@ export default Vue.extend({
 		> span
 			flex-shrink 0
 
-			&:last-of-type
-				margin-right 8px
-
 		.name
 			overflow hidden
 			flex-shrink 1
@@ -179,6 +185,18 @@ export default Vue.extend({
 			flex-shrink 0
 			font-size 0.9em
 
+		> .visibility
+			margin-left 8px
+
+			[data-icon]
+				margin-right 0
+
+		> .localOnly
+			margin-left 4px
+
+			[data-icon]
+				margin-right 0
+
 		& + article
 			padding-top 8px
 
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index df7a5c5a04..f941c59d9f 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -102,6 +102,7 @@ export default Vue.extend({
 			geo: null,
 			visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
 			visibleUsers: [],
+			localOnly: false,
 			useCw: false,
 			cw: null,
 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
@@ -274,7 +275,14 @@ export default Vue.extend({
 				compact: true
 			});
 			w.$once('chosen', v => {
-				this.visibility = v;
+				const m = v.match(/^local-(.+)/);
+				if (m) {
+					this.localOnly = true;
+					this.visibility = m[1];
+				} else {
+					this.localOnly = false;
+					this.visibility = v;
+				}
 			});
 		},
 
@@ -320,6 +328,7 @@ export default Vue.extend({
 				} : null,
 				visibility: this.visibility,
 				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
+				localOnly: this.localOnly,
 				viaMobile: viaMobile
 			}).then(data => {
 				this.$emit('posted');
diff --git a/src/docs/api/entities/note.yaml b/src/docs/api/entities/note.yaml
index 6654be2b02..89846a56c7 100644
--- a/src/docs/api/entities/note.yaml
+++ b/src/docs/api/entities/note.yaml
@@ -26,6 +26,13 @@ props:
       ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)"
       en-US: "Whether this note sent via a mobile device"
 
+  localOnly:
+    type: "boolean"
+    optional: true
+    desc:
+      ja-JP: "ローカルのみに公開する投稿か否か"
+      en-US: "Whether this note is no federation"
+
   text:
     type: "string"
     optional: true
diff --git a/src/models/note.ts b/src/models/note.ts
index 516045225c..717960bb23 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -50,6 +50,7 @@ export type INote = {
 	userId: mongo.ObjectID;
 	appId: mongo.ObjectID;
 	viaMobile: boolean;
+	localOnly: boolean;
 	renoteCount: number;
 	repliesCount: number;
 	reactionCounts: any;
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 5a48dbe648..8683bcd1df 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -6,6 +6,8 @@ export function createHttpJob(data: any) {
 }
 
 export function deliver(user: ILocalUser, content: any, to: any) {
+	if (content == null) return;
+
 	createHttpJob({
 		type: 'deliver',
 		user,
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 7501bf1a89..48a02e79bd 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -116,6 +116,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 		cw: note.summary,
 		text: text,
 		viaMobile: false,
+		localOnly: false,
 		geo: undefined,
 		visibility,
 		visibleUsers,
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 8da933a0f6..888feb08ce 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -66,7 +66,8 @@ router.get('/notes/:note', async (ctx, next) => {
 
 	const note = await Note.findOne({
 		_id: new mongo.ObjectID(ctx.params.note),
-		visibility: { $in: ['public', 'home'] }
+		visibility: { $in: ['public', 'home'] },
+		localOnly: { $ne: true }
 	});
 
 	if (note === null) {
@@ -83,7 +84,8 @@ router.get('/notes/:note', async (ctx, next) => {
 router.get('/notes/:note/activity', async ctx => {
 	const note = await Note.findOne({
 		_id: new mongo.ObjectID(ctx.params.note),
-		visibility: { $in: ['public', 'home'] }
+		visibility: { $in: ['public', 'home'] },
+		localOnly: { $ne: true }
 	});
 
 	if (note === null) {
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 24d4e3730e..6b917ef843 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -55,7 +55,8 @@ export default async (ctx: Router.IRouterContext) => {
 
 		const query = {
 			userId: user._id,
-			visibility: { $in: ['public', 'home'] }
+			visibility: { $in: ['public', 'home'] },
+			localOnly: { $ne: true }
 		} as any;
 
 		if (sinceId) {
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index a7050e2ec2..4f8d6a4f4f 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -74,6 +74,14 @@ export const meta = {
 			}
 		},
 
+		localOnly: {
+			validator: $.bool.optional,
+			default: false,
+			desc: {
+				'ja-JP': 'ローカルのみに投稿か否か。'
+			}
+		},
+
 		geo: {
 			validator: $.obj({
 				coordinates: $.arr().length(2)
@@ -226,6 +234,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
 		cw: ps.cw,
 		app,
 		viaMobile: ps.viaMobile,
+		localOnly: ps.localOnly,
 		visibility: ps.visibility,
 		visibleUsers,
 		geo: ps.geo
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 53d51036b3..0fd983d6c2 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -95,6 +95,7 @@ type Option = {
 	geo?: any;
 	poll?: any;
 	viaMobile?: boolean;
+	localOnly?: boolean;
 	cw?: string;
 	visibility?: string;
 	visibleUsers?: IUser[];
@@ -109,6 +110,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 	if (data.createdAt == null) data.createdAt = new Date();
 	if (data.visibility == null) data.visibility = 'public';
 	if (data.viaMobile == null) data.viaMobile = false;
+	if (data.localOnly == null) data.localOnly = false;
 
 	if (data.visibleUsers) {
 		data.visibleUsers = erase(null, data.visibleUsers);
@@ -139,6 +141,16 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		return rej('Renote target is private of others');
 	}
 
+	// ローカルのみをRenoteしたらローカルのみにする
+	if (data.renote && data.renote.localOnly) {
+		data.localOnly = true;
+	}
+
+	// ローカルのみにリプライしたらローカルのみにする
+	if (data.reply && data.reply.localOnly) {
+		data.localOnly = true;
+	}
+
 	if (data.text) {
 		data.text = data.text.trim();
 	}
@@ -308,6 +320,8 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 });
 
 async function renderActivity(data: Option, note: INote) {
+	if (data.localOnly) return null;
+
 	const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
 		? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
 		: renderCreate(await renderNote(note, false), note);
@@ -389,6 +403,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
 		emojis,
 		userId: user._id,
 		viaMobile: data.viaMobile,
+		localOnly: data.localOnly,
 		geo: data.geo || null,
 		appId: data.app ? data.app._id : null,
 		visibility: data.visibility,