diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
new file mode 100644
index 0000000000..2dc22d11f9
--- /dev/null
+++ b/src/api/endpoints/othello/match.ts
@@ -0,0 +1,80 @@
+import $ from 'cafy';
+import Matching from '../../models/othello-matchig';
+import Game, { pack } from '../../models/othello-game';
+import User from '../../models/user';
+import { publishOthelloStream } from '../../event';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'user_id' parameter
+	const [childId, childIdErr] = $(params.user_id).id().$;
+	if (childIdErr) return rej('invalid user_id param');
+
+	// Myself
+	if (childId.equals(user._id)) {
+		return rej('invalid user_id param');
+	}
+
+	// Find session
+	const exist = await Matching.findOne({
+		parent_id: childId,
+		child_id: user._id
+	});
+
+	if (exist) {
+		// Destroy session
+		Matching.remove({
+			_id: exist._id
+		});
+
+		const parentIsBlack = Math.random() > 0.5;
+
+		// Start game
+		const game = await Game.insert({
+			created_at: new Date(),
+			black_user_id: parentIsBlack ? exist.parent_id : user._id,
+			white_user_id: parentIsBlack ? user._id : exist.parent_id,
+			logs: []
+		});
+
+		const packedGame = await pack(game);
+
+		// Reponse
+		res(packedGame);
+
+		publishOthelloStream(exist.parent_id, 'matched', {
+			game
+		});
+	} else {
+		// Fetch child
+		const child = await User.findOne({
+			_id: childId
+		}, {
+			fields: {
+				_id: true
+			}
+		});
+
+		if (child === null) {
+			return rej('user not found');
+		}
+
+		// 以前のセッションはすべて削除しておく
+		await Matching.remove({
+			parent_id: user._id
+		});
+
+		// セッションを作成
+		await Matching.insert({
+			parent_id: user._id,
+			child_id: child._id
+		});
+
+		// Reponse
+		res(204);
+
+		// 招待
+		publishOthelloStream(child._id, 'invited', {
+			user_id: user._id
+		});
+	}
+});
diff --git a/src/api/endpoints/othello/sessions/create.ts b/src/api/endpoints/othello/sessions/create.ts
deleted file mode 100644
index 09c3cff62b..0000000000
--- a/src/api/endpoints/othello/sessions/create.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import rndstr from 'rndstr';
-import Session, { pack } from '../../../models/othello-session';
-
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	// 以前のセッションはすべて削除しておく
-	await Session.remove({
-		user_id: user._id
-	});
-
-	// セッションを作成
-	const session = await Session.insert({
-		user_id: user._id,
-		code: rndstr('a-z0-9', 3)
-	});
-
-	// Reponse
-	res(await pack(session));
-});
diff --git a/src/api/endpoints/othello/sessions/in.ts b/src/api/endpoints/othello/sessions/in.ts
deleted file mode 100644
index d4b95bc4f9..0000000000
--- a/src/api/endpoints/othello/sessions/in.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import Session from '../../../models/othello-session';
-import Game, { pack } from '../../../models/othello-game';
-
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'code' parameter
-	const [code, codeErr] = $(params.code).string().$;
-	if (codeErr) return rej('invalid code param');
-
-	// Fetch session
-	const session = await Session.findOne({ code });
-
-	if (session == null) {
-		return rej('session not found');
-	}
-
-	// Destroy session
-	Session.remove({
-		_id: session._id
-	});
-
-	const parentIsBlack = Math.random() > 0.5;
-
-	// Start game
-	const game = await Game.insert({
-		created_at: new Date(),
-		black_user_id: parentIsBlack ? session.user_id : user._id,
-		white_user_id: parentIsBlack ? user._id : session.user_id,
-		logs: []
-	});
-
-	// Reponse
-	res(await pack(game));
-});
diff --git a/src/api/event.ts b/src/api/event.ts
index 4a2e4e453d..e68082f0a9 100644
--- a/src/api/event.ts
+++ b/src/api/event.ts
@@ -38,6 +38,10 @@ class MisskeyEvent {
 		this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
+	public publishOthelloStream(userId: ID, type: string, value?: any): void {
+		this.publish(`othello-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
 	public publishChannelStream(channelId: ID, type: string, value?: any): void {
 		this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
 	}
@@ -65,4 +69,6 @@ export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
 
 export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
 
+export const publishOthelloStream = ev.publishOthelloStream.bind(ev);
+
 export const publishChannelStream = ev.publishChannelStream.bind(ev);
diff --git a/src/api/models/othello-matching.ts b/src/api/models/othello-matching.ts
new file mode 100644
index 0000000000..bd7aeef3cf
--- /dev/null
+++ b/src/api/models/othello-matching.ts
@@ -0,0 +1,11 @@
+import * as mongo from 'mongodb';
+import db from '../../db/mongodb';
+
+const Matching = db.get<IMatching>('othello_matchings');
+export default Matching;
+
+export interface IMatching {
+	_id: mongo.ObjectID;
+	parent_id: mongo.ObjectID;
+	child_id: mongo.ObjectID;
+}
diff --git a/src/api/models/othello-session.ts b/src/api/models/othello-session.ts
deleted file mode 100644
index 0aa1d01e54..0000000000
--- a/src/api/models/othello-session.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
-
-const Session = db.get<ISession>('othello_sessions');
-export default Session;
-
-export interface ISession {
-	_id: mongo.ObjectID;
-	code: string;
-	user_id: mongo.ObjectID;
-}
-
-/**
- * Pack an othello session for API response
- *
- * @param {any} session
- * @return {Promise<any>}
- */
-export const pack = (
-	session: any
-) => new Promise<any>(async (resolve, reject) => {
-
-	const _session = deepcopy(session);
-
-	delete _session._id;
-
-	resolve(_session);
-});
diff --git a/src/api/stream/messaging.ts b/src/api/stream/messaging.ts
index 3f505cfafa..a4a12426a3 100644
--- a/src/api/stream/messaging.ts
+++ b/src/api/stream/messaging.ts
@@ -2,7 +2,7 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import read from '../common/read-messaging-message';
 
-export default function messagingStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	const otherparty = request.resourceURL.query.otherparty;
 
 	// Subscribe messaging stream
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
new file mode 100644
index 0000000000..ab91ef6422
--- /dev/null
+++ b/src/api/stream/othello-game.ts
@@ -0,0 +1,12 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
+	const game = request.resourceURL.query.game;
+
+	// Subscribe game stream
+	subscriber.subscribe(`misskey:othello-game-stream:${game}`);
+	subscriber.on('message', (_, data) => {
+		connection.send(data);
+	});
+}
diff --git a/src/api/stream/othello-matching.ts b/src/api/stream/othello-matching.ts
new file mode 100644
index 0000000000..f30ce6eb0a
--- /dev/null
+++ b/src/api/stream/othello-matching.ts
@@ -0,0 +1,12 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+	const otherparty = request.resourceURL.query.otherparty;
+
+	// Subscribe matching stream
+	subscriber.subscribe(`misskey:othello-matching:${user._id}-${otherparty}`);
+	subscriber.on('message', (_, data) => {
+		connection.send(data);
+	});
+}
diff --git a/src/api/stream/requests.ts b/src/api/stream/requests.ts
index 2c36e58b6e..d7bb5e6c5c 100644
--- a/src/api/stream/requests.ts
+++ b/src/api/stream/requests.ts
@@ -3,7 +3,7 @@ import Xev from 'xev';
 
 const ev = new Xev();
 
-export default function homeStream(request: websocket.request, connection: websocket.connection): void {
+export default function(request: websocket.request, connection: websocket.connection): void {
 	const onRequest = request => {
 		connection.send(JSON.stringify({
 			type: 'request',
diff --git a/src/api/stream/server.ts b/src/api/stream/server.ts
index 0db6643d40..4ca2ad1b10 100644
--- a/src/api/stream/server.ts
+++ b/src/api/stream/server.ts
@@ -3,7 +3,7 @@ import Xev from 'xev';
 
 const ev = new Xev();
 
-export default function homeStream(request: websocket.request, connection: websocket.connection): void {
+export default function(request: websocket.request, connection: websocket.connection): void {
 	const onStats = stats => {
 		connection.send(JSON.stringify({
 			type: 'stats',
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index c06d64c245..66c2e0cec0 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -10,6 +10,8 @@ import homeStream from './stream/home';
 import driveStream from './stream/drive';
 import messagingStream from './stream/messaging';
 import messagingIndexStream from './stream/messaging-index';
+import othelloGameStream from './stream/othello-game';
+import othelloMatchingStream from './stream/othello-matching';
 import serverStream from './stream/server';
 import requestsStream from './stream/requests';
 import channelStream from './stream/channel';
@@ -62,6 +64,8 @@ module.exports = (server: http.Server) => {
 			request.resourceURL.pathname === '/drive' ? driveStream :
 			request.resourceURL.pathname === '/messaging' ? messagingStream :
 			request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
+			request.resourceURL.pathname === '/othello-game' ? othelloGameStream :
+			request.resourceURL.pathname === '/othello-matching' ? othelloMatchingStream :
 			null;
 
 		if (channel !== null) {
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index a94a996685..2ec488c247 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -89,7 +89,7 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.connection.off('message', this.onMessage);
 		this.connection.off('read', this.onRead);
-		(this as any).os.stream.dispose(this.connectionId);
+		(this as any).streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
 		isMe(message) {
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
new file mode 100644
index 0000000000..3d3ffb2c07
--- /dev/null
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -0,0 +1,29 @@
+<template>
+<div>
+	<header>黒:{{ game.black_user.name }} 白:{{ game.white_user.name }}</header>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['game'],
+	data() {
+		return {
+			game: null,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = (this as any).os.streams.othelloGameStream.getConnection();
+		this.connectionId = (this as any).os.streams.othelloGameStream.use();
+
+		this.connection.on('set', this.onSet);
+	},
+	beforeDestroy() {
+		this.connection.off('set', this.onSet);
+		(this as any).streams.othelloGameStream.dispose(this.connectionId);
+	},
+});
+</script>
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 136046db24..f5abcfb103 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -1,16 +1,19 @@
 <template>
 <div>
-	<div v-if="session">
-		<h1>相手を待っています<mk-ellipsis/></h1>
-		<p>セッションID:<code>{{ session.code }}</code></p>
-		<p>対戦したい相手に上記のセッションIDを伝えてください。相手が「セッションイン」でセッションIDを入力すると、対局が開始されます。</p>
+	<div v-if="game">
+		<x-game :game="game"/>
+	</div>
+	<div v-if="matching">
+		<h1><b>{{ matching.name }}</b>を待っています<mk-ellipsis/></h1>
 	</div>
 	<div v-else>
 		<h1>Misskey Othello</h1>
 		<p>他のMisskeyユーザーとオセロで対戦しよう。</p>
 		<button>フリーマッチ(準備中)</button>
-		<button @click="inSession">セッションイン</button>
-		<button @click="createSession">セッションを作成する</button>
+		<button @click="match">指名</button>
+		<section>
+			<h2>対局の招待があります:</h2>
+		</section>
 		<section>
 			<h2>過去の対局</h2>
 		</section>
@@ -20,11 +23,70 @@
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend({
-	methods: {
-		createSession() {
-			(this as any).api('othello/sessions/create');
+import XGame from './othello.game.vue';
 
+export default Vue.extend({
+	components: {
+		XGame
+	},
+	data() {
+		return {
+			game: null,
+			games: [],
+			gamesFetching: true,
+			gamesMoreFetching: false,
+			matching: false,
+			invitations: [],
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = (this as any).os.streams.othelloStream.getConnection();
+		this.connectionId = (this as any).os.streams.othelloStream.use();
+
+		this.connection.on('macthed', this.onMatched);
+		this.connection.on('invited', this.onInvited);
+
+		(this as any).api('othello/games').then(games => {
+			this.games = games;
+			this.gamesFetching = false;
+		});
+
+		(this as any).api('othello/invitations').then(invitations => {
+			this.invitations = this.invitations.concat(invitations);
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('macthed', this.onMatched);
+		this.connection.off('invited', this.onInvited);
+		(this as any).streams.othelloStream.dispose(this.connectionId);
+	},
+	methods: {
+		match() {
+			(this as any).apis.input({
+				title: 'ユーザー名を入力してください'
+			}).then(username => {
+				(this as any).api('users/show', {
+					username
+				}).then(user => {
+					(this as any).api('othello/match', {
+						user_id: user.id
+					}).then(res => {
+						if (res == null) {
+							this.matching = user;
+						} else {
+							this.game = res;
+						}
+					});
+				});
+			});
+		},
+		onMatched(game) {
+			this.game = game;
+		},
+		onInvited(invite) {
+			this.invitations.unshift(invite);
 		}
 	}
 });