diff --git a/package-lock.json b/package-lock.json
index c363c69d719e1c07f80d3767b10b453455d398b7..ec0e364b43ccb7a2920648b5cdfa18c5f81cbe5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
     },
     "@types/events": {
       "version": "1.2.0",
-      "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
+      "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
       "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
       "dev": true
     },
@@ -34,9 +34,17 @@
       "integrity": "sha512-53ElVDSnZeFUUFIYzI8WLQ25IhWzb6vbddNp8UHlXQyU0ET2RhV5zg0NfubzU7iNMh5bBXb0htCzfvrSVNgzaQ==",
       "dev": true
     },
+<<<<<<< HEAD
+=======
+    "@types/p-queue": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-3.0.0.tgz",
+      "integrity": "sha512-brMFTEZiltJnAxNy07LzRVw2bdQX1ElCElHEmo5WEI70oWQe00aXew9RKmpf8MOzBMiMfNeuQoEyQCWKawPzmQ=="
+    },
+>>>>>>> hs/userstore-db
     "@types/sqlite3": {
       "version": "3.1.3",
-      "resolved": "http://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.3.tgz",
+      "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.3.tgz",
       "integrity": "sha512-BgGToABnI/8/HnZtZz2Qac6DieU2Dm/j3rtbMmUlDVo4T/uLu8cuVfU/n2UkHowiiwXb6/7h/CmSqBIVKgcTMA==",
       "dev": true,
       "requires": {
@@ -66,7 +74,7 @@
     },
     "acorn-jsx": {
       "version": "3.0.1",
-      "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
       "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
       "dev": true,
       "requires": {
@@ -75,7 +83,7 @@
       "dependencies": {
         "acorn": {
           "version": "3.3.0",
-          "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
           "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
           "dev": true
         }
@@ -120,7 +128,7 @@
     },
     "ansi-escapes": {
       "version": "1.4.0",
-      "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
       "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
       "dev": true
     },
@@ -401,7 +409,7 @@
     },
     "chai": {
       "version": "3.5.0",
-      "resolved": "http://registry.npmjs.org/chai/-/chai-3.5.0.tgz",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz",
       "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=",
       "dev": true,
       "requires": {
@@ -595,7 +603,7 @@
     },
     "d": {
       "version": "1.0.0",
-      "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
       "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
       "dev": true,
       "requires": {
@@ -620,7 +628,7 @@
     },
     "deep-eql": {
       "version": "0.1.3",
-      "resolved": "http://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
       "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=",
       "dev": true,
       "requires": {
@@ -737,7 +745,7 @@
     },
     "enabled": {
       "version": "1.0.2",
-      "resolved": "http://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
+      "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
       "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=",
       "requires": {
         "env-variable": "0.0.x"
@@ -1064,7 +1072,7 @@
     },
     "fast-deep-equal": {
       "version": "1.1.0",
-      "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
       "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
     },
     "fast-json-stable-stringify": {
@@ -1085,7 +1093,7 @@
     },
     "fecha": {
       "version": "2.3.3",
-      "resolved": "http://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
+      "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
       "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
     },
     "figures": {
@@ -1413,7 +1421,7 @@
     },
     "inquirer": {
       "version": "0.12.0",
-      "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
       "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=",
       "dev": true,
       "requires": {
@@ -1603,7 +1611,7 @@
         },
         "async": {
           "version": "1.5.2",
-          "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
           "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
           "dev": true
         },
@@ -1821,7 +1829,7 @@
       "dependencies": {
         "bluebird": {
           "version": "2.11.0",
-          "resolved": "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
+          "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
           "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE="
         }
       }
@@ -1841,7 +1849,7 @@
     },
     "media-typer": {
       "version": "0.3.0",
-      "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
       "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
     },
     "merge-descriptors": {
@@ -1904,7 +1912,7 @@
     },
     "mkdirp": {
       "version": "0.5.1",
-      "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
       "requires": {
         "minimist": "0.0.8"
@@ -2098,7 +2106,7 @@
     },
     "onetime": {
       "version": "1.1.0",
-      "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
       "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
       "dev": true
     },
@@ -2140,6 +2148,11 @@
       "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
       "dev": true
     },
+    "p-queue": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-3.0.0.tgz",
+      "integrity": "sha512-2tv/MRmPXfmfnjLLJAHl+DdU8p2DhZafAnlpm/C/T5BpF5L9wKz5tMin4A4N2zVpJL2YMhPlRmtO7s5EtNrjfA=="
+    },
     "packet-reader": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.3.1.tgz",
@@ -2244,7 +2257,7 @@
     },
     "pify": {
       "version": "2.3.0",
-      "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
       "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
       "dev": true
     },
@@ -2310,7 +2323,7 @@
     },
     "progress": {
       "version": "1.1.8",
-      "resolved": "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
       "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
       "dev": true
     },
@@ -2599,7 +2612,7 @@
     },
     "slice-ansi": {
       "version": "0.0.4",
-      "resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
       "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
       "dev": true
     },
@@ -2714,7 +2727,7 @@
     },
     "strip-ansi": {
       "version": "3.0.1",
-      "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
       "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
       "requires": {
         "ansi-regex": "^2.0.0"
@@ -2742,7 +2755,7 @@
     },
     "table": {
       "version": "3.8.3",
-      "resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz",
+      "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
       "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=",
       "dev": true,
       "requires": {
@@ -2890,7 +2903,7 @@
     },
     "through": {
       "version": "2.3.8",
-      "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
       "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
     },
     "tough-cookie": {
diff --git a/package.json b/package.json
index 66093ca7af87515273ffc1efbe08f2d826c81736..be68f10ef09ac6414fa4491b7277cb6f14f7bf84 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
   },
   "homepage": "https://github.com/Half-Shot/matrix-appservice-discord#readme",
   "dependencies": {
+    "@types/p-queue": "^3.0.0",
     "better-sqlite3": "^5.0.1",
     "bluebird": "^3.5.1",
     "command-line-args": "^4.0.1",
@@ -47,6 +48,7 @@
     "mime": "^1.6.0",
     "moment": "^2.22.2",
     "node-html-parser": "^1.1.11",
+    "p-queue": "^3.0.0",
     "pg-promise": "^8.5.1",
     "tslint": "^5.11.0",
     "typescript": "^3.1.3",
diff --git a/src/bot.ts b/src/bot.ts
index 1a67e3a5714abc9f8ca18c5643d480ca0b8a5603..25a2b2199929daf165cf3c35defe5aad92dc9d3b 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -139,7 +139,7 @@ export class DiscordBot {
     public async init(): Promise<void> {
         await this.clientFactory.init();
         // This immediately pokes UserStore, so it must be created after the bridge has started.
-        this.userSync = new UserSyncroniser(this.bridge, this.config, this);
+        this.userSync = new UserSyncroniser(this.bridge, this.config, this, this.store.userStore);
     }
 
     public async run(): Promise<void> {
diff --git a/src/db/postgres.ts b/src/db/postgres.ts
index aa9116d9dc94fe0585c701b79901e334796a10bd..b863d6293eed999cdae258cdab2320f45b3b187f 100644
--- a/src/db/postgres.ts
+++ b/src/db/postgres.ts
@@ -51,7 +51,14 @@ export class Postgres implements IDatabaseConnector {
 
     public async All(sql: string, parameters?: ISqlCommandParameters): Promise<ISqlRow[]> {
         log.silly("All:", sql);
-        return this.db.many(Postgres.ParameterizeSql(sql), parameters);
+        try {
+            return await this.db.many(Postgres.ParameterizeSql(sql), parameters);
+        } catch (ex) {
+            if (ex.code === pgPromise.errors.queryResultErrorCode.noData ) {
+                return [];
+            }
+            throw ex;
+        }
     }
 
     public async Run(sql: string, parameters?: ISqlCommandParameters): Promise<void> {
diff --git a/src/db/schema/v9.ts b/src/db/schema/v9.ts
new file mode 100644
index 0000000000000000000000000000000000000000..59aa65c4e2ead21845cdfd4af07b01f8ec51affb
--- /dev/null
+++ b/src/db/schema/v9.ts
@@ -0,0 +1,120 @@
+/*
+Copyright 2019 matrix-appservice-discord
+
+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 { IDbSchema } from "./dbschema";
+import { DiscordStore } from "../../store";
+import { Log } from "../../log";
+import {
+    UserStore,
+} from "matrix-appservice-bridge";
+import { RemoteUser } from "../userstore";
+import * as Queue from "p-queue";
+const log = new Log("SchemaV9");
+
+export class Schema implements IDbSchema {
+    public description = "create user store tables";
+
+    constructor(private userStore: UserStore|null) {
+
+    }
+
+    public async run(store: DiscordStore): Promise<void> {
+        await store.create_table(`
+            CREATE TABLE remote_user_guild_nicks (
+                remote_id TEXT NOT NULL,
+                guild_id TEXT NOT NULL,
+                nick TEXT NOT NULL,
+                PRIMARY KEY(remote_id, guild_id)
+        );`, "remote_user_guild_nicks");
+
+        await store.create_table(`
+            CREATE TABLE remote_user_data (
+                remote_id TEXT NOT NULL,
+                displayname TEXT,
+                avatarurl TEXT,
+                avatarurl_mxc TEXT,
+                PRIMARY KEY(remote_id)
+        );`, "remote_user_data");
+
+        await store.create_table(`
+            CREATE TABLE user_entries (
+                matrix_id TEXT,
+                remote_id TEXT,
+                PRIMARY KEY(matrix_id, remote_id)
+        );`, "user_entries");
+
+        if (this.userStore === null) {
+            log.warn("Not migrating users from users store, users store is null");
+            return;
+        }
+        log.warn("Migrating users from userstore, this may take a while...");
+        const remoteUsers = await this.userStore.select({type: "remote"});
+        log.info(`Found ${remoteUsers.length} remote users in the DB`);
+        let migrated = 0;
+        const processQueue = new Queue({
+        autoStart: true,
+            concurrency: 100,
+        });
+        for (const user of remoteUsers) {
+            const matrixIds = await this.userStore.getMatrixLinks(user.id);
+            if (!matrixIds || matrixIds.length === 0) {
+                log.warn(`Not migrating ${user.id}, has no linked matrix user`);
+                continue;
+            } else if (matrixIds.length > 1) {
+                log.warn(`Multiple matrix ids for ${user.id}, using first`);
+            }
+            const matrixId = matrixIds[0];
+            try {
+                const remote = new RemoteUser(user.id);
+                remote.avatarurl = user.data.avatarurl;
+                remote.avatarurlMxc = user.data.avatarurl_mxc;
+                remote.displayname = user.data.displayname;
+                Object.keys(user.data).filter((k) => k.startsWith("nick_")).forEach((k) => {
+                    remote.guildNicks.set(k.substr("nick_".length), user.data[k]);
+                });
+                processQueue.add(async () => {
+                    await store.userStore.linkUsers(matrixId, remote.id);
+                    return store.userStore.setRemoteUser(remote);
+                }).then(() => {
+                    log.info(`Migrated ${matrixId}, ${processQueue.pending} to go.`);
+                    migrated++;
+                }).catch((err) => {
+                    log.error(`Failed to migrate ${matrixId} ${err}`);
+                });
+            } catch (ex) {
+                log.error(`Failed to link ${matrixId}: `, ex);
+            }
+        }
+        await processQueue.onIdle();
+        if (migrated !== remoteUsers.length) {
+            log.error(`Didn't migrate all users, ${remoteUsers.length - migrated} failed to be migrated.`);
+        } else {
+            log.info("Migrated all users successfully");
+        }
+    }
+
+    public async rollBack(store: DiscordStore): Promise<void> {
+        await store.db.Run(
+            `DROP TABLE IF EXISTS remote_user_guild_nicks;`,
+        );
+        await store.db.Run(
+            `DROP TABLE IF EXISTS remote_user_data;`,
+        );
+        await store.db.Run(
+            `DROP TABLE IF EXISTS user_entries;`,
+        );
+    }
+}
diff --git a/src/db/userstore.ts b/src/db/userstore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..119931fcc616fbe9145b160011b338ccf6228997
--- /dev/null
+++ b/src/db/userstore.ts
@@ -0,0 +1,153 @@
+import { IDatabaseConnector } from "./connector";
+import * as uuid from "uuid/v4";
+import { Log } from "../log";
+
+/**
+ * A UserStore compatible with
+ * https://github.com/matrix-org/matrix-appservice-bridge/blob/master/lib/components/user-bridge-store.js
+ * that accesses the database instead.
+ */
+
+const ENTRY_CACHE_LIMETIME = 30000;
+
+export class RemoteUser {
+    public displayname: string|null = null;
+    public avatarurl: string|null = null;
+    public avatarurlMxc: string|null = null;
+    public guildNicks: Map<string, string> = new Map();
+    constructor(public readonly id: string) {
+
+    }
+}
+
+const log = new Log("DbUserStore");
+
+export interface IUserStoreEntry {
+    id: string;
+    matrix: string|null;
+    remote: RemoteUser|null;
+}
+
+export class DbUserStore {
+    private remoteUserCache: Map<string, {e: RemoteUser, ts: number}>;
+
+    constructor(private db: IDatabaseConnector) {
+        this.remoteUserCache = new Map();
+    }
+
+    public async getRemoteUser(remoteId: string): Promise<RemoteUser|null> {
+        const cached = this.remoteUserCache.get(remoteId);
+        if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) {
+            return cached.e;
+        }
+        const row = await this.db.Get(
+            "SELECT * FROM user_entries WHERE remote_id = $id", {id: remoteId},
+        );
+        if (!row) {
+            return null;
+        }
+        const remoteUser = new RemoteUser(remoteId);
+        const data = await this.db.Get(
+            "SELECT * FROM remote_user_data WHERE remote_id = $remoteId",
+            {remoteId},
+        );
+        if (data) {
+            remoteUser.avatarurl = data.avatarurl as string|null;
+            remoteUser.displayname = data.displayname as string|null;
+            remoteUser.avatarurlMxc = data.avatarurl_mxc as string|null;
+        }
+        const nicks = await this.db.All(
+            "SELECT guild_id, nick FROM remote_user_guild_nicks WHERE remote_id = $remoteId",
+            {remoteId},
+        );
+        if (nicks) {
+            nicks.forEach(({nick, guild_id}) => {
+                remoteUser.guildNicks.set(guild_id as string, nick as string);
+            });
+        }
+        this.remoteUserCache.set(remoteId, {e: remoteUser, ts: Date.now()});
+        return remoteUser;
+    }
+
+    public async setRemoteUser(user: RemoteUser) {
+        this.remoteUserCache.delete(user.id);
+        const existingData = await this.db.Get(
+            "SELECT * FROM remote_user_data WHERE remote_id = $remoteId",
+            {remoteId: user.id},
+        );
+        if (!existingData) {
+            await this.db.Run(
+            `INSERT INTO remote_user_data VALUES (
+                $remote_id,
+                $displayname,
+                $avatarurl,
+                $avatarurl_mxc
+            )`,
+            {
+                avatarurl: user.avatarurl,
+                avatarurl_mxc: user.avatarurlMxc,
+                displayname: user.displayname,
+                remote_id: user.id,
+            });
+        } else {
+            await this.db.Run(
+`UPDATE remote_user_data SET displayname = $displayname,
+avatarurl = $avatarurl,
+avatarurl_mxc = $avatarurl_mxc WHERE remote_id = $remote_id`,
+            {
+                avatarurl: user.avatarurl,
+                avatarurl_mxc: user.avatarurlMxc,
+                displayname: user.displayname,
+                remote_id: user.id,
+            });
+        }
+        const existingNicks = {};
+        (await this.db.All(
+            "SELECT guild_id, nick FROM remote_user_guild_nicks WHERE remote_id = $remoteId",
+            {remoteId: user.id},
+        )).forEach(({g, n}) => existingNicks[g as string] = n);
+        for (const guildId of user.guildNicks.keys()) {
+            const nick = user.guildNicks.get(guildId) || null;
+            if (existingData) {
+                if (existingNicks[guildId] === nick) {
+                    return;
+                } else if (existingNicks[guildId]) {
+                    await this.db.Run(
+`UPDATE remote_user_guild_nicks SET nick = $nick
+WHERE remote_id = $remote_id
+AND guild_id = $guild_id`,
+                    {
+                        guild_id: guildId,
+                        nick,
+                        remote_id: user.id,
+                    });
+                    return;
+                }
+            }
+            await this.db.Run(
+            `INSERT INTO remote_user_guild_nicks VALUES (
+                $remote_id,
+                $guild_id,
+                $nick
+            )`,
+            {
+                guild_id: guildId,
+                nick,
+                remote_id: user.id,
+            });
+        }
+
+    }
+
+    public async linkUsers(matrixId: string, remoteId: string) {
+        // This is used  ONCE in the bridge to link two IDs, so do not UPSURT data.
+        try {
+            await this.db.Run(`INSERT INTO user_entries VALUES ($matrixId, $remoteId)`, {
+                matrixId,
+                remoteId,
+            });
+        } catch (ex) {
+            log.verbose("Failed to insert into user_entries, entry probably exists:", ex);
+        }
+    }
+}
diff --git a/src/discordas.ts b/src/discordas.ts
index ad328a2207d723fb4e115e66bcb45495b5e2dbda..e395fef41d577bcc3e57c3062c768465bc9ca230 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -149,11 +149,16 @@ async function run(port: number, fileConfig: DiscordBridgeConfig) {
                + "The config option roomStorePath no longer has any use.");
     }
 
+    if (config.database.userStorePath) {
+        log.warn("[DEPRECATED] The user store is now part of the SQL database."
+               + "The config option userStorePath no longer has any use.");
+    }
+
     await bridge.run(port, config);
     log.info(`Started listening on port ${port}`);
 
     try {
-        await store.init(undefined, bridge.getRoomStore());
+        await store.init(undefined, bridge.getRoomStore(), bridge.getUserStore());
     } catch (ex) {
         log.error("Failed to init database. Exiting.", ex);
         process.exit(1);
diff --git a/src/store.ts b/src/store.ts
index 7397b3b6c765066f90a2518c5fc368ccc8af53ea..df328829e90a5a192569918b4f4645297431fba3 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -18,15 +18,16 @@ import * as fs from "fs";
 import { IDbSchema } from "./db/schema/dbschema";
 import { IDbData} from "./db/dbdatainterface";
 import { SQLite3 } from "./db/sqlite3";
-export const CURRENT_SCHEMA = 8;
+export const CURRENT_SCHEMA = 9;
 
 import { Log } from "./log";
 import { DiscordBridgeConfigDatabase } from "./config";
 import { Postgres } from "./db/postgres";
 import { IDatabaseConnector } from "./db/connector";
 import { DbRoomStore } from "./db/roomstore";
+import { DbUserStore } from "./db/userstore";
 import {
-    RoomStore,
+    RoomStore, UserStore,
 } from "matrix-appservice-bridge";
 const log = new Log("DiscordStore");
 /**
@@ -34,9 +35,9 @@ const log = new Log("DiscordStore");
  */
 export class DiscordStore {
     public db: IDatabaseConnector;
-    private version: number;
     private config: DiscordBridgeConfigDatabase;
     private pRoomStore: DbRoomStore;
+    private pUserStore: DbUserStore;
     constructor(configOrFile: DiscordBridgeConfigDatabase|string) {
         if (typeof(configOrFile) === "string") {
             this.config = new DiscordBridgeConfigDatabase();
@@ -44,13 +45,16 @@ export class DiscordStore {
         } else {
             this.config = configOrFile;
         }
-        this.version = 0;
     }
 
     get roomStore() {
         return this.pRoomStore;
     }
 
+    get userStore() {
+        return this.pUserStore;
+    }
+
     public async backup_database(): Promise<void|{}> {
         if (this.config.filename == null) {
             log.warn("Backups not supported on non-sqlite connector");
@@ -86,8 +90,11 @@ export class DiscordStore {
     /**
      * Checks the database has all the tables needed.
      */
-    public async init(overrideSchema: number = 0, roomStore: RoomStore = null): Promise<void> {
+    public async init(
+        overrideSchema: number = 0, roomStore: RoomStore = null, userStore: UserStore = null,
+    ): Promise<void> {
         const SCHEMA_ROOM_STORE_REQUIRED = 8;
+        const SCHEMA_USER_STORE_REQUIRED = 9;
         log.info("Starting DB Init");
         await this.open_database();
         let version = await this.getSchemaVersion();
@@ -99,6 +106,8 @@ export class DiscordStore {
             let schema: IDbSchema;
             if (version === SCHEMA_ROOM_STORE_REQUIRED) { // 8 requires access to the roomstore.
                 schema = (new schemaClass(roomStore) as IDbSchema);
+            } else if (version === SCHEMA_USER_STORE_REQUIRED) {
+                schema = (new schemaClass(userStore) as IDbSchema);
             } else {
                 schema = (new schemaClass() as IDbSchema);
             }
@@ -118,7 +127,6 @@ export class DiscordStore {
                 }
                 throw Error("Failure to update to latest schema.");
             }
-            this.version = version;
             await this.setSchemaVersion(version);
         }
         log.info("Updated database to the latest schema");
@@ -341,6 +349,7 @@ export class DiscordStore {
         try {
             this.db.Open();
             this.pRoomStore = new DbRoomStore(this.db);
+            this.pUserStore = new DbUserStore(this.db);
         } catch (ex) {
             log.error("Error opening database:", ex);
             throw new Error("Couldn't open database. The appservice won't be able to continue.");
diff --git a/src/usersyncroniser.ts b/src/usersyncroniser.ts
index 07a4dacbbae94963fa86937b5e950c60d42870ad..77428d2bc814bd795031d6a65a2ea3be6abe8f14 100644
--- a/src/usersyncroniser.ts
+++ b/src/usersyncroniser.ts
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { User, GuildMember, GuildChannel } from "discord.js";
+import { User, GuildMember } from "discord.js";
 import { DiscordBot } from "./bot";
 import { Util } from "./util";
-import { MatrixUser, RemoteUser, Bridge, Entry, UserBridgeStore, Intent } from "matrix-appservice-bridge";
+import { Bridge, Intent, MatrixUser } from "matrix-appservice-bridge";
 import { DiscordBridgeConfig } from "./config";
-import * as Bluebird from "bluebird";
 import { Log } from "./log";
 import { IMatrixEvent } from "./matrixtypes";
+import { DbUserStore, RemoteUser } from "./db/userstore";
 
 const log = new Log("UserSync");
 
@@ -81,12 +81,11 @@ export class UserSyncroniser {
 
     // roomId+userId => ev
     public userStateHold: Map<string, IMatrixEvent>;
-    private userStore: UserBridgeStore;
     constructor(
         private bridge: Bridge,
         private config: DiscordBridgeConfig,
-        private discord: DiscordBot) {
-        this.userStore = this.bridge.getUserStore();
+        private discord: DiscordBot,
+        private userStore: DbUserStore) {
         this.userStateHold = new Map<string, IMatrixEvent>();
     }
 
@@ -115,18 +114,19 @@ export class UserSyncroniser {
             log.info(`Creating new user ${userState.mxUserId}`);
             remoteUser = new RemoteUser(userState.id);
             await this.userStore.linkUsers(
-                new MatrixUser(userState.mxUserId.substr("@".length)),
-                remoteUser,
+                userState.mxUserId.substr("@".length),
+                userState.id,
             );
 
         } else {
-            remoteUser = await this.userStore.getRemoteUser(userState.id);
+            const rUser = await this.userStore.getRemoteUser(userState.id);
+            remoteUser = rUser ? rUser : new RemoteUser(userState.id);
         }
 
         if (userState.displayName !== null) {
             log.verbose(`Updating displayname for ${userState.mxUserId} to "${userState.displayName}"`);
             await intent.setDisplayName(userState.displayName);
-            remoteUser.set("displayname", userState.displayName);
+            remoteUser.displayname = userState.displayName;
             userUpdated = true;
         }
 
@@ -138,16 +138,16 @@ export class UserSyncroniser {
                 userState.avatarId,
             );
             await intent.setAvatarUrl(avatarMxc.mxcUrl);
-            remoteUser.set("avatarurl", userState.avatarUrl);
-            remoteUser.set("avatarurl_mxc", avatarMxc.mxcUrl);
+            remoteUser.avatarurl = userState.avatarUrl;
+            remoteUser.avatarurlMxc = avatarMxc.mxcUrl;
             userUpdated = true;
         }
 
         if (userState.removeAvatar) {
             log.verbose(`Clearing avatar_url for ${userState.mxUserId} to "${userState.avatarUrl}"`);
             await intent.setAvatarUrl(null);
-            remoteUser.set("avatarurl", null);
-            remoteUser.set("avatarurl_mxc", null);
+            remoteUser.avatarurl = null;
+            remoteUser.avatarurlMxc = null;
             userUpdated = true;
         }
 
@@ -198,11 +198,14 @@ export class UserSyncroniser {
             return;
         }
         const remoteUser = await this.userStore.getRemoteUser(memberState.id);
+        if (!remoteUser) {
+            throw Error("Remote user not found");
+        }
         const intent = this.bridge.getIntent(memberState.mxUserId);
         /* The intent class tries to be smart and deny a state update for <PL50 users.
            Obviously a user can change their own state so we use the client instead. */
         await intent.getClient().sendStateEvent(roomId, "m.room.member", {
-            "avatar_url": remoteUser.get("avatarurl_mxc"),
+            "avatar_url": remoteUser.avatarurlMxc,
             "displayname": memberState.displayName,
             "membership": "join",
             "uk.half-shot.discord.member": {
@@ -215,8 +218,7 @@ export class UserSyncroniser {
         }, memberState.mxUserId);
 
         if (guildId) {
-            const nickKey = `nick_${guildId}`;
-            remoteUser.set(nickKey, memberState.displayName);
+            remoteUser.guildNicks.set(guildId, memberState.displayName);
         }
         await this.userStore.setRemoteUser(remoteUser);
     }
@@ -245,13 +247,13 @@ export class UserSyncroniser {
             return userState;
         }
 
-        const oldDisplayName = remoteUser.get("displayname");
+        const oldDisplayName = remoteUser.displayname;
         if (oldDisplayName !== displayName) {
             log.verbose(`User ${discordUser.id} displayname should be updated`);
             userState.displayName = displayName;
         }
 
-        const oldAvatarUrl = remoteUser.get("avatarurl");
+        const oldAvatarUrl = remoteUser.avatarurl;
         if (oldAvatarUrl !== discordUser.avatarURL) {
             log.verbose(`User ${discordUser.id} avatarurl should be updated`);
             if (discordUser.avatarURL !== null) {
@@ -370,7 +372,7 @@ export class UserSyncroniser {
     }
 
     public async UpdateStateForGuilds(remoteUser: RemoteUser) {
-        const id = remoteUser.getId();
+        const id = remoteUser.id;
         log.info(`Got update for ${id}.`);
 
         await Util.AsyncForEach(this.discord.GetGuilds(), async (guild) => {
diff --git a/test/test_usersyncroniser.ts b/test/test_usersyncroniser.ts
index dacc5c7754a6f8b1ba011abd281f0307b34a25f6..c5b7b1cff0418e3f72494acf3194652745255b53 100644
--- a/test/test_usersyncroniser.ts
+++ b/test/test_usersyncroniser.ts
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import * as Chai from "chai";
-import { Bridge, RemoteUser } from "matrix-appservice-bridge";
+import { Bridge } from "matrix-appservice-bridge";
 import {IGuildMemberState, IUserState, UserSyncroniser} from "../src/usersyncroniser";
 import {MockUser} from "./mocks/user";
 import {DiscordBridgeConfig} from "../src/config";
@@ -26,6 +26,7 @@ import { MockChannel } from "./mocks/channel";
 import { MockRole } from "./mocks/role";
 import { IMatrixEvent } from "../src/matrixtypes";
 import { Util } from "../src/util";
+import { RemoteUser } from "../src/db/userstore";
 
 // we are a test file and thus need those
 /* tslint:disable:no-unused-expression max-file-line-count no-any */
@@ -63,7 +64,7 @@ const UserSync = (Proxyquire("../src/usersyncroniser", {
     },
 })).UserSyncroniser;
 
-function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser {
+function CreateUserSync(remoteUsers: RemoteUser[] = []): UserSyncroniser {
     UTIL_UPLOADED_AVATAR = false;
     SEV_ROOM_ID = null;
     SEV_CONTENT = null;
@@ -111,30 +112,6 @@ function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser {
                 },
             };
         },
-        getUserStore: () => {
-            REMOTEUSER_SET = null;
-            LINK_RM_USER = null;
-            LINK_MX_USER = null;
-            return {
-                getRemoteUser: (id) => {
-                    const user = remoteUsers.find((u) => u.id === id);
-                    if (user === undefined) {
-                        return null;
-                    }
-                    return user;
-                },
-                getRemoteUsersFromMatrixId: (id) => {
-                    return remoteUsers.filter((u) => u.id === id);
-                },
-                linkUsers: (mxUser, remoteUser) => {
-                    LINK_MX_USER = mxUser;
-                    LINK_RM_USER = remoteUser;
-                },
-                setRemoteUser: async (remoteUser) => {
-                    REMOTEUSER_SET = remoteUser;
-                },
-            };
-        },
     };
     const discordbot: any = {
         GetChannelFromRoomId: (id) => {
@@ -160,9 +137,23 @@ function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser {
             return GUILD_ROOM_IDS;
         },
     };
+    REMOTEUSER_SET = null;
+    LINK_RM_USER = null;
+    LINK_MX_USER = null;
+    const userStore = {
+        getRemoteUser: (id) => remoteUsers.find((u) => u.id === id) || null,
+        getRemoteUsersFromMatrixId: (id) => remoteUsers.filter((u) => u.id === id),
+        linkUsers: (mxUser, remoteUser) => {
+            LINK_MX_USER = mxUser;
+            LINK_RM_USER = remoteUser;
+        },
+        setRemoteUser: async (remoteUser) => {
+            REMOTEUSER_SET = remoteUser;
+        },
+    };
     const config = new DiscordBridgeConfig();
     config.bridge.domain = "localhost";
-    return new UserSync(bridge as Bridge, config, discordbot);
+    return new UserSync(bridge as Bridge, config, discordbot, userStore as any);
 }
 
 describe("UserSyncroniser", () => {
@@ -185,10 +176,9 @@ describe("UserSyncroniser", () => {
             expect(state.avatarUrl).equals("test.jpg");
         });
         it("Will change display names", async () => {
-            const remoteUser = new RemoteUser("123456", {
-                avatarurl: "test.jpg",
-                displayname: "MrFake",
-            });
+            const remoteUser = new RemoteUser("123456");
+            remoteUser.avatarurl = "test.jpg";
+            remoteUser.displayname = "TestUsername";
 
             const userSync = CreateUserSync([remoteUser]);
             const user = new MockUser(
@@ -207,10 +197,9 @@ describe("UserSyncroniser", () => {
             expect(state.avatarUrl, "AvatarUrl").is.null;
         });
         it("Will change avatars", async () => {
-            const remoteUser = new RemoteUser("123456", {
-                avatarurl: "test.jpg",
-                displayname: "TestUsername#6969",
-            });
+            const remoteUser = new RemoteUser("123456");
+            remoteUser.avatarurl = "test.jpg";
+            remoteUser.displayname = "TestUsername#6969";
 
             const userSync = CreateUserSync([remoteUser]);
             const user = new MockUser(
@@ -229,10 +218,9 @@ describe("UserSyncroniser", () => {
             expect(state.displayName, "DisplayName").is.null;
         });
         it("Will remove avatars", async () => {
-            const remoteUser = new RemoteUser("123456", {
-                avatarurl: "test.jpg",
-                displayname: "TestUsername#6969",
-            });
+            const remoteUser = new RemoteUser("123456");
+            remoteUser.avatarurl = "test.jpg";
+            remoteUser.displayname = "TestUsername#6969";
 
             const userSync = CreateUserSync([remoteUser]);
             const user = new MockUser(
@@ -284,9 +272,9 @@ describe("UserSyncroniser", () => {
             expect(LINK_RM_USER).is.not.null;
             expect(REMOTEUSER_SET).is.not.null;
             expect(DISPLAYNAME_SET).equal("123456");
-            expect(REMOTEUSER_SET.data.displayname).equal("123456");
+            expect(REMOTEUSER_SET.displayname).equal("123456");
             expect(AVATAR_SET).is.null;
-            expect(REMOTEUSER_SET.data.avatarurl).is.undefined;
+            expect(REMOTEUSER_SET.avatarurl).is.null;
         });
         it("Will set an avatar", async () => {
             const userSync = CreateUserSync();
@@ -305,8 +293,8 @@ describe("UserSyncroniser", () => {
             expect(AVATAR_SET).equal("avatarset");
             expect(UTIL_UPLOADED_AVATAR).to.be.true;
             expect(REMOTEUSER_SET).is.not.null;
-            expect(REMOTEUSER_SET.data.avatarurl).equal("654321");
-            expect(REMOTEUSER_SET.data.displayname).is.undefined;
+            expect(REMOTEUSER_SET.avatarurl).equal("654321");
+            expect(REMOTEUSER_SET.displayname).is.null;
             expect(DISPLAYNAME_SET).is.null;
         });
         it("Will remove an avatar", async () => {
@@ -326,8 +314,8 @@ describe("UserSyncroniser", () => {
             expect(AVATAR_SET).is.null;
             expect(UTIL_UPLOADED_AVATAR).to.be.false;
             expect(REMOTEUSER_SET).is.not.null;
-            expect(REMOTEUSER_SET.data.avatarurl).is.null;
-            expect(REMOTEUSER_SET.data.displayname).is.undefined;
+            expect(REMOTEUSER_SET.avatarurl).is.null;
+            expect(REMOTEUSER_SET.displayname).is.null;
             expect(DISPLAYNAME_SET).is.null;
         });
         it("will do nothing if nothing needs to be done", async () => {
@@ -363,7 +351,7 @@ describe("UserSyncroniser", () => {
             };
             await userSync.ApplyStateToRoom(state, "!abc:localhost", "123456");
             expect(REMOTEUSER_SET).is.not.null;
-            expect(REMOTEUSER_SET.data.nick_123456).is.equal("Good Boy");
+            expect(REMOTEUSER_SET.guildNicks.get("123456")).is.equal("Good Boy");
             expect(SEV_ROOM_ID).is.equal("!abc:localhost");
             expect(SEV_CONTENT.displayname).is.equal("Good Boy");
             expect(SEV_KEY).is.equal("@_discord_123456:localhost");