diff --git a/locales/ja.yml b/locales/ja.yml
index 310a73a64e..acc4efe89f 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -317,6 +317,8 @@ common/views/components/signin.vue:
   login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
 
 common/views/components/signup.vue:
+  invitation-code: "招待コード"
+  invitation-info: "招待コードをお持ちでない方は、<a href=\"{}\">管理者</a>までご連絡ください。"
   username: "ユーザー名"
   checking: "確認しています..."
   available: "利用できます"
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 45a183e144..1d33702159 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -1,5 +1,10 @@
 <template>
 <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
+	<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
+		<span>%i18n:@invitation-code%</span>
+		<span slot="prefix">%fa:id-card-alt%</span>
+		<p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p>
+	</ui-input>
 	<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
 		<span>%i18n:@username%</span>
 		<span slot="prefix">@</span>
@@ -46,11 +51,13 @@ export default Vue.extend({
 			username: '',
 			password: '',
 			retypedPassword: '',
+			invitationCode: '',
 			url,
 			recaptchaSitekey,
 			usernameState: null,
 			passwordStrength: '',
-			passwordRetypeState: null
+			passwordRetypeState: null,
+			meta: null
 		}
 	},
 	computed: {
@@ -61,6 +68,11 @@ export default Vue.extend({
 				this.usernameState != 'max-range');
 		}
 	},
+	created() {
+		(this as any).os.getMeta().then(meta => {
+			this.meta = meta;
+		});
+	},
 	methods: {
 		onChangeUsername() {
 			if (this.username == '') {
@@ -110,6 +122,7 @@ export default Vue.extend({
 			(this as any).api('signup', {
 				username: this.username,
 				password: this.password,
+				invitationCode: this.invitationCode,
 				'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
 			}).then(() => {
 				(this as any).api('signin', {
diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index b10e829965..d0f11e73b6 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -7,6 +7,10 @@
 		<p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
 		<p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
 	</div>
+	<div>
+		<button class="ui" @click="invite">%i18n:@invite%</button>
+		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+	</div>
 </div>
 </template>
 
@@ -16,13 +20,21 @@ import Vue from "vue";
 export default Vue.extend({
 	data() {
 		return {
-			stats: null
+			stats: null,
+			inviteCode: null
 		};
 	},
 	created() {
 		(this as any).api('stats').then(stats => {
 			this.stats = stats;
 		});
+	},
+	methods: {
+		invite() {
+			(this as any).api('admin/invite').then(x => {
+				this.inviteCode = x.code;
+			});
+		}
 	}
 });
 </script>
diff --git a/src/models/meta.ts b/src/models/meta.ts
index 11b9b186ce..aef0163dfe 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -11,4 +11,5 @@ export type IMeta = {
 		usersCount: number;
 		originalUsersCount: number;
 	};
+	disableRegistration: boolean;
 };
diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts
new file mode 100644
index 0000000000..846acefedf
--- /dev/null
+++ b/src/models/registration-tickets.ts
@@ -0,0 +1,12 @@
+import * as mongo from 'mongodb';
+import db from '../db/mongodb';
+
+const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets');
+RegistrationTicket.createIndex('code', { unique: true });
+export default RegistrationTicket;
+
+export interface IRegistrationTicket {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	code: string;
+}
diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts
new file mode 100644
index 0000000000..77608e715c
--- /dev/null
+++ b/src/server/api/endpoints/admin/invite.ts
@@ -0,0 +1,26 @@
+import rndstr from 'rndstr';
+import RegistrationTicket from '../../../../models/registration-tickets';
+
+export const meta = {
+	desc: {
+		ja: '招待コードを発行します。'
+	},
+
+	requireCredential: true,
+	requireAdmin: true,
+
+	params: {}
+};
+
+export default (params: any) => new Promise(async (res, rej) => {
+	const code = rndstr({ length: 5, chars: '0-9' });
+
+	await RegistrationTicket.insert({
+		createdAt: new Date(),
+		code: code
+	});
+
+	res({
+		code: code
+	});
+});
diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts
index 8698120cdb..9c32ba987d 100644
--- a/src/server/api/endpoints/admin/suspend-user.ts
+++ b/src/server/api/endpoints/admin/suspend-user.ts
@@ -4,43 +4,43 @@ import getParams from '../../get-params';
 import User from '../../../../models/user';
 
 export const meta = {
-  desc: {
-    ja: '指定したユーザーを凍結します。',
-    en: 'Suspend a user.'
-  },
+	desc: {
+		ja: '指定したユーザーを凍結します。',
+		en: 'Suspend a user.'
+	},
 
-  requireCredential: true,
-  requireAdmin: true,
+	requireCredential: true,
+	requireAdmin: true,
 
-  params: {
-    userId: $.type(ID).note({
-      desc: {
-        ja: '対象のユーザーID',
-        en: 'The user ID which you want to suspend'
-      }
-    }),
-  }
+	params: {
+		userId: $.type(ID).note({
+			desc: {
+				ja: '対象のユーザーID',
+				en: 'The user ID which you want to suspend'
+			}
+		}),
+	}
 };
 
 export default (params: any) => new Promise(async (res, rej) => {
-  const [ps, psErr] = getParams(meta, params);
-  if (psErr) return rej(psErr);
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-  const user = await User.findOne({
-    _id: ps.userId
-  });
+	const user = await User.findOne({
+		_id: ps.userId
+	});
 
-  if (user == null) {
-    return rej('user not found');
-  }
+	if (user == null) {
+		return rej('user not found');
+	}
 
-  await User.findOneAndUpdate({
-    _id: user._id
-  }, {
-      $set: {
-        isSuspended: true
-      }
-    });
+	await User.findOneAndUpdate({
+		_id: user._id
+	}, {
+			$set: {
+				isSuspended: true
+			}
+		});
 
-  res();
+	res();
 });
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index c2d93997a7..000a56024d 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -28,6 +28,7 @@ export default () => new Promise(async (res, rej) => {
 			model: os.cpus()[0].model,
 			cores: os.cpus().length
 		},
-		broadcasts: meta.broadcasts
+		broadcasts: meta.broadcasts,
+		disableRegistration: meta.disableRegistration
 	});
 });
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 16ec33bcbf..2bf56a9791 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -6,6 +6,7 @@ import User, { IUser, validateUsername, validatePassword, pack } from '../../../
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../../config';
 import Meta from '../../../models/meta';
+import RegistrationTicket from '../../../models/registration-tickets';
 
 if (config.recaptcha) {
 	recaptcha.init({
@@ -29,6 +30,29 @@ export default async (ctx: Koa.Context) => {
 
 	const username = body['username'];
 	const password = body['password'];
+	const invitationCode = body['invitationCode'];
+
+	const meta = await Meta.findOne({});
+
+	if (meta.disableRegistration) {
+		if (invitationCode == null || typeof invitationCode != 'string') {
+			ctx.status = 400;
+			return;
+		}
+
+		const ticket = await RegistrationTicket.findOne({
+			code: invitationCode
+		});
+
+		if (ticket == null) {
+			ctx.status = 400;
+			return;
+		}
+
+		RegistrationTicket.remove({
+			_id: ticket._id
+		});
+	}
 
 	// Validate username
 	if (!validateUsername(username)) {