diff --git a/.gitignore b/.gitignore
index 7811cc43..d3ad9558 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
 /vector/olm.*
 .DS_Store
 npm-debug.log
+electron/dist
diff --git a/electron/build/icon.icns b/electron/build/icon.icns
new file mode 100644
index 00000000..55c03d96
Binary files /dev/null and b/electron/build/icon.icns differ
diff --git a/electron/build/icon.ico b/electron/build/icon.ico
new file mode 100644
index 00000000..8b681ffb
Binary files /dev/null and b/electron/build/icon.ico differ
diff --git a/electron/src/electron-main.js b/electron/src/electron-main.js
new file mode 100644
index 00000000..13cbe57a
--- /dev/null
+++ b/electron/src/electron-main.js
@@ -0,0 +1,158 @@
+// @flow
+
+/*
+Copyright 2016 Aviral Dasgupta
+Copyright 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const electron = require('electron');
+const url = require('url');
+
+const VectorMenu = require('./vectormenu');
+
+const PERMITTED_URL_SCHEMES = [
+    'http:',
+    'https:',
+    'mailto:',
+];
+
+const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
+
+let mainWindow = null;
+let appQuitting = false;
+
+function safeOpenURL(target) {
+    // openExternal passes the target to open/start/xdg-open,
+    // so put fairly stringent limits on what can be opened
+    // (for instance, open /bin/sh does indeed open a terminal
+    // with a shell, albeit with no arguments)
+    const parsed_url = url.parse(target);
+    if (PERMITTED_URL_SCHEMES.indexOf(parsed_url.protocol) > -1) {
+        // explicitly use the URL re-assembled by the url library,
+        // so we know the url parser has understood all the parts
+        // of the input string
+        const new_target = url.format(parsed_url);
+        electron.shell.openExternal(new_target);
+    }
+}
+
+function onWindowOrNavigate(ev, target) {
+    // always prevent the default: if something goes wrong,
+    // we don't want to end up opening it in the electron
+    // app, as we could end up opening any sort of random
+    // url in a window that has node scripting access.
+    ev.preventDefault();
+    safeOpenURL(target);
+}
+
+function onLinkContextMenu(ev, params) {
+    const popup_menu = new electron.Menu();
+    popup_menu.append(new electron.MenuItem({
+        label: params.linkURL,
+        click() {
+            safeOpenURL(params.linkURL);
+        },
+    }));
+    popup_menu.popup();
+    ev.preventDefault();
+}
+
+function installUpdate() {
+    // for some reason, quitAndInstall does not fire the
+    // before-quit event, so we need to set the flag here.
+    appQuitting = true;
+    electron.autoUpdater.quitAndInstall();
+}
+
+function pollForUpdates() {
+    try {
+        electron.autoUpdater.checkForUpdates();
+    } catch (e) {
+        console.log("Couldn't check for update", e);
+    }
+}
+
+electron.ipcMain.on('install_update', installUpdate);
+
+electron.app.on('ready', () => {
+    try {
+        // For reasons best known to Squirrel, the way it checks for updates
+        // is completely different between macOS and windows. On macOS, it
+        // hits a URL that either gives it a 200 with some json or
+        // 204 No Content. On windows it takes a base path and looks for
+        // files under that path.
+        if (process.platform == 'darwin') {
+            electron.autoUpdater.setFeedURL("https://riot.im/autoupdate/desktop/");
+        } else if (process.platform == 'win32') {
+            electron.autoUpdater.setFeedURL("https://riot.im/download/desktop/win32/");
+        } else {
+            // Squirrel / electron only supports auto-update on these two platforms.
+            // I'm not even going to try to guess which feed style they'd use if they
+            // implemented it on Linux, or if it would be different again.
+            console.log("Auto update not supported on this platform");
+        }
+        // We check for updates ourselves rather than using 'updater' because we need to
+        // do it in the main process (and we don't really need to check every 10 minutes:
+        // every hour should be just fine for a desktop app)
+        // However, we still let the main window listen for the update events.
+        pollForUpdates();
+        setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
+    } catch (err) {
+        // will fail if running in debug mode
+        console.log("Couldn't enable update checking", err);
+    }
+
+    mainWindow = new electron.BrowserWindow({
+        icon: `${__dirname}/../../vector/img/logo.png`,
+        width: 1024, height: 768,
+    });
+    mainWindow.loadURL(`file://${__dirname}/../../vector/index.html`);
+    electron.Menu.setApplicationMenu(VectorMenu);
+
+    mainWindow.on('closed', () => {
+        mainWindow = null;
+    });
+    mainWindow.on('close', (e) => {
+        if (process.platform == 'darwin' && !appQuitting) {
+            // On Mac, closing the window just hides it
+            // (this is generally how single-window Mac apps
+            // behave, eg. Mail.app)
+            e.preventDefault();
+            mainWindow.hide();
+            return false;
+        }
+    });
+
+    mainWindow.webContents.on('new-window', onWindowOrNavigate);
+    mainWindow.webContents.on('will-navigate', onWindowOrNavigate);
+
+    mainWindow.webContents.on('context-menu', function(ev, params) {
+        if (params.linkURL) {
+            onLinkContextMenu(ev, params);
+        }
+    });
+});
+
+electron.app.on('window-all-closed', () => {
+    electron.app.quit();
+});
+
+electron.app.on('activate', () => {
+    mainWindow.show();
+});
+
+electron.app.on('before-quit', () => {
+    appQuitting = true;
+});
diff --git a/electron/src/vectormenu.js b/electron/src/vectormenu.js
new file mode 100644
index 00000000..fd08af6c
--- /dev/null
+++ b/electron/src/vectormenu.js
@@ -0,0 +1,188 @@
+/*
+Copyright 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const electron = require('electron');
+
+// Menu template from http://electron.atom.io/docs/api/menu/, edited
+const template = [
+    {
+        label: 'Edit',
+        submenu: [
+            {
+                role: 'undo'
+            },
+            {
+                role: 'redo'
+            },
+            {
+                type: 'separator'
+            },
+            {
+                role: 'cut'
+            },
+            {
+                role: 'copy'
+            },
+            {
+                role: 'paste'
+            },
+            {
+                role: 'pasteandmatchstyle'
+            },
+            {
+                role: 'delete'
+            },
+            {
+                role: 'selectall'
+            }
+        ]
+    },
+    {
+        label: 'View',
+        submenu: [
+            {
+                type: 'separator'
+            },
+            {
+                role: 'resetzoom'
+            },
+            {
+                role: 'zoomin'
+            },
+            {
+                role: 'zoomout'
+            },
+            {
+                type: 'separator'
+            },
+            {
+                role: 'togglefullscreen'
+            },
+            {
+                label: 'Toggle Developer Tools',
+                accelerator: process.platform == 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
+                click: function(item, focusedWindow) {
+                    if (focusedWindow) focusedWindow.toggleDevTools();
+                }
+            }
+        ]
+    },
+    {
+        role: 'window',
+        submenu: [
+            {
+                role: 'minimize'
+            },
+            {
+                role: 'close'
+            }
+        ]
+    },
+    {
+        role: 'help',
+        submenu: [
+            {
+                label: 'riot.im',
+                click () { electron.shell.openExternal('https://riot.im/') }
+            }
+        ]
+    }
+];
+
+// macOS has specific menu conventions...
+if (process.platform === 'darwin') {
+    // first macOS menu is the name of the app
+    const name = electron.app.getName()
+    template.unshift({
+        label: name,
+        submenu: [
+            {
+                role: 'about'
+            },
+            {
+                type: 'separator'
+            },
+            {
+                role: 'services',
+                submenu: []
+            },
+            {
+                type: 'separator'
+            },
+            {
+                role: 'hide'
+            },
+            {
+                role: 'hideothers'
+            },
+            {
+                role: 'unhide'
+            },
+            {
+                type: 'separator'
+            },
+            {
+                role: 'quit'
+            }
+        ]
+    })
+    // Edit menu.
+    // This has a 'speech' section on macOS
+    template[1].submenu.push(
+        {
+            type: 'separator'
+        },
+        {
+            label: 'Speech',
+            submenu: [
+                {
+                    role: 'startspeaking'
+                },
+                {
+                    role: 'stopspeaking'
+                }
+            ]
+        }
+    )
+    // Window menu.
+    // This also has specific functionality on macOS
+    template[3].submenu = [
+        {
+            label: 'Close',
+            accelerator: 'CmdOrCtrl+W',
+            role: 'close'
+        },
+        {
+            label: 'Minimize',
+            accelerator: 'CmdOrCtrl+M',
+            role: 'minimize'
+        },
+        {
+            label: 'Zoom',
+            role: 'zoom'
+        },
+        {
+            type: 'separator'
+        },
+        {
+            label: 'Bring All to Front',
+            role: 'front'
+        }
+    ]
+};
+
+module.exports = electron.Menu.buildFromTemplate(template)
+
diff --git a/package.json b/package.json
index 1c8fd007..52efab04 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,10 @@
 {
   "name": "vector-web",
+  "productName": "Riot",
+  "main": "electron/src/electron-main.js",
   "version": "0.8.4-rc.2",
-  "description": "Vector webapp",
-  "author": "matrix.org",
+  "description": "A feature-rich client for Matrix.org",
+  "author": "Vector Creations Ltd.",
   "repository": {
     "type": "git",
     "url": "https://github.com/vector-im/vector-web"
@@ -31,6 +33,7 @@
     "build:compile": "babel --source-maps -d lib src",
     "build:bundle": "NODE_ENV=production webpack -p --progress",
     "build:bundle:dev": "webpack --optimize-occurence-order --progress",
+    "build:electron": "build -lwm",
     "build": "node scripts/babelcheck.js && npm run build:emojione && npm run build:css && npm run build:bundle",
     "build:dev": "npm run build:emojione && npm run build:css && npm run build:bundle:dev",
     "dist": "scripts/package.sh",
@@ -90,6 +93,7 @@
     "catw": "^1.0.1",
     "cpx": "^1.3.2",
     "css-raw-loader": "^0.1.1",
+    "electron-builder": "^7.10.2",
     "emojione": "^2.2.3",
     "expect": "^1.16.0",
     "fs-extra": "^0.30.0",
@@ -116,5 +120,24 @@
   },
   "optionalDependencies": {
     "olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
+  },
+  "build": {
+    "appId": "im.riot.app",
+    "category": "Network",
+    "electronVersion": "1.4.2",
+    "//asar=false": "https://github.com/electron-userland/electron-builder/issues/675",
+    "asar": false,
+    "dereference": true,
+    "//files": "We bundle everything, so we only need to include vector/",
+    "files": [
+      "!**/*",
+      "electron/src/**",
+      "vector/**",
+      "package.json"
+    ]
+  },
+  "directories": {
+    "buildResources": "electron/build",
+    "output": "electron/dist"
   }
 }
diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js
new file mode 100644
index 00000000..810fa984
--- /dev/null
+++ b/src/vector/platform/ElectronPlatform.js
@@ -0,0 +1,99 @@
+// @flow
+
+/*
+Copyright 2016 Aviral Dasgupta
+Copyright 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import VectorBasePlatform from './VectorBasePlatform';
+import dis from 'matrix-react-sdk/lib/dispatcher';
+
+const electron = require('electron');
+const remote = electron.remote;
+
+electron.remote.autoUpdater.on('update-downloaded', onUpdateDownloaded);
+
+function onUpdateDownloaded(ev, releaseNotes, ver, date, updateURL) {
+    dis.dispatch({
+        action: 'new_version',
+        currentVersion: electron.remote.app.getVersion(),
+        newVersion: ver,
+        releaseNotes: releaseNotes,
+    });
+}
+
+export default class ElectronPlatform extends VectorBasePlatform {
+    setNotificationCount(count: number) {
+        super.setNotificationCount(count);
+        // this sometimes throws because electron is made of fail:
+        // https://github.com/electron/electron/issues/7351
+        // For now, let's catch the error, but I suspect it may
+        // continue to fail and we might just have to accept that
+        // electron's remote RPC is a non-starter for now and use IPC
+        try {
+            remote.app.setBadgeCount(count);
+        } catch (e) {
+            console.error("Failed to set notification count", e);
+        }
+    }
+
+    supportsNotifications() : boolean {
+        return true;
+    }
+
+    maySendNotifications() : boolean {
+        return true;
+    }
+
+    displayNotification(title: string, msg: string, avatarUrl: string): Notification {
+        // Notifications in Electron use the HTML5 notification API
+        const notification = new global.Notification(
+            title,
+            {
+                body: msg,
+                icon: avatarUrl,
+                tag: "vector",
+                silent: true, // we play our own sounds
+            }
+        );
+
+        notification.onclick = function() {
+            dis.dispatch({
+                action: 'view_room',
+                room_id: room.roomId
+            });
+            global.focus();
+        };
+
+        return notification;
+    }
+
+    clearNotification(notif: Notification) {
+        notif.close();
+    }
+
+    pollForUpdate() {
+        // In electron we control the update process ourselves, since
+        // it needs to run in the main process, so we just run the timer
+        // loop in the main electron process instead.
+    }
+
+    installUpdate() {
+        // IPC to the main process to install the update, since quitAndInstall
+        // doesn't fire the before-quit event so the main process needs to know
+        // it should exit.
+        electron.ipcRenderer.send('install_update');
+    }
+}
diff --git a/src/vector/platform/index.js b/src/vector/platform/index.js
index 741f1df0..90714200 100644
--- a/src/vector/platform/index.js
+++ b/src/vector/platform/index.js
@@ -17,8 +17,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import WebPlatform from './WebPlatform';
+let Platform = null;
 
-let Platform = WebPlatform;
+if (window && window.process && window.process && window.process.type === 'renderer') {
+    // we're running inside electron
+    Platform = require('./ElectronPlatform');
+} else {
+    Platform = require('./WebPlatform');
+}
 
 export default Platform;
diff --git a/webpack.config.js b/webpack.config.js
index 7d5ba474..5d669bd2 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -66,6 +66,9 @@ module.exports = {
     },
     externals: {
         "olm": "Olm",
+        // Don't try to bundle electron: leave it as a commonjs dependency
+        // (the 'commonjs' here means it will output a 'require')
+        "electron": "commonjs electron",
     },
     plugins: [
         new webpack.DefinePlugin({