From 49b51e478fa75b8d5254662de3265edcf8906004 Mon Sep 17 00:00:00 2001
From: "Cheng-Han, Wu" <jackymaxj@gmail.com>
Date: Wed, 20 Apr 2016 18:03:55 +0800
Subject: [PATCH] Refactor server with Sequelize ORM, refactor server configs,
 now will show note status (created or updated) and support docs (note alias)

---
 README.md                                    |  99 ++-
 app.js                                       | 298 +++----
 config.js                                    |  88 ---
 config.json                                  |  43 ++
 hackmd_schema.sql                            |  77 --
 lib/auth.js                                  |  76 +-
 lib/config.js                                | 112 +++
 lib/db.js                                    | 151 ----
 lib/models/index.js                          |  37 +
 lib/models/note.js                           | 208 +++++
 lib/models/temp.js                           |  19 +
 lib/models/user.js                           |  77 ++
 lib/note.js                                  | 237 ------
 lib/ot/server.js                             |   2 +-
 lib/realtime.js                              | 665 +++++++---------
 lib/response.js                              | 774 ++++++++-----------
 lib/temp.js                                  |  84 --
 lib/user.js                                  | 110 ---
 package.json                                 |  11 +-
 public/{ => docs}/features.md                |  52 +-
 public/docs/slide-example.md                 |  81 ++
 public/docs/yaml-metadata.md                 |  97 +++
 public/index.ejs                             | 237 ------
 public/js/extra.js                           |  15 +-
 public/js/history.js                         |  10 +-
 public/js/index.js                           |  17 +-
 public/js/pretty.js                          |   3 +-
 public/views/body.ejs                        |  28 +-
 public/views/hackmd.ejs                      |  15 +
 public/views/header.ejs                      |   6 +-
 public/views/index.ejs                       | 206 ++++-
 public/views/modal.ejs                       |  34 +
 public/views/pretty.ejs                      |   4 +-
 public/views/{slide/reveal.hbs => slide.hbs} |   2 +-
 public/views/slide/listing.hbs               |  22 -
 35 files changed, 1877 insertions(+), 2120 deletions(-)
 delete mode 100644 config.js
 create mode 100644 config.json
 delete mode 100644 hackmd_schema.sql
 create mode 100644 lib/config.js
 delete mode 100644 lib/db.js
 create mode 100644 lib/models/index.js
 create mode 100644 lib/models/note.js
 create mode 100644 lib/models/temp.js
 create mode 100644 lib/models/user.js
 delete mode 100644 lib/note.js
 delete mode 100644 lib/temp.js
 delete mode 100644 lib/user.js
 rename public/{ => docs}/features.md (91%)
 create mode 100644 public/docs/slide-example.md
 create mode 100644 public/docs/yaml-metadata.md
 delete mode 100644 public/index.ejs
 create mode 100644 public/views/hackmd.ejs
 create mode 100644 public/views/modal.ejs
 rename public/views/{slide/reveal.hbs => slide.hbs} (98%)
 delete mode 100644 public/views/slide/listing.hbs

diff --git a/README.md b/README.md
index e15da92..7e1cc44 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,17 @@ Still in early stage, feel free to fork or contribute to this.
 
 Thanks for your using! :smile:
 
+[docker-hackmd](https://github.com/hackmdio/docker-hackmd)
+---
+Before you going too far, here is the great docker repo for HackMD.  
+With docker, you can deploy a server in minutes without any hardtime.
+
+[migration-to-0.4.0](https://github.com/hackmdio/migration-to-0.4.0)
+---
+We've dropped MongoDB after version 0.4.0.  
+So here is the migration tool for you to transfer old DB data to new DB.  
+This tool is also used for official service.
+
 Browsers Requirement
 ---
 - Chrome >= 45, Chrome for Android >= 47
@@ -20,33 +31,24 @@ Browsers Requirement
 
 Prerequisite
 ---
-- Node.js 4.x or up (test up to 5.8.0)
-- PostgreSQL 9.3.x or 9.4.x
-- MongoDB 3.0.x
+- Node.js 4.x or up (test up to 5.10.1)
+- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL)
 - npm and bower
 
 Get started
 ---
 1. Download a release and unzip or clone into a directory
 2. Enter the directory and type `npm install && bower install`, will install all the dependencies
-3. Install PostgreSQL and MongoDB (yes, currently we need both)
-4. Import database schema, see more on below
-5. Setup the configs, see more on below
-6. Setup environment variables, which will overwrite the configs
-7. Run the server as you like (node, forever, pm2)
-
-Import database schema
----
-The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`  
-To import the sql file in PostgreSQL, see http://www.postgresql.org/docs/9.4/static/backup-dump.html
-
-The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
+3. Setup the configs, see more on below
+4. Setup environment variables, which will overwrite the configs
+5. Run the server as you like (node, forever, pm2)
 
 Structure
 ---
 ```
 hackmd/
 ├── tmp/			--- temporary files
+├── docs/			--- document files
 ├── lib/			--- server libraries
 └── public/			--- client files
 	├── css/		--- css styles
@@ -57,63 +59,58 @@ hackmd/
 
 Configuration files
 ---
-There are some config you need to change in below files
+There are some configs you need to change in below files
 ```
-./config.js				--- for server settings
-./public/js/index.js	--- for client settings
+./config.json			--- for server settings
 ./public/js/common.js	--- for client settings
 ```
 
-Client-side index.js settings
+Client settings `common.js`
 ---
 | variables | example values | description |
 | --------- | ------ | ----------- |
 | debug | `true` or `false` | set debug mode, show more logs |
-| version | `0.3.2` | current version, must match same var in server side `config.js` |
-
-Client-side common.js settings
----
-| variables | example values | description |
-| --------- | ------ | ----------- |
 | domain | `localhost` | domain name |
 | urlpath | `hackmd` | sub url path, like: `www.example.com/<urlpath>` |
 
-Environment variables
+Environment variables (will overwrite other server configs)
 ---
 | variables | example values | description |
 | --------- | ------ | ----------- |
-| NODE_ENV  | `production` or `development` | show current environment status |
-| DATABASE_URL | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string |
-| MONGOLAB_URI | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string |
-| PORT | `80` | web port |
-| SSLPORT | `443` | ssl web port |
-| DOMAIN | `localhost` | domain name |
-| URL_PATH | `hackmd` | sub url path, like `www.example.com/<URL_PATH>` |
+| NODE_ENV  | `production` or `development` | set current environment (will apply correspond settings in the `config.json`) |
+| PORT | `80` | web app port |
+| DEBUG | `true` or `false` | set debug mode, show more logs |
 
-Server-side config.js settings
+Server settings `config.json`
 ---
 | variables | example values | description |
 | --------- | ------ | ----------- |
-| testport  | `3000` | debug web port, fallback to this when not set in environment |
-| testsslport | `3001` | debug web ssl port, fallback to this when not set in environment |
-| usessl | `true` or `false` | set to use ssl |
-| protocolusessl | `true` or `false` | set to use ssl protocol |
-| urladdport | `true` or `false` | set to add port on oauth callback url |
 | debug | `true` or `false` | set debug mode, show more logs |
-| usecdn | `true` or `false` | set to use CDN resources or not |
-| version | `0.3.2` | currnet version, must match same var in client side `index.js` |
+| domain | `localhost` | domain name |
+| urlpath | `hackmd` | sub url path, like `www.example.com/<urlpath>` |
+| port | `80` | web app port |
 | alloworigin | `['localhost']` | domain name whitelist |
-| sslkeypath | `./cert/client.key` | ssl key path |
-| sslcertpath | `./cert/hackmd_io.crt` | ssl cert path |
-| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain |
-| dhparampath | `./cert/dhparam.pem` | ssl dhparam path |
-| tmppath | `./tmp/` | temp file path |
-| postgresqlstring | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string, fallback to this when not set in environment |
-| mongodbstring | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string, fallback to this when not set in environment |
+| usessl | `true` or `false` | set to use ssl server (if true will auto turn on `protocolusessl`) |
+| protocolusessl | `true` or `false` | set to use ssl protocol for resources path |
+| urladdport | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) |
+| usecdn | `true` or `false` | set to use CDN resources or not |
+| db | `{ "dialect": "sqlite", "storage": "./db.hackmd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) |
+| sslkeypath | `./cert/client.key` | ssl key path (only need when you set usessl) |
+| sslcertpath | `./cert/hackmd_io.crt` | ssl cert path (only need when you set usessl) |
+| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain (only need when you set usessl) |
+| dhparampath | `./cert/dhparam.pem` | ssl dhparam path (only need when you set usessl) |
+| tmppath | `./tmp/` | temp directory path |
+| defaultnotepath | `./public/default.md` | default note file path |
+| docspath | `./public/docs` | docs directory path |
+| indexpath | `./public/views/index.ejs` | index template file path |
+| hackmdpath | `./public/views/hackmd.ejs` | hackmd template file path |
+| errorpath | `./public/views/error.ejs` | error template file path |
+| prettypath | `./public/views/pretty.ejs` | pretty template file path |
+| slidepath | `./public/views/slide.hbs` | slide template file path |
 | sessionname | `connect.sid` | cookie session name |
 | sessionsecret | `secret` | cookie session secret |
 | sessionlife | `14 * 24 * 60 * 60 * 1000` | cookie session life |
-| sessiontouch | `1 * 3600` | cookie session touch |
+| staticcachetime | `1 * 24 * 60 * 60 * 1000` | static file cache time |
 | heartbeatinterval | `5000` | socket.io heartbeat interval |
 | heartbeattimeout | `10000` | socket.io heartbeat timeout |
 | documentmaxlength | `100000` | note max length |
@@ -122,8 +119,8 @@ Third-party integration api key settings
 ---
 | service | file path | description |
 | ------- | --------- | ----------- |
-| facebook, twitter, github, dropbox | `config.js` | for signin |
-| imgur | `config.js` | for image upload |
+| facebook, twitter, github, dropbox | `config.json` | for signin |
+| imgur | `config.json` | for image upload |
 | dropbox | `public/views/foot.ejs` | for chooser and saver |
 | google drive | `public/js/common.js` | for export and import |
 
diff --git a/app.js b/app.js
index 2261cc3..1dae8a4 100644
--- a/app.js
+++ b/app.js
@@ -7,12 +7,10 @@ var passport = require('passport');
 var methodOverride = require('method-override');
 var cookieParser = require('cookie-parser');
 var bodyParser = require('body-parser');
-var mongoose = require('mongoose');
 var compression = require('compression')
 var session = require('express-session');
-var MongoStore = require('connect-mongo')(session);
+var SequelizeStore = require('connect-session-sequelize')(session.Store);
 var fs = require('fs');
-var shortid = require('shortid');
 var imgur = require('imgur');
 var formidable = require('formidable');
 var morgan = require('morgan');
@@ -20,12 +18,11 @@ var passportSocketIo = require("passport.socketio");
 var helmet = require('helmet');
 
 //core
-var config = require("./config.js");
+var config = require("./lib/config.js");
 var logger = require("./lib/logger.js");
-var User = require("./lib/user.js");
-var Temp = require("./lib/temp.js");
 var auth = require("./lib/auth.js");
 var response = require("./lib/response.js");
+var models = require("./lib/models");
 
 //server setup
 if (config.usessl) {
@@ -60,11 +57,7 @@ app.use(morgan('combined', {
 //socket io
 var io = require('socket.io')(server);
 
-// connect to the mongodb
-mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
-
 //others
-var db = require("./lib/db.js");
 var realtime = require("./lib/realtime.js");
 
 //assign socket io to realtime
@@ -82,13 +75,9 @@ var urlencodedParser = bodyParser.urlencoded({
 });
 
 //session store
-var sessionStore = new MongoStore({
-        mongooseConnection: mongoose.connection,
-        touchAfter: config.sessiontouch
-    },
-    function (err) {
-        logger.info(err);
-    });
+var sessionStore = new SequelizeStore({
+    db: models.sequelize
+});
 
 //compression
 app.use(compression());
@@ -139,15 +128,21 @@ app.use(passport.session());
 
 //serialize and deserialize
 passport.serializeUser(function (user, done) {
-    //logger.info('serializeUser: ' + user._id);
-    done(null, user._id);
+    logger.info('serializeUser: ' + user.id);
+    return done(null, user.id);
 });
 passport.deserializeUser(function (id, done) {
-    User.model.findById(id, function (err, user) {
-        //logger.info(user)
-        if (!err) done(null, user);
-        else done(err, null);
-    })
+    models.User.findOne({
+        where: {
+            id: id
+        }
+    }).then(function (user) {
+        logger.info('deserializeUser: ' + user.id);
+        return done(null, user);
+    }).catch(function (err) {
+        logger.error(err);
+        return done(err, null);
+    });
 });
 
 //routes
@@ -161,13 +156,17 @@ app.engine('html', ejs.renderFile);
 //get index
 app.get("/", response.showIndex);
 //get 403 forbidden
-app.get("/403", function(req, res) {
+app.get("/403", function (req, res) {
     response.errorForbidden(res);
 });
 //get 404 not found
-app.get("/404", function(req, res) {
+app.get("/404", function (req, res) {
     response.errorNotFound(res);
 });
+//get 500 internal error
+app.get("/500", function (req, res) {
+    response.errorInternalError(res);
+});
 //get status
 app.get("/status", function (req, res, next) {
     realtime.getStatus(function (data) {
@@ -184,19 +183,26 @@ app.get("/temp", function (req, res) {
         if (!tempid)
             response.errorForbidden(res);
         else {
-            Temp.findTemp(tempid, function (err, temp) {
-                if (err || !temp)
-                    response.errorForbidden(res);
+            models.Temp.findOne({
+                where: {
+                    id: tempid
+                }
+            }).then(function (temp) {
+                if (!temp)
+                    response.errorNotFound(res);
                 else {
                     res.header("Access-Control-Allow-Origin", "*");
                     res.send({
                         temp: temp.data
                     });
-                    temp.remove(function (err) {
+                    temp.destroy().catch(function (err) {
                         if (err)
                             logger.error('remove temp failed: ' + err);
                     });
                 }
+            }).catch(function (err) {
+                logger.error(err);
+                return response.errorInternalError(res);
             });
         }
     }
@@ -207,15 +213,16 @@ app.post("/temp", urlencodedParser, function (req, res) {
     if (config.alloworigin.indexOf(host) == -1)
         response.errorForbidden(res);
     else {
-        var id = shortid.generate();
         var data = req.body.data;
-        if (!id || !data)
+        if (!data)
             response.errorForbidden(res);
         else {
             if (config.debug)
                 logger.info('SERVER received temp from [' + host + ']: ' + req.body.data);
-            Temp.newTemp(id, data, function (err, temp) {
-                if (!err && temp) {
+            models.Temp.create({
+                data: data
+            }).then(function (temp) {
+                if (temp) {
                     res.header("Access-Control-Allow-Origin", "*");
                     res.send({
                         status: 'ok',
@@ -223,125 +230,149 @@ app.post("/temp", urlencodedParser, function (req, res) {
                     });
                 } else
                     response.errorInternalError(res);
+            }).catch(function (err) {
+                logger.error(err);
+                return response.errorInternalError(res);
             });
         }
     }
 });
 //facebook auth
-app.get('/auth/facebook',
-    passport.authenticate('facebook'),
-    function (req, res) {});
-//facebook auth callback
-app.get('/auth/facebook/callback',
-    passport.authenticate('facebook', {
-        failureRedirect: config.getserverurl()
-    }),
-    function (req, res) {
-        res.redirect(config.getserverurl());
-    });
+if (config.facebook) {
+    app.get('/auth/facebook',
+        passport.authenticate('facebook'),
+        function (req, res) {});
+    //facebook auth callback
+    app.get('/auth/facebook/callback',
+        passport.authenticate('facebook', {
+            failureRedirect: config.serverurl
+        }),
+        function (req, res) {
+            res.redirect(config.serverurl);
+        });
+}
 //twitter auth
-app.get('/auth/twitter',
-    passport.authenticate('twitter'),
-    function (req, res) {});
-//twitter auth callback
-app.get('/auth/twitter/callback',
-    passport.authenticate('twitter', {
-        failureRedirect: config.getserverurl()
-    }),
-    function (req, res) {
-        res.redirect(config.getserverurl());
-    });
+if (config.twitter) {
+    app.get('/auth/twitter',
+        passport.authenticate('twitter'),
+        function (req, res) {});
+    //twitter auth callback
+    app.get('/auth/twitter/callback',
+        passport.authenticate('twitter', {
+            failureRedirect: config.serverurl
+        }),
+        function (req, res) {
+            res.redirect(config.serverurl);
+        });
+}
 //github auth
-app.get('/auth/github',
-    passport.authenticate('github'),
-    function (req, res) {});
-//github auth callback
-app.get('/auth/github/callback',
-    passport.authenticate('github', {
-        failureRedirect: config.getserverurl()
-    }),
-    function (req, res) {
-        res.redirect(config.getserverurl());
-    });
-//github callback actions
-app.get('/auth/github/callback/:noteId/:action', response.githubActions);
+if (config.github) {
+    app.get('/auth/github',
+        passport.authenticate('github'),
+        function (req, res) {});
+    //github auth callback
+    app.get('/auth/github/callback',
+        passport.authenticate('github', {
+            failureRedirect: config.serverurl
+        }),
+        function (req, res) {
+            res.redirect(config.serverurl);
+        });
+    //github callback actions
+    app.get('/auth/github/callback/:noteId/:action', response.githubActions);
+}
 //dropbox auth
-app.get('/auth/dropbox',
-    passport.authenticate('dropbox-oauth2'),
-    function (req, res) {});
-//dropbox auth callback
-app.get('/auth/dropbox/callback',
-    passport.authenticate('dropbox-oauth2', {
-        failureRedirect: config.getserverurl()
-    }),
-    function (req, res) {
-        res.redirect(config.getserverurl());
-    });
+if (config.dropbox) {
+    app.get('/auth/dropbox',
+        passport.authenticate('dropbox-oauth2'),
+        function (req, res) {});
+    //dropbox auth callback
+    app.get('/auth/dropbox/callback',
+        passport.authenticate('dropbox-oauth2', {
+            failureRedirect: config.serverurl
+        }),
+        function (req, res) {
+            res.redirect(config.serverurl);
+        });
+}
 //logout
 app.get('/logout', function (req, res) {
     if (config.debug && req.isAuthenticated())
-        logger.info('user logout: ' + req.user._id);
+        logger.info('user logout: ' + req.user.id);
     req.logout();
-    res.redirect(config.getserverurl());
+    res.redirect(config.serverurl);
 });
 //get history
 app.get('/history', function (req, res) {
     if (req.isAuthenticated()) {
-        User.model.findById(req.user._id, function (err, user) {
-            if (err) {
-                logger.error('read history failed: ' + err);
-            } else {
-                var history = [];
-                if (user.history)
-                    history = JSON.parse(user.history);
-                res.send({
-                    history: history
-                });
+        models.User.findOne({
+            where: {
+                id: req.user.id
             }
+        }).then(function (user) {
+            if (!user)
+                return response.errorNotFound(res);
+            var history = [];
+            if (user.history)
+                history = JSON.parse(user.history);
+            res.send({
+                history: history
+            });
+            if (config.debug)
+                logger.info('read history success: ' + user.id);
+        }).catch(function (err) {
+            logger.error('read history failed: ' + err);
+            return response.errorInternalError(res);
         });
     } else {
-        response.errorForbidden(res);
+        return response.errorForbidden(res);
     }
 });
 //post history
 app.post('/history', urlencodedParser, function (req, res) {
     if (req.isAuthenticated()) {
         if (config.debug)
-            logger.info('SERVER received history from [' + req.user._id + ']: ' + req.body.history);
-        User.model.findById(req.user._id, function (err, user) {
-            if (err) {
-                logger.error('write history failed: ' + err);
-            } else {
-                user.history = req.body.history;
-                user.save(function (err) {
-                    if (err) {
-                        logger.error('write user history failed: ' + err);
-                    } else {
-                        if (config.debug)
-                            logger.info("write user history success: " + user._id);
-                    };
-                });
+            logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history);
+        models.User.update({
+            history: req.body.history
+        }, {
+            where: {
+                id: req.user.id
             }
+        }).then(function (count) {
+            if (!count)
+                return response.errorNotFound(res);
+            if (config.debug)
+                logger.info("write user history success: " + req.user.id);
+        }).catch(function (err) {
+            logger.error('write history failed: ' + err);
+            return response.errorInternalError(res);
         });
         res.end();
     } else {
-        response.errorForbidden(res);
+        return response.errorForbidden(res);
     }
 });
 //get me info
 app.get('/me', function (req, res) {
     if (req.isAuthenticated()) {
-        User.model.findById(req.user._id, function (err, user) {
-            if (err) {
-                logger.error('read me failed: ' + err);
-            } else {
-                var profile = JSON.parse(user.profile);
-                res.send({
-                    status: 'ok',
-                    id: req.user._id,
-                    name: profile.displayName || profile.username
-                });
+        models.User.findOne({
+            where: {
+                id: req.user.id
             }
+        }).then(function (user) {
+            if (!user)
+                return response.errorNotFound(res);
+            var profile = models.User.parseProfile(user.profile);
+            res.send({
+                status: 'ok',
+                id: req.user.id,
+                name: profile.name,
+                photo: profile.photo
+            });
+        }).catch(function (err) {
+            logger.error('read me failed: ' + err);
+            return response.errorInternalError(res);
         });
     } else {
         res.send({
@@ -370,19 +401,17 @@ app.post('/uploadimage', function (req, res) {
                     })
                     .catch(function (err) {
                         logger.error(err);
-                        res.send('upload image error');
+                        return res.send('upload image error');
                     });
             } catch (err) {
                 logger.error(err);
-                res.send('upload image error');
+                return res.send('upload image error');
             }
         }
     });
 });
 //get new note
 app.get("/new", response.newNote);
-//get features
-app.get("/features", response.showFeatures);
 //get publish note
 app.get("/s/:shortid", response.showPublishNote);
 //publish note actions
@@ -412,15 +441,22 @@ io.set('heartbeat timeout', config.heartbeattimeout);
 io.sockets.on('connection', realtime.connection);
 
 //listen
-if (config.usessl) {
-    server.listen(config.sslport, function () {
-        logger.info('HTTPS Server listening at sslport %d', config.sslport);
-    });
-} else {
-    server.listen(config.port, function () {
-        logger.info('HTTP Server listening at port %d', config.port);
-    });
+function startListen() {
+    if (config.usessl) {
+        server.listen(config.port, function () {
+            logger.info('HTTPS Server listening at port %d', config.port);
+        });
+    } else {
+        server.listen(config.port, function () {
+            logger.info('HTTP Server listening at port %d', config.port);
+        });
+    }
 }
+
+// sync db then start listen
+models.sequelize.sync().then(startListen);
+
+// log uncaught exception
 process.on('uncaughtException', function (err) {
     logger.error(err);
-});
+});
\ No newline at end of file
diff --git a/config.js b/config.js
deleted file mode 100644
index 972fc3b..0000000
--- a/config.js
+++ /dev/null
@@ -1,88 +0,0 @@
-//config
-var path = require('path');
-
-var domain = process.env.DOMAIN;
-var urlpath = process.env.URL_PATH;
-var testport = '3000';
-var testsslport = '3001';
-var port = process.env.PORT || testport;
-var sslport = process.env.SSLPORT || testsslport;
-var usessl = false; // use node https server
-var protocolusessl = false; // use ssl protocol
-var urladdport = true; //add port on getserverurl
-
-var config = {
-    debug: false,
-    usecdn: false,
-    version: '0.3.4',
-    domain: domain,
-    alloworigin: ['add here to allow origin to cross'],
-    urlpath: urlpath,
-    testport: testport,
-    testsslport: testsslport,
-    port: port,
-    sslport: sslport,
-    sslkeypath: 'change this',
-    sslcertpath: 'change this',
-    sslcapath: ['change this'],
-    dhparampath: 'change this',
-    usessl: usessl,
-    protocolusessl: protocolusessl,
-    getserverurl: function() {
-        var protocol = protocolusessl ? 'https://' : 'http://';
-        var url = domain;
-        if (usessl)
-            url = protocol + url + (sslport == 443 || !urladdport ? '' : ':' + sslport);
-        else
-            url = protocol + url + (port == 80 || !urladdport ? '' : ':' + port);
-        if (urlpath)
-            url = url + '/' + urlpath;
-        return url;
-    },
-    //path
-    tmppath: "./tmp/",
-    defaultnotepath: path.join(__dirname, '/public', "default.md"),
-    defaultfeaturespath: path.join(__dirname, '/public', "features.md"),
-    indexpath: path.join(__dirname, '/public/', "index.ejs"),
-    hackmdpath: path.join(__dirname, '/public/views', "index.ejs"),
-    errorpath: path.join(__dirname, '/public/views', "error.ejs"),
-    prettypath: path.join(__dirname, '/public/views', 'pretty.ejs'),
-    //db string
-    postgresqlstring: "change this",
-    mongodbstring: "change this",
-    //constants
-    featuresnotename: "features",
-    sessionname: 'change this',
-    sessionsecret: 'change this',
-    sessionlife: 14 * 24 * 60 * 60 * 1000, //14 days
-    sessiontouch: 1 * 3600, //1 hour
-    heartbeatinterval: 5000,
-    heartbeattimeout: 10000,
-    documentmaxlength: 100000,
-    //auth
-    facebook: {
-        clientID: 'change this',
-        clientSecret: 'change this',
-        callbackPath: '/auth/facebook/callback'
-    },
-    twitter: {
-        consumerKey: 'change this',
-        consumerSecret: 'change this',
-        callbackPath: '/auth/twitter/callback'
-    },
-    github: {
-        clientID: 'change this',
-        clientSecret: 'change this',
-        callbackPath: '/auth/github/callback'
-    },
-    dropbox: {
-        clientID: 'change this',
-        clientSecret: 'change this',
-        callbackPath: '/auth/dropbox/callback'
-    },
-    imgur: {
-        clientID: 'change this'
-    }
-};
-
-module.exports = config;
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..16ad258
--- /dev/null
+++ b/config.json
@@ -0,0 +1,43 @@
+{
+    "development": {
+        "domain": "localhost",
+        "db": {
+            "username": "",
+            "password": "",
+            "database": "hackmd",
+            "host": "localhost",
+            "port": "3306",
+            "dialect": "mysql"
+        }
+    },
+    "production": {
+        "domain": "localhost",
+        "db": {
+            "username": "",
+            "password": "",
+            "database": "hackmd",
+            "host": "localhost",
+            "port": "5432",
+            "dialect": "postgres"
+        },
+        "facebook": {
+            "clientID": "change this",
+            "clientSecret": "change this"
+        },
+        "twitter": {
+            "consumerKey": "change this",
+            "consumerSecret": "change this"
+        },
+        "github": {
+            "clientID": "change this",
+            "clientSecret": "change this"
+        },
+        "dropbox": {
+            "clientID": "change this",
+            "clientSecret": "change this"
+        },
+        "imgur": {
+            "clientID": "change this"
+        }
+    }
+}
\ No newline at end of file
diff --git a/hackmd_schema.sql b/hackmd_schema.sql
deleted file mode 100644
index ac7d671..0000000
--- a/hackmd_schema.sql
+++ /dev/null
@@ -1,77 +0,0 @@
---
--- PostgreSQL database dump
---
-
-SET statement_timeout = 0;
-SET lock_timeout = 0;
-SET client_encoding = 'UTF8';
-SET standard_conforming_strings = on;
-SET check_function_bodies = false;
-SET client_min_messages = warning;
-
---
--- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: 
---
-
-CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
-
-
---
--- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: 
---
-
-COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
-
-
-SET search_path = public, pg_catalog;
-
-SET default_tablespace = '';
-
-SET default_with_oids = false;
-
---
--- Name: notes; Type: TABLE; Schema: public; Owner: postgres; Tablespace: 
---
-
-CREATE TABLE notes (
-    id character varying(256) NOT NULL,
-    owner character varying(256) NOT NULL,
-    content text,
-    title text,
-    create_time timestamp without time zone DEFAULT now() NOT NULL,
-    update_time timestamp without time zone DEFAULT now() NOT NULL
-);
-
-
-ALTER TABLE notes OWNER TO "postgres";
-
---
--- Name: notes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: 
---
-
-ALTER TABLE ONLY notes
-    ADD CONSTRAINT notes_pkey PRIMARY KEY (id);
-
-
---
--- Name: unique_notes; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: 
---
-
-ALTER TABLE ONLY notes
-    ADD CONSTRAINT unique_notes UNIQUE (id);
-
-
---
--- Name: public; Type: ACL; Schema: -; Owner: postgres
---
-
-REVOKE ALL ON SCHEMA public FROM PUBLIC;
-REVOKE ALL ON SCHEMA public FROM "postgres";
-GRANT ALL ON SCHEMA public TO "postgres";
-GRANT ALL ON SCHEMA public TO PUBLIC;
-
-
---
--- PostgreSQL database dump complete
---
-
diff --git a/lib/auth.js b/lib/auth.js
index dc8b94c..af3e8d1 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -7,44 +7,60 @@ var GithubStrategy = require('passport-github').Strategy;
 var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
 
 //core
-var User = require('./user.js');
-var config = require('../config.js');
+var config = require('./config.js');
 var logger = require("./logger.js");
+var models = require("./models");
 
 function callback(accessToken, refreshToken, profile, done) {
     //logger.info(profile.displayName || profile.username);
-    User.findOrNewUser(profile.id, profile, function (err, user) {
-        if (err || user == null) {
-            logger.error('auth callback failed: ' + err);
-        } else {
-            if (config.debug && user)
-                logger.info('user login: ' + user._id);
-            done(null, user);
+    models.User.findOrCreate({
+        where: {
+            profileid: profile.id.toString()
+        },
+        defaults: {
+            profile: JSON.stringify(profile)
         }
-    });
+    }).spread(function(user, created) {
+        if (user) {
+            if (config.debug)
+                logger.info('user login: ' + user.id);
+            return done(null, user);
+        }
+    }).catch(function(err) {
+        logger.error('auth callback failed: ' + err);
+        return done(err, null);
+    })
 }
 
 //facebook
-module.exports = passport.use(new FacebookStrategy({
-    clientID: config.facebook.clientID,
-    clientSecret: config.facebook.clientSecret,
-    callbackURL: config.getserverurl() + config.facebook.callbackPath
-}, callback));
+if (config.facebook) {
+    module.exports = passport.use(new FacebookStrategy({
+        clientID: config.facebook.clientID,
+        clientSecret: config.facebook.clientSecret,
+        callbackURL: config.serverurl + '/auth/facebook/callback'
+    }, callback));
+}
 //twitter
-passport.use(new TwitterStrategy({
-    consumerKey: config.twitter.consumerKey,
-    consumerSecret: config.twitter.consumerSecret,
-    callbackURL: config.getserverurl() + config.twitter.callbackPath
-}, callback));
+if (config.twitter) {
+    passport.use(new TwitterStrategy({
+        consumerKey: config.twitter.consumerKey,
+        consumerSecret: config.twitter.consumerSecret,
+        callbackURL: config.serverurl + '/auth/twitter/callback'
+    }, callback));
+}
 //github
-passport.use(new GithubStrategy({
-    clientID: config.github.clientID,
-    clientSecret: config.github.clientSecret,
-    callbackURL: config.getserverurl() + config.github.callbackPath
-}, callback));
+if (config.github) {
+    passport.use(new GithubStrategy({
+        clientID: config.github.clientID,
+        clientSecret: config.github.clientSecret,
+        callbackURL: config.serverurl + '/auth/github/callback'
+    }, callback));
+}
 //dropbox
-passport.use(new DropboxStrategy({
-    clientID: config.dropbox.clientID,
-    clientSecret: config.dropbox.clientSecret,
-    callbackURL: config.getserverurl() + config.dropbox.callbackPath
-}, callback));
\ No newline at end of file
+if (config.dropbox) {
+    passport.use(new DropboxStrategy({
+        clientID: config.dropbox.clientID,
+        clientSecret: config.dropbox.clientSecret,
+        callbackURL: config.serverurl + '/auth/dropbox/callback'
+    }, callback));
+}
\ No newline at end of file
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 0000000..386b088
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,112 @@
+// external modules
+var path = require('path');
+
+// configs
+var env = process.env.NODE_ENV || 'development';
+var config = require(path.join(__dirname, '..', 'config.json'))[env];
+var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development'));
+
+// url
+var domain = config.domain || 'localhost';
+var urlpath = config.urlpath || '';
+var port = process.env.PORT || config.port || 3000;
+var alloworigin = config.alloworigin || ['localhost'];
+
+var usessl = !!config.usessl;
+var protocolusessl = (config.usessl === true && typeof config.protocolusessl === 'undefined') ? true : !!config.protocolusessl;
+var urladdport = !!config.urladdport;
+
+var usecdn = !!config.usecdn;
+
+// db
+var db = config.db || {
+    dialect: 'sqlite',
+    storage: './db.hackmd.sqlite'
+};
+
+// ssl path
+var sslkeypath = config.sslkeypath || ''
+var sslcertpath = config.sslcertpath || '';
+var sslcapath = config.sslcapath || '';
+var dhparampath = config.dhparampath || '';
+
+// other path
+var tmppath = config.tmppath || './tmp';
+var defaultnotepath = config.defaultnotepath || './public/default.md';
+var docspath = config.docspath || './public/docs';
+var indexpath = config.indexpath || './public/views/index.ejs';
+var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs';
+var errorpath = config.errorpath || './public/views/error.ejs';
+var prettypath = config.prettypath || './public/views/pretty.ejs';
+var slidepath = config.slidepath || './public/views/slide.hbs';
+
+// session
+var sessionname = config.sessionname || 'connect.sid';
+var sessionsecret = config.sessionsecret || 'secret';
+var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000; //14 days
+
+// static files
+var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000; // 1 day
+
+// socket.io
+var heartbeatinterval = config.heartbeatinterval || 5000;
+var heartbeattimeout = config.heartbeattimeout || 10000;
+
+// document
+var documentmaxlength = config.documentmaxlength || 100000;
+
+// auth
+var facebook = config.facebook || false;
+var twitter = config.twitter || false;
+var github = config.github || false;
+var dropbox = config.dropbox || false;
+var imgur = config.imgur || false;
+
+function getserverurl() {
+    var protocol = protocolusessl ? 'https://' : 'http://';
+    var url = protocol + domain;
+    if (urladdport && ((usessl && port != 443) || (!usessl && port != 80)))
+        url += ':' + port;
+    if (urlpath)
+        url += '/' + urlpath;
+    return url;
+}
+
+var version = '0.4.0';
+var cwd = path.join(__dirname, '..');
+
+module.exports = {
+    version: version,
+    debug: debug,
+    urlpath: urlpath,
+    port: port,
+    alloworigin: alloworigin,
+    usessl: usessl,
+    serverurl: getserverurl(),
+    usecdn: usecdn,
+    db: db,
+    sslkeypath: path.join(cwd, sslkeypath),
+    sslcertpath: path.join(cwd, sslcertpath),
+    sslcapath: path.join(cwd, sslcapath),
+    dhparampath: path.join(cwd, dhparampath),
+    tmppath: path.join(cwd, tmppath),
+    defaultnotepath: path.join(cwd, defaultnotepath),
+    docspath: path.join(cwd, docspath),
+    indexpath: path.join(cwd, indexpath),
+    hackmdpath: path.join(cwd, hackmdpath),
+    errorpath: path.join(cwd, errorpath),
+    prettypath: path.join(cwd, prettypath),
+    slidepath: path.join(cwd, slidepath),
+    sessionname: sessionname,
+    sessionsecret: sessionsecret,
+    sessionlife: sessionlife,
+    staticcachetime: staticcachetime,
+    heartbeatinterval: heartbeatinterval,
+    heartbeattimeout: heartbeattimeout,
+    documentmaxlength: documentmaxlength,
+    facebook: facebook,
+    twitter: twitter,
+    github: github,
+    dropbox: dropbox,
+    imgur: imgur
+};
\ No newline at end of file
diff --git a/lib/db.js b/lib/db.js
deleted file mode 100644
index 1ac6f0e..0000000
--- a/lib/db.js
+++ /dev/null
@@ -1,151 +0,0 @@
-//db
-//external modules
-var pg = require('pg');
-var fs = require('fs');
-var util = require('util');
-
-//core
-var config = require("../config.js");
-var logger = require("./logger.js");
-
-//public
-var db = {
-    readFromFile: readFromDB,
-    saveToFile: saveToFile,
-    newToDB: newToDB,
-    readFromDB: readFromDB,
-    saveToDB: saveToDB,
-    countFromDB: countFromDB
-};
-
-function getDBClient() {
-    return new pg.Client(process.env.DATABASE_URL || config.postgresqlstring);
-}
-
-function readFromFile(callback) {
-    fs.readFile('hackmd', 'utf8', function (err, data) {
-        if (err) throw err;
-        callback(data);
-    });
-}
-
-function saveToFile(doc) {
-    fs.writeFile('hackmd', doc, function (err) {
-        if (err) throw err;
-    });
-}
-
-var updatequery = "UPDATE notes SET title='%s', content='%s', update_time=NOW() WHERE id='%s';";
-var insertquery = "INSERT INTO notes (id, owner, content) VALUES ('%s', '%s', '%s');";
-var insertifnotexistquery = "INSERT INTO notes (id, owner, content) \
-SELECT '%s', '%s', '%s' \
-WHERE NOT EXISTS (SELECT 1 FROM notes WHERE id='%s') RETURNING *;";
-var selectquery = "SELECT * FROM notes WHERE id='%s';";
-var countquery = "SELECT count(*) FROM notes;";
-
-function newToDB(id, owner, body, callback) {
-    var client = getDBClient();
-    client.connect(function (err) {
-        if (err) {
-            client.end();
-            callback(err, null);
-            return logger.error('could not connect to postgres', err);
-        }
-        var newnotequery = util.format(insertquery, id, owner, body);
-        //logger.info(newnotequery);
-        client.query(newnotequery, function (err, result) {
-            client.end();
-            if (err) {
-                callback(err, null);
-                return logger.error("new note to db failed: " + err);
-            } else {
-                if (config.debug)
-                    logger.info("new note to db success");
-                callback(null, result);
-            }
-        });
-    });
-}
-
-function readFromDB(id, callback) {
-    var client = getDBClient();
-    client.connect(function (err) {
-        if (err) {
-            client.end();
-            callback(err, null);
-            return logger.error('could not connect to postgres', err);
-        }
-        var readquery = util.format(selectquery, id);
-        //logger.info(readquery);
-        client.query(readquery, function (err, result) {
-            client.end();
-            if (err) {
-                callback(err, null);
-                return logger.error("read from db failed: " + err);
-            } else {
-                //logger.info(result.rows);
-                if (result.rows.length <= 0) {
-                    callback("not found note in db", null);
-                    return logger.error("not found note in db: " + id, err);
-                } else {
-                    if(config.debug)
-                        logger.info("read from db success");
-                    callback(null, result);
-                }
-            }
-        });
-    });
-}
-
-function saveToDB(id, title, data, callback) {
-    var client = getDBClient();
-    client.connect(function (err) {
-        if (err) {
-            client.end();
-            callback(err, null);
-            return logger.error('could not connect to postgres', err);
-        }
-        var savequery = util.format(updatequery, title, data, id);
-        //logger.info(savequery);
-        client.query(savequery, function (err, result) {
-            client.end();
-            if (err) {
-                callback(err, null);
-                return logger.error("save to db failed: " + err);
-            } else {
-                if (config.debug)
-                    logger.info("save to db success");
-                callback(null, result);
-            }
-        });
-    });
-}
-
-function countFromDB(callback) {
-    var client = getDBClient();
-    client.connect(function (err) {
-        if (err) {
-            client.end();
-            callback(err, null);
-            return logger.error('could not connect to postgres', err);
-        }
-        client.query(countquery, function (err, result) {
-            client.end();
-            if (err) {
-                callback(err, null);
-                return logger.error("count from db failed: " + err);
-            } else {
-                //logger.info(result.rows);
-                if (result.rows.length <= 0) {
-                    callback("not found note in db", null);
-                } else {
-                    if(config.debug)
-                        logger.info("count from db success");
-                    callback(null, result);
-                }
-            }
-        });
-    });
-}
-
-module.exports = db;
\ No newline at end of file
diff --git a/lib/models/index.js b/lib/models/index.js
new file mode 100644
index 0000000..3b49d45
--- /dev/null
+++ b/lib/models/index.js
@@ -0,0 +1,37 @@
+"use strict";
+
+// external modules
+var fs = require("fs");
+var path = require("path");
+var Sequelize = require("sequelize");
+
+// core
+var config = require('../config.js');
+var logger = require("../logger.js");
+
+var dbconfig = config.db;
+dbconfig.logging = config.debug ? logger.info : false;
+var sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig);
+
+var db = {};
+
+fs
+    .readdirSync(__dirname)
+    .filter(function (file) {
+        return (file.indexOf(".") !== 0) && (file !== "index.js");
+    })
+    .forEach(function (file) {
+        var model = sequelize.import(path.join(__dirname, file));
+        db[model.name] = model;
+    });
+
+Object.keys(db).forEach(function (modelName) {
+    if ("associate" in db[modelName]) {
+        db[modelName].associate(db);
+    }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+module.exports = db;
\ No newline at end of file
diff --git a/lib/models/note.js b/lib/models/note.js
new file mode 100644
index 0000000..96043b7
--- /dev/null
+++ b/lib/models/note.js
@@ -0,0 +1,208 @@
+"use strict";
+
+// external modules
+var fs = require('fs');
+var path = require('path');
+var LZString = require('lz-string');
+var marked = require('marked');
+var cheerio = require('cheerio');
+var shortId = require('shortid');
+var Sequelize = require("sequelize");
+var async = require('async');
+
+// core
+var config = require("../config.js");
+var logger = require("../logger.js");
+
+// permission types
+var permissionTypes = ["freely", "editable", "locked", "private"];
+
+module.exports = function (sequelize, DataTypes) {
+    var Note = sequelize.define("Note", {
+        id: {
+            type: DataTypes.UUID,
+            primaryKey: true,
+            defaultValue: Sequelize.UUIDV4
+        },
+        shortid: {
+            type: DataTypes.STRING,
+            unique: true,
+            allowNull: false,
+            defaultValue: shortId.generate
+        },
+        alias: {
+            type: DataTypes.STRING,
+            unique: true
+        },
+        permission: {
+            type: DataTypes.ENUM,
+            values: permissionTypes
+        },
+        viewcount: {
+            type: DataTypes.INTEGER,
+            allowNull: false,
+            defaultValue: 0
+        },
+        title: {
+            type: DataTypes.TEXT
+        },
+        content: {
+            type: DataTypes.TEXT
+        },
+        lastchangeAt: {
+            type: DataTypes.DATE
+        }
+    }, {
+        classMethods: {
+            associate: function (models) {
+                Note.belongsTo(models.User, {
+                    foreignKey: "ownerId",
+                    as: "owner",
+                    constraints: false
+                });
+                Note.belongsTo(models.User, {
+                    foreignKey: "lastchangeuserId",
+                    as: "lastchangeuser",
+                    constraints: false
+                });
+            },
+            checkFileExist: function (filePath) {
+                try {
+                    return fs.statSync(filePath).isFile();
+                } catch (err) {
+                    return false;
+                }
+            },
+            checkNoteIdValid: function (id) {
+                var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+                var result = id.match(uuidRegex);
+                if (result && result.length == 1)
+                    return true;
+                else
+                    return false;
+            },
+            parseNoteId: function (noteId, callback) {
+                async.series({
+                    parseNoteIdByAlias: function (_callback) {
+                        // try to parse note id by alias (e.g. doc)
+                        Note.findOne({
+                            where: {
+                                alias: noteId
+                            }
+                        }).then(function (note) {
+                            if (note) {
+                                return callback(null, note.id);
+                            } else {
+                                var filePath = path.join(config.docspath, noteId + '.md');
+                                if (Note.checkFileExist(filePath)) {
+                                    Note.create({
+                                        alias: noteId,
+                                        owner: null,
+                                        permission: 'locked'
+                                    }).then(function (note) {
+                                        return callback(null, note.id);
+                                    }).catch(function (err) {
+                                        return _callback(err, null);
+                                    });
+                                } else {
+                                    return _callback(null, null);
+                                }
+                            }
+                        }).catch(function (err) {
+                            return _callback(err, null);
+                        });
+                    },
+                    parseNoteIdByLZString: function (_callback) {
+                        // try to parse note id by LZString Base64
+                        try {
+                            var id = LZString.decompressFromBase64(noteId);
+                            if (id && Note.checkNoteIdValid(id))
+                                return callback(null, id);
+                            else
+                                return _callback(null, null);
+                        } catch (err) {
+                            return _callback(err, null);
+                        }
+                    },
+                    parseNoteIdByShortId: function (_callback) {
+                        // try to parse note id by shortId
+                        try {
+                            if (shortId.isValid(noteId)) {
+                                Note.findOne({
+                                    where: {
+                                        shortid: noteId
+                                    }
+                                }).then(function (note) {
+                                    if (!note) return _callback(null, null);
+                                    return callback(null, note.id);
+                                }).catch(function (err) {
+                                    return _callback(err, null);
+                                });
+                            } else {
+                                return _callback(null, null);
+                            }
+                        } catch (err) {
+                            return _callback(err, null);
+                        }
+                    }
+                }, function (err, result) {
+                    if (err) {
+                        logger.error(err);
+                        return callback(err, null);
+                    }
+                    return callback(null, null);
+                });
+            },
+            parseNoteTitle: function (body) {
+                var $ = cheerio.load(marked(body));
+                var h1s = $("h1");
+                var title = "";
+                if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
+                    title = h1s.first().text();
+                else
+                    title = "Untitled";
+                return title;
+            },
+            decodeTitle: function (title) {
+                var decodedTitle = LZString.decompressFromBase64(title);
+                if (decodedTitle) title = decodedTitle;
+                else title = 'Untitled';
+                return title;
+            },
+            generateWebTitle: function (title) {
+                title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
+                return title;
+            }
+        },
+        hooks: {
+            beforeCreate: function (note, options, callback) {
+                // if no content specified then use default note
+                if (!note.content) {
+                    var body = null;
+                    var filePath = null;
+                    if (!note.alias) {
+                        filePath = config.defaultnotepath;
+                    } else {
+                        filePath = path.join(config.docspath, note.alias + '.md');
+                    }
+                    if (Note.checkFileExist(filePath)) {
+                        body = fs.readFileSync(filePath, 'utf8');
+                        note.title = LZString.compressToBase64(Note.parseNoteTitle(body));
+                        note.content = LZString.compressToBase64(body);
+                    }
+                }
+                // if no permission specified and have owner then give editable permission, else default permission is freely
+                if (!note.permission) {
+                    if (note.ownerId) {
+                        note.permission = "editable";
+                    } else {
+                        note.permission = "freely";
+                    }
+                }
+                return callback(null, note);
+            }
+        }
+    });
+
+    return Note;
+};
\ No newline at end of file
diff --git a/lib/models/temp.js b/lib/models/temp.js
new file mode 100644
index 0000000..6eeff15
--- /dev/null
+++ b/lib/models/temp.js
@@ -0,0 +1,19 @@
+"use strict";
+
+//external modules
+var shortId = require('shortid');
+
+module.exports = function (sequelize, DataTypes) {
+    var Temp = sequelize.define("Temp", {
+        id: {
+            type: DataTypes.STRING,
+            primaryKey: true,
+            defaultValue: shortId.generate
+        },
+        data: {
+            type: DataTypes.TEXT
+        }
+    });
+    
+    return Temp;
+};
\ No newline at end of file
diff --git a/lib/models/user.js b/lib/models/user.js
new file mode 100644
index 0000000..e1a373d
--- /dev/null
+++ b/lib/models/user.js
@@ -0,0 +1,77 @@
+"use strict";
+
+// external modules
+var md5 = require("blueimp-md5");
+var Sequelize = require("sequelize");
+
+// core
+var logger = require("../logger.js");
+
+module.exports = function (sequelize, DataTypes) {
+    var User = sequelize.define("User", {
+        id: {
+            type: DataTypes.UUID,
+            primaryKey: true,
+            defaultValue: Sequelize.UUIDV4
+        },
+        profileid: {
+            type: DataTypes.STRING,
+            unique: true
+        },
+        profile: {
+            type: DataTypes.TEXT
+        },
+        history: {
+            type: DataTypes.TEXT
+        }
+    }, {
+        classMethods: {
+            associate: function (models) {
+                User.hasMany(models.Note, {
+                    foreignKey: "ownerId",
+                    constraints: false
+                });
+                User.hasMany(models.Note, {
+                    foreignKey: "lastchangeuserId",
+                    constraints: false
+                });
+            },
+            parseProfile: function (profile) {
+                try {
+                    var profile = JSON.parse(profile);
+                } catch (err) {
+                    logger.error(err);
+                    profile = null;
+                }
+                if (profile) {
+                    profile = {
+                        name: profile.displayName || profile.username,
+                        photo: User.parsePhotoByProfile(profile)
+                    }
+                }
+                return profile;
+            },
+            parsePhotoByProfile: function (profile) {
+                var photo = null;
+                switch (profile.provider) {
+                    case "facebook":
+                        photo = 'https://graph.facebook.com/' + profile.id + '/picture';
+                        break;
+                    case "twitter":
+                        photo = profile.photos[0].value;
+                        break;
+                    case "github":
+                        photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
+                        break;
+                    case "dropbox":
+                        //no image api provided, use gravatar
+                        photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
+                        break;
+                }
+                return photo;
+            }
+        }
+    });
+    
+    return User;
+};
\ No newline at end of file
diff --git a/lib/note.js b/lib/note.js
deleted file mode 100644
index 0965b23..0000000
--- a/lib/note.js
+++ /dev/null
@@ -1,237 +0,0 @@
-//note
-//external modules
-var mongoose = require('mongoose');
-var Schema = mongoose.Schema;
-var LZString = require('lz-string');
-var marked = require('marked');
-var cheerio = require('cheerio');
-var shortId = require('shortid');
-
-//others
-var db = require("./db.js");
-var logger = require("./logger.js");
-
-//permission types
-permissionTypes = ["freely", "editable", "locked", "private"];
-
-// create a note model
-var model = mongoose.model('note', {
-    id: String,
-    shortid: {
-        type: String,
-        unique: true,
-        default: shortId.generate
-    },
-    permission: {
-        type: String,
-        enum: permissionTypes
-    },
-    lastchangeuser: {
-        type: Schema.Types.ObjectId,
-        ref: 'user'
-    },
-    viewcount: {
-        type: Number,
-        default: 0
-    },
-    updated: Date,
-    created: Date
-});
-
-//public
-var note = {
-    model: model,
-    findNote: findNote,
-    newNote: newNote,
-    findOrNewNote: findOrNewNote,
-    checkNoteIdValid: checkNoteIdValid,
-    checkNoteExist: checkNoteExist,
-    getNoteTitle: getNoteTitle,
-    decodeTitle: decodeTitle,
-    generateWebTitle: generateWebTitle,
-    increaseViewCount: increaseViewCount,
-    updatePermission: updatePermission,
-    updateLastChangeUser: updateLastChangeUser
-};
-
-function checkNoteIdValid(noteId) {
-    try {
-        //logger.info(noteId);
-        var id = LZString.decompressFromBase64(noteId);
-        if (!id) return false;
-        var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
-        var result = id.match(uuidRegex);
-        if (result && result.length == 1)
-            return true;
-        else
-            return false;
-    } catch (err) {
-        logger.error(err);
-        return false;
-    }
-}
-
-function checkNoteExist(noteId) {
-    try {
-        //logger.info(noteId);
-        var id = LZString.decompressFromBase64(noteId);
-        db.readFromDB(id, function (err, result) {
-            if (err) return false;
-            return true;
-        });
-    } catch (err) {
-        logger.error(err);
-        return false;
-    }
-}
-
-//get title
-function getNoteTitle(body) {
-    var $ = cheerio.load(marked(body));
-    var h1s = $("h1");
-    var title = "";
-    if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
-        title = h1s.first().text();
-    else
-        title = "Untitled";
-    return title;
-}
-
-// decode title
-function decodeTitle(title) {
-    var decodedTitle = LZString.decompressFromBase64(title);
-    if (decodedTitle) title = decodedTitle;
-    else title = 'Untitled';
-    return title;
-}
-
-//generate note web page title
-function generateWebTitle(title) {
-    title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
-    return title;
-}
-
-function findNote(id, callback) {
-    model.findOne({
-        $or: [
-            {
-                id: id
-            },
-            {
-                shortid: id
-            }
-        ]
-    }, function (err, note) {
-        if (err) {
-            logger.error('find note failed: ' + err);
-            callback(err, null);
-        }
-        if (!err && note) {
-            callback(null, note);
-        } else {
-            logger.error('find note failed: ' + err);
-            callback(err, null);
-        };
-    });
-}
-
-function newNote(id, owner, callback) {
-    var permission = "freely";
-    if (owner && owner != "null") {
-        permission = "editable";
-    }
-    var note = new model({
-        id: id,
-        permission: permission,
-        updated: Date.now(),
-        created: Date.now()
-    });
-    note.save(function (err) {
-        if (err) {
-            logger.error('new note failed: ' + err);
-            callback(err, null);
-        } else {
-            logger.info("new note success: " + note.id);
-            callback(null, note);
-        };
-    });
-}
-
-function findOrNewNote(id, owner, callback) {
-    findNote(id, function (err, note) {
-        if (err || !note) {
-            newNote(id, owner, function (err, note) {
-                if (err) {
-                    logger.error('find or new note failed: ' + err);
-                    callback(err, null);
-                } else {
-                    callback(null, note);
-                }
-            });
-        } else {
-            if (!note.permission) {
-                var permission = "freely";
-                if (owner && owner != "null") {
-                    permission = "editable";
-                }
-                note.permission = permission;
-                note.updated = Date.now();
-                note.save(function (err) {
-                    if (err) {
-                        logger.error('add note permission failed: ' + err);
-                        callback(err, null);
-                    } else {
-                        logger.info("add note permission success: " + note.id);
-                        callback(null, note);
-                    };
-                });
-            } else {
-                callback(null, note);
-            }
-        }
-    });
-}
-
-function increaseViewCount(note, callback) {
-    note.viewcount++;
-    note.updated = Date.now();
-    note.save(function (err) {
-        if (err) {
-            logger.error('increase note viewcount failed: ' + err);
-            callback(err, null);
-        } else {
-            logger.info("increase note viewcount success: " + note.id);
-            callback(null, note);
-        };
-    });
-}
-
-function updatePermission(note, permission, callback) {
-    note.permission = permission;
-    note.updated = Date.now();
-    note.save(function (err) {
-        if (err) {
-            logger.error('update note permission failed: ' + err);
-            callback(err, null);
-        } else {
-            logger.info("update note permission success: " + note.id);
-            callback(null, note);
-        };
-    });
-}
-
-function updateLastChangeUser(note, lastchangeuser, callback) {
-    note.lastchangeuser = lastchangeuser;
-    note.updated = Date.now();
-    note.save(function (err) {
-        if (err) {
-            logger.error('update note lastchangeuser failed: ' + err);
-            callback(err, null);
-        } else {
-            logger.info("update note lastchangeuser success: " + note.id);
-            callback(null, note);
-        };
-    });
-}
-
-module.exports = note;
\ No newline at end of file
diff --git a/lib/ot/server.js b/lib/ot/server.js
index b655929..227eba2 100644
--- a/lib/ot/server.js
+++ b/lib/ot/server.js
@@ -1,4 +1,4 @@
-var config = require('../../config');
+var config = require('../config');
 
 if (typeof ot === 'undefined') {
   var ot = {};
diff --git a/lib/realtime.js b/lib/realtime.js
index 081d401..a8bef97 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -5,24 +5,19 @@ var cookieParser = require('cookie-parser');
 var url = require('url');
 var async = require('async');
 var LZString = require('lz-string');
-var shortId = require('shortid');
 var randomcolor = require("randomcolor");
 var Chance = require('chance'),
     chance = new Chance();
 var moment = require('moment');
 
 //core
-var config = require("../config.js");
+var config = require("./config.js");
 var logger = require("./logger.js");
+var models = require("./models");
 
 //ot
 var ot = require("./ot/index.js");
 
-//others
-var db = require("./db.js");
-var Note = require("./note.js");
-var User = require("./user.js");
-
 //public
 var realtime = {
     io: null,
@@ -72,12 +67,6 @@ function emitCheck(note) {
         lastchangeuserprofile: note.lastchangeuserprofile
     };
     realtime.io.to(note.id).emit('check', out);
-    /*
-    for (var i = 0, l = note.socks.length; i < l; i++) {
-        var sock = note.socks[i];
-        sock.emit('check', out);
-    };
-    */
 }
 
 //actions
@@ -88,70 +77,82 @@ var updater = setInterval(function () {
     async.each(Object.keys(notes), function (key, callback) {
         var note = notes[key];
         if (note.server.isDirty) {
-            if (config.debug)
-                logger.info("updater found dirty note: " + key);
-            updaterUpdateMongo(note, function(err, result) {
-                if (err) return callback(err, null);
-                updaterUpdatePostgres(note, function(err, result) {
-                    if (err) return callback(err, null);
-                    callback(null, null);
-                });
+            if (config.debug) logger.info("updater found dirty note: " + key);
+            updateNote(note, function(err, _note) {
+                if (!_note) {
+                    realtime.io.to(note.id).emit('info', {
+                        code: 404
+                    });
+                    logger.error('note not found: ', note.id);
+                }
+                if (err || !_note) {
+                    for (var i = 0, l = note.socks.length; i < l; i++) {
+                        var sock = note.socks[i];
+                        sock.disconnect(true);
+                    }
+                    return callback(err, null);
+                }
+                note.server.isDirty = false;
+                note.updatetime = moment(_note.lastchangeAt).valueOf();
+                emitCheck(note);
+                return callback(null, null);
             });
         } else {
-            callback(null, null);
+            return callback(null, null);
         }
     }, function (err) {
         if (err) return logger.error('updater error', err);
     });
 }, 1000);
-function updaterUpdateMongo(note, callback) {
-    Note.findNote(note.id, function (err, _note) {
-        if (err || !_note) return callback(err, null);
+function updateNote(note, callback) {
+    models.Note.findOne({
+        where: {
+            id: note.id
+        }
+    }).then(function (_note) {
+        if (!_note) return callback(null, null);
         if (note.lastchangeuser) {
-            if (_note.lastchangeuser != note.lastchangeuser) {
-                var lastchangeuser = note.lastchangeuser;
-                var lastchangeuserprofile = null;
-                User.findUser(lastchangeuser, function (err, user) {
-                    if (err) return callback(err, null);
-                    if (user && user.profile) {
-                        var profile = JSON.parse(user.profile);
-                        if (profile) {
-                            lastchangeuserprofile = {
-                                name: profile.displayName || profile.username,
-                                photo: User.parsePhotoByProfile(profile)
-                            }
-                            _note.lastchangeuser = lastchangeuser;
-                            note.lastchangeuserprofile = lastchangeuserprofile;
-                            Note.updateLastChangeUser(_note, lastchangeuser, function (err, result) {
-                                if (err) return callback(err, null);
-                                callback(null, null);
-                            });
-                        }
+            if (_note.lastchangeuserId != note.lastchangeuser) {
+                models.User.findOne({
+                    where: {
+                        id: note.lastchangeuser
                     }
+                }).then(function (user) {
+                    if (!user) return callback(null, null);
+                    note.lastchangeuserprofile = models.User.parseProfile(user.profile);
+                    return finishUpdateNote(note, _note, callback);
+                }).catch(function (err) {
+                    logger.error(err);
+                    return callback(err, null);
                 });
+            } else {
+                return finishUpdateNote(note, _note, callback);
             }
         } else {
-            _note.lastchangeuser = null;
             note.lastchangeuserprofile = null;
-            Note.updateLastChangeUser(_note, null, function (err, result) {
-                if (err) return callback(err, null);
-                callback(null, null);
-            });
+            return finishUpdateNote(note, _note, callback);
         }
+    }).catch(function (err) {
+        logger.error(err);
+        return callback(err, null);
     });
 }
-function updaterUpdatePostgres(note, callback) {
-    //postgres update
+function finishUpdateNote(note, _note, callback) {
     var body = note.server.document;
-    var title = Note.getNoteTitle(body);
+    var title = models.Note.parseNoteTitle(body);
     title = LZString.compressToBase64(title);
     body = LZString.compressToBase64(body);
-    db.saveToDB(note.id, title, body, function (err, result) {
-        if (err) return callback(err, null);
-        note.server.isDirty = false;
-        note.updatetime = Date.now();
-        emitCheck(note);
-        callback(null, null);
+    var values = {
+        title: title,
+        content: body,
+        lastchangeuserId: note.lastchangeuser,
+        lastchangeAt: Date.now()
+    };
+    _note.update(values).then(function (_note) {
+        return callback(null, _note);
+    }).catch(function (err) {
+        logger.error(err);
+        return callback(err, null);
     });
 }
 //clean when user not in any rooms or user not in connected list
@@ -170,15 +171,14 @@ var cleaner = setInterval(function () {
             disconnectSocketQueue.push(socket);
             disconnect(socket);
         }
-        callback(null, null);
+        return callback(null, null);
     }, function (err) {
         if (err) return logger.error('cleaner error', err);
     });
 }, 60000);
 
 function getStatus(callback) {
-    db.countFromDB(function (err, data) {
-        if (err) return logger.info(err);
+    models.Note.count().then(function (notecount) {
         var distinctaddresses = [];
         var regaddresses = [];
         var distinctregaddresses = [];
@@ -208,58 +208,58 @@ function getStatus(callback) {
                 }
             }
         });
-        User.getUserCount(function (err, regcount) {
-            if (err) {
-                logger.error('get status failed: ' + err);
-                return;
-            }
-            if (callback)
-                callback({
-                    onlineNotes: Object.keys(notes).length,
-                    onlineUsers: Object.keys(users).length,
-                    distinctOnlineUsers: distinctaddresses.length,
-                    notesCount: data.rows[0].count,
-                    registeredUsers: regcount,
-                    onlineRegisteredUsers: regaddresses.length,
-                    distinctOnlineRegisteredUsers: distinctregaddresses.length,
-					isConnectionBusy: isConnectionBusy,
-					connectionSocketQueueLength: connectionSocketQueue.length,
-					isDisconnectBusy: isDisconnectBusy,
-					disconnectSocketQueueLength: disconnectSocketQueue.length
-                });
+        models.User.count().then(function (regcount) {
+            return callback ? callback({
+                onlineNotes: Object.keys(notes).length,
+                onlineUsers: Object.keys(users).length,
+                distinctOnlineUsers: distinctaddresses.length,
+                notesCount: notecount,
+                registeredUsers: regcount,
+                onlineRegisteredUsers: regaddresses.length,
+                distinctOnlineRegisteredUsers: distinctregaddresses.length,
+                isConnectionBusy: isConnectionBusy,
+                connectionSocketQueueLength: connectionSocketQueue.length,
+                isDisconnectBusy: isDisconnectBusy,
+                disconnectSocketQueueLength: disconnectSocketQueue.length
+            }) : null;
+        }).catch(function (err) {
+            return logger.error('count user failed: ' + err);
         });
+    }).catch(function (err) {
+        return logger.error('count note failed: ' + err); 
     });
 }
 
-function getNotenameFromSocket(socket) {
+function extractNoteIdFromSocket(socket) {
     if (!socket || !socket.handshake || !socket.handshake.headers) {
-        return;
+        return false;
     }
     var referer = socket.handshake.headers.referer;
     if (!referer) {
-        return socket.disconnect(true);
+        return false;
     }
     var hostUrl = url.parse(referer);
-    var notename = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1];
-    if (notename == config.featuresnotename) {
-        return notename;
+    var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1];
+    return noteId;
+}
+
+function parseNoteIdFromSocket(socket, callback) {
+    var noteId = extractNoteIdFromSocket(socket);
+    if (!noteId) {
+        return callback(null, null);
     }
-    if (!Note.checkNoteIdValid(notename)) {
-        socket.emit('info', {
-            code: 404
-        });
-        return socket.disconnect(true);
-    }
-    notename = LZString.decompressFromBase64(notename);
-    return notename;
+    models.Note.parseNoteId(noteId, function (err, id) {
+        if (err || !id) return callback(err, id);
+        return callback(null, id);
+    });
 }
 
 function emitOnlineUsers(socket) {
-    var notename = getNotenameFromSocket(socket);
-    if (!notename || !notes[notename]) return;
+    var noteId = socket.noteId;
+    if (!noteId || !notes[noteId]) return;
     var users = [];
-    Object.keys(notes[notename].users).forEach(function (key) {
-        var user = notes[notename].users[key];
+    Object.keys(notes[noteId].users).forEach(function (key) {
+        var user = notes[noteId].users[key];
         if (user)
             users.push(buildUserOutData(user));
     });
@@ -267,35 +267,20 @@ function emitOnlineUsers(socket) {
         users: users
     };
     out = LZString.compressToUTF16(JSON.stringify(out));
-    realtime.io.to(notename).emit('online users', out);
-    /*
-    for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
-        var sock = notes[notename].socks[i];
-        if (sock && out)
-            sock.emit('online users', out);
-    };
-    */
+    realtime.io.to(noteId).emit('online users', out);
 }
 
 function emitUserStatus(socket) {
-    var notename = getNotenameFromSocket(socket);
-    if (!notename || !notes[notename]) return;
+    var noteId = socket.noteId;
+    if (!noteId || !notes[noteId]) return;
     var out = buildUserOutData(users[socket.id]);
-    socket.broadcast.to(notename).emit('user status', out);
-    /*
-    for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
-        var sock = notes[notename].socks[i];
-        if (sock != socket) {
-            sock.emit('user status', out);
-        }
-    };
-    */
+    socket.broadcast.to(noteId).emit('user status', out);
 }
 
 function emitRefresh(socket) {
-    var notename = getNotenameFromSocket(socket);
-    if (!notename || !notes[notename]) return;
-    var note = notes[notename];
+    var noteId = socket.noteId;
+    if (!noteId || !notes[noteId]) return;
+    var note = notes[noteId];
     socket.emit('refresh', {
         docmaxlength: config.documentmaxlength,
         owner: note.owner,
@@ -326,15 +311,10 @@ function finishConnection(socket, note, user) {
     if (!socket || !note || !user) return;
     //check view permission
     if (note.permission == 'private') {
-        if (socket.request.user && socket.request.user.logged_in && socket.request.user._id == note.owner) {
+        if (socket.request.user && socket.request.user.logged_in && socket.request.user.id == note.owner) {
             //na
         } else {
-            socket.emit('info', {
-                code: 403
-            });
-            clearSocketQueue(connectionSocketQueue, socket);
-            isConnectionBusy = false;
-            return socket.disconnect(true);
+            return failConnection(403, 'connection forbidden', socket);
         }
     }
     note.users[socket.id] = user;
@@ -354,8 +334,8 @@ function finishConnection(socket, note, user) {
         startConnection(connectionSocketQueue[0]);
 
     if (config.debug) {
-        var notename = getNotenameFromSocket(socket);
-        logger.info('SERVER connected a client to [' + notename + ']:');
+        var noteId = socket.noteId;
+        logger.info('SERVER connected a client to [' + noteId + ']:');
         logger.info(JSON.stringify(user));
         //logger.info(notes);
         getStatus(function (data) {
@@ -367,117 +347,76 @@ function finishConnection(socket, note, user) {
 function startConnection(socket) {
     if (isConnectionBusy) return;
     isConnectionBusy = true;
+        
+    var noteId = socket.noteId;
+    if (!noteId) {
+        return failConnection(404, 'note id not found', socket);
+    }
 
-    var notename = getNotenameFromSocket(socket);
-    if (!notename) {
-		clearSocketQueue(connectionSocketQueue, socket);
-		isConnectionBusy = false;
-		return;
-	}
+    if (!notes[noteId]) {
+        var include = [{
+            model: models.User,
+            as: "owner"
+        }, {
+            model: models.User,
+            as: "lastchangeuser"
+        }];
 
-    if (!notes[notename]) {
-        db.readFromDB(notename, function (err, data) {
-            if (err) {
-                socket.emit('info', {
-                    code: 404
-                });
-                socket.disconnect(true);
-                //clear err socket in queue
-				clearSocketQueue(connectionSocketQueue, socket);
-                isConnectionBusy = false;
-                return logger.error(err);
+        models.Note.findOne({
+            where: {
+                id: noteId
+            },
+            include: include
+        }).then(function (note) {
+            if (!note) {
+                return failConnection(404, 'note not found', socket);
             }
+            var owner = note.ownerId;
+            var ownerprofile = note.owner ? models.User.parseProfile(note.owner.profile) : null;
 
-            var owner = data.rows[0].owner;
-            var ownerprofile = null;
+            var lastchangeuser = note.lastchangeuserId;
+            var lastchangeuserprofile = note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null;
 
-            //find or new note
-            Note.findOrNewNote(notename, owner, function (err, note) {
-                if (err) {
-                    socket.emit('info', {
-                        code: 404
-                    });
-                    socket.disconnect(true);
-					clearSocketQueue(connectionSocketQueue, socket);
-					isConnectionBusy = false;
-                    return logger.error(err);
-                }
+            var body = LZString.decompressFromBase64(note.content);
+            var createtime = note.createdAt;
+            var updatetime = note.lastchangeAt;
+            var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
 
-                var body = LZString.decompressFromBase64(data.rows[0].content);
-                //body = LZString.compressToUTF16(body);
-                var createtime = data.rows[0].create_time;
-                var updatetime = data.rows[0].update_time;
-                var server = new ot.EditorSocketIOServer(body, [], notename, ifMayEdit);
+            notes[noteId] = {
+                id: noteId,
+                owner: owner,
+                ownerprofile: ownerprofile,
+                permission: note.permission,
+                lastchangeuser: lastchangeuser,
+                lastchangeuserprofile: lastchangeuserprofile,
+                socks: [],
+                users: {},
+                createtime: moment(createtime).valueOf(),
+                updatetime: moment(updatetime).valueOf(),
+                server: server
+            };
 
-                var lastchangeuser = note.lastchangeuser || null;
-                var lastchangeuserprofile = null;
-
-                notes[notename] = {
-                    id: notename,
-                    owner: owner,
-                    ownerprofile: ownerprofile,
-                    permission: note.permission,
-                    lastchangeuser: lastchangeuser,
-                    lastchangeuserprofile: lastchangeuserprofile,
-                    socks: [],
-                    users: {},
-                    createtime: moment(createtime).valueOf(),
-                    updatetime: moment(updatetime).valueOf(),
-                    server: server
-                };
-                
-                async.parallel([
-                    function getlastchangeuser(callback) {
-                        if (lastchangeuser) {
-                            //find last change user profile if lastchangeuser exists
-                            User.findUser(lastchangeuser, function (err, user) {
-                                if (!err && user && user.profile) {
-                                    var profile = JSON.parse(user.profile);
-                                    if (profile) {
-                                        lastchangeuserprofile = {
-                                            name: profile.displayName || profile.username,
-                                            photo: User.parsePhotoByProfile(profile)
-                                        }
-                                        notes[notename].lastchangeuserprofile = lastchangeuserprofile;
-                                    }
-                                }
-                                callback(null, null);
-                            });
-                        } else {
-                            callback(null, null);
-                        }
-                    },
-                    function getowner(callback) {
-                        if (owner && owner != "null") {
-                            //find owner profile if owner exists
-                            User.findUser(owner, function (err, user) {
-                                if (!err && user && user.profile) {
-                                    var profile = JSON.parse(user.profile);
-                                    if (profile) {
-                                        ownerprofile = {
-                                            name: profile.displayName || profile.username,
-                                            photo: User.parsePhotoByProfile(profile)
-                                        }
-                                        notes[notename].ownerprofile = ownerprofile;
-                                    }
-                                }
-                                callback(null, null);
-                            });
-                        } else {
-                            callback(null, null);
-                        }
-                    }
-                ], function(err, results){
-                    if (err) return;
-                    finishConnection(socket, notes[notename], users[socket.id]);
-                });
-            });
+            return finishConnection(socket, notes[noteId], users[socket.id]);
+        }).catch(function (err) {
+            return failConnection(500, err, socket);
         });
     } else {
-        finishConnection(socket, notes[notename], users[socket.id]);
+        return finishConnection(socket, notes[noteId], users[socket.id]);
     }
 }
 
+function failConnection(code, err, socket) {
+    logger.error(err);
+    // clear error socket in queue
+    clearSocketQueue(connectionSocketQueue, socket);
+    isConnectionBusy = false;
+    // emit error info
+    socket.emit('info', {
+        code: code
+    });
+    return socket.disconnect(true);
+}
+
 function disconnect(socket) {
     if (isDisconnectBusy) return;
     isDisconnectBusy = true;
@@ -490,8 +429,8 @@ function disconnect(socket) {
     if (users[socket.id]) {
         delete users[socket.id];
     }
-	var notename = getNotenameFromSocket(socket);
-    var note = notes[notename];
+	var noteId = socket.noteId;
+    var note = notes[noteId];
     if (note) {
         delete note.users[socket.id];
         do {
@@ -502,22 +441,18 @@ function disconnect(socket) {
         } while (index != -1);
         if (Object.keys(note.users).length <= 0) {
             if (note.server.isDirty) {
-                var body = note.server.document;
-                var title = Note.getNoteTitle(body);
-                title = LZString.compressToBase64(title);
-                body = LZString.compressToBase64(body);
-                db.saveToDB(notename, title, body,
-                    function (err, result) {
-                        delete notes[notename];
-                        if (config.debug) {
-                            //logger.info(notes);
-                            getStatus(function (data) {
-                                logger.info(JSON.stringify(data));
-                            });
-                        }
-                    });
+                updateNote(note, function (err, _note) {
+                    if (err) return logger.error('disconnect note failed: ' + err);
+                    delete notes[noteId];
+                    if (config.debug) {
+                        //logger.info(notes);
+                        getStatus(function (data) {
+                            logger.info(JSON.stringify(data));
+                        });
+                    }
+                });
             } else {
-                delete notes[notename];
+                delete notes[noteId];
             }
         }
     }
@@ -556,10 +491,10 @@ function buildUserOutData(user) {
 function updateUserData(socket, user) {
     //retrieve user data from passport
     if (socket.request.user && socket.request.user.logged_in) {
-        var profile = JSON.parse(socket.request.user.profile);
-        user.photo = User.parsePhotoByProfile(profile);
-        user.name = profile.displayName || profile.username;
-        user.userid = socket.request.user._id;
+        var profile = models.User.parseProfile(socket.request.user.profile);
+        user.photo = profile.photo;
+        user.name = profile.name;
+        user.userid = socket.request.user.id;
         user.login = true;
     } else {
         user.userid = null;
@@ -569,9 +504,9 @@ function updateUserData(socket, user) {
 }
 
 function ifMayEdit(socket, callback) {
-    var notename = getNotenameFromSocket(socket);
-    if (!notename || !notes[notename]) return;
-    var note = notes[notename];
+    var noteId = socket.noteId;
+    if (!noteId || !notes[noteId]) return;
+    var note = notes[noteId];
     var mayEdit = true;
     switch (note.permission) {
         case "freely":
@@ -584,69 +519,78 @@ function ifMayEdit(socket, callback) {
             break;
         case "locked": case "private":
             //only owner can change
-            if (note.owner != socket.request.user._id)
+            if (note.owner != socket.request.user.id)
                 mayEdit = false;
             break;
     }
     //if user may edit and this note have owner (not anonymous usage)
-    if (socket.origin == 'operation' && mayEdit && note.owner && note.owner != "null") {
+    if (socket.origin == 'operation' && mayEdit && note.owner) {
         //save for the last change user id
 		if (socket.request.user && socket.request.user.logged_in) {
-        	note.lastchangeuser = socket.request.user._id;
+        	note.lastchangeuser = socket.request.user.id;
 		} else {
 			note.lastchangeuser = null;
 		}
     }
-    callback(mayEdit);
+    return callback(mayEdit);
 }
 
 function connection(socket) {
-    //split notename from socket
-    var notename = getNotenameFromSocket(socket);
-
-    //initialize user data
-    //random color
-    var color = randomcolor({
-        luminosity: 'light'
-    });
-    //make sure color not duplicated or reach max random count
-    if (notename && notes[notename]) {
-        var randomcount = 0;
-        var maxrandomcount = 5;
-        var found = false;
-        do {
-            Object.keys(notes[notename].users).forEach(function (user) {
-                if (user.color == color) {
-                    found = true;
-                    return;
-                }
-            });
-            if (found) {
-                color = randomcolor({
-                    luminosity: 'light'
+    parseNoteIdFromSocket(socket, function (err, noteId) {
+        if (err) {
+            return failConnection(500, err, socket);
+        }
+        if (!noteId) {
+            return failConnection(404, 'note id not found', socket);
+        }
+        
+        // store noteId in this socket session
+        socket.noteId = noteId;
+        
+        //initialize user data
+        //random color
+        var color = randomcolor({
+            luminosity: 'light'
+        });
+        //make sure color not duplicated or reach max random count
+        if (notes[noteId]) {
+            var randomcount = 0;
+            var maxrandomcount = 5;
+            var found = false;
+            do {
+                Object.keys(notes[noteId].users).forEach(function (user) {
+                    if (user.color == color) {
+                        found = true;
+                        return;
+                    }
                 });
-                randomcount++;
-            }
-        } while (found && randomcount < maxrandomcount);
-    }
-    //create user data
-    users[socket.id] = {
-        id: socket.id,
-        address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
-        'user-agent': socket.handshake.headers['user-agent'],
-        color: color,
-        cursor: null,
-        login: false,
-        userid: null,
-        name: null,
-        idle: false,
-        type: null
-    };
-    updateUserData(socket, users[socket.id]);
+                if (found) {
+                    color = randomcolor({
+                        luminosity: 'light'
+                    });
+                    randomcount++;
+                }
+            } while (found && randomcount < maxrandomcount);
+        }
+        //create user data
+        users[socket.id] = {
+            id: socket.id,
+            address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
+            'user-agent': socket.handshake.headers['user-agent'],
+            color: color,
+            cursor: null,
+            login: false,
+            userid: null,
+            name: null,
+            idle: false,
+            type: null
+        };
+        updateUserData(socket, users[socket.id]);
 
-    //start connection
-    connectionSocketQueue.push(socket);
-    startConnection(socket);
+        //start connection
+        connectionSocketQueue.push(socket);
+        startConnection(socket);
+    });
 
     //received client refresh request
     socket.on('refresh', function () {
@@ -655,10 +599,10 @@ function connection(socket) {
 
     //received user status
     socket.on('user status', function (data) {
-        var notename = getNotenameFromSocket(socket);
-        if (!notename || !notes[notename]) return;
+        var noteId = socket.noteId;
+        if (!noteId || !notes[noteId]) return;
         if (config.debug)
-            logger.info('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
+            logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
         if (data) {
             var user = users[socket.id];
             user.idle = data.idle;
@@ -671,41 +615,44 @@ function connection(socket) {
     socket.on('permission', function (permission) {
         //need login to do more actions
         if (socket.request.user && socket.request.user.logged_in) {
-            var notename = getNotenameFromSocket(socket);
-            if (!notename || !notes[notename]) return;
-            var note = notes[notename];
+            var noteId = socket.noteId;
+            if (!noteId || !notes[noteId]) return;
+            var note = notes[noteId];
             //Only owner can change permission
-            if (note.owner == socket.request.user._id) {
+            if (note.owner == socket.request.user.id) {
                 note.permission = permission;
-                Note.findNote(notename, function (err, _note) {
-                    if (err || !_note) {
+                models.Note.update({
+                    permission: permission
+                }, {
+                    where: {
+                        id: noteId
+                    }
+                }).then(function (count) {
+                    if (!count) {
                         return;
                     }
-                    Note.updatePermission(_note, permission, function (err, _note) {
-                        if (err || !_note) {
-                            return;
-                        }
-                        var out = {
-                            permission: permission
-                        };
-                        realtime.io.to(note.id).emit('permission', out);
-                        for (var i = 0, l = note.socks.length; i < l; i++) {
-                            var sock = note.socks[i];
-                            if (typeof sock !== 'undefined' && sock) {
-                                //check view permission
-                                if (permission == 'private') {
-                                    if (sock.request.user && sock.request.user.logged_in && sock.request.user._id == note.owner) {
-                                        //na
-                                    } else {
-                                        sock.emit('info', {
-                                            code: 403
-                                        });
-                                        return sock.disconnect(true);
-                                    }
+                    var out = {
+                        permission: permission
+                    };
+                    realtime.io.to(note.id).emit('permission', out);
+                    for (var i = 0, l = note.socks.length; i < l; i++) {
+                        var sock = note.socks[i];
+                        if (typeof sock !== 'undefined' && sock) {
+                            //check view permission
+                            if (permission == 'private') {
+                                if (sock.request.user && sock.request.user.logged_in && sock.request.user.id == note.owner) {
+                                    //na
+                                } else {
+                                    sock.emit('info', {
+                                        code: 403
+                                    });
+                                    return sock.disconnect(true);
                                 }
                             }
                         }
-                    });
+                    }
+                }).catch(function (err) {
+                    return logger.error('update note permission failed: ' + err);
                 });
             }
         }
@@ -714,19 +661,19 @@ function connection(socket) {
     //reveiced when user logout or changed
     socket.on('user changed', function () {
         logger.info('user changed');
-        var notename = getNotenameFromSocket(socket);
-        if (!notename || !notes[notename]) return;
-        updateUserData(socket, notes[notename].users[socket.id]);
+        var noteId = socket.noteId;
+        if (!noteId || !notes[noteId]) return;
+        updateUserData(socket, notes[noteId].users[socket.id]);
         emitOnlineUsers(socket);
     });
 
     //received sync of online users request
     socket.on('online users', function () {
-        var notename = getNotenameFromSocket(socket);
-        if (!notename || !notes[notename]) return;
+        var noteId = socket.noteId;
+        if (!noteId || !notes[noteId]) return;
         var users = [];
-        Object.keys(notes[notename].users).forEach(function (key) {
-            var user = notes[notename].users[key];
+        Object.keys(notes[noteId].users).forEach(function (key) {
+            var user = notes[noteId].users[key];
             if (user)
                 users.push(buildUserOutData(user));
         });
@@ -744,55 +691,31 @@ function connection(socket) {
 
     //received cursor focus
     socket.on('cursor focus', function (data) {
-        var notename = getNotenameFromSocket(socket);
-        if (!notename || !notes[notename]) return;
+        var noteId = socket.noteId;
+        if (!noteId || !notes[noteId]) return;
         users[socket.id].cursor = data;
         var out = buildUserOutData(users[socket.id]);
-        socket.broadcast.to(notename).emit('cursor focus', out);
-        /*
-        for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
-            var sock = notes[notename].socks[i];
-            if (sock != socket) {
-                sock.emit('cursor focus', out);
-            }
-        };
-        */
+        socket.broadcast.to(noteId).emit('cursor focus', out);
     });
 
     //received cursor activity
     socket.on('cursor activity', function (data) {
-        var notename = getNotenameFromSocket(socket);
-        if (!notename || !notes[notename]) return;
+        var noteId = socket.noteId;
+        if (!noteId || !notes[noteId]) return;
         users[socket.id].cursor = data;
         var out = buildUserOutData(users[socket.id]);
-        socket.broadcast.to(notename).emit('cursor activity', out);
-        /*
-        for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
-            var sock = notes[notename].socks[i];
-            if (sock != socket) {
-                sock.emit('cursor activity', out);
-            }
-        };
-        */
+        socket.broadcast.to(noteId).emit('cursor activity', out);
     });
 
     //received cursor blur
     socket.on('cursor blur', function () {
-        var notename = getNotenameFromSocket(socket);
-        if (!notename || !notes[notename]) return;
+        var noteId = socket.noteId;
+        if (!noteId || !notes[noteId]) return;
         users[socket.id].cursor = null;
         var out = {
             id: socket.id
         };
-        socket.broadcast.to(notename).emit('cursor blur', out);
-        /*
-        for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
-            var sock = notes[notename].socks[i];
-            if (sock != socket) {
-                sock.emit('cursor blur', out);
-            }
-        };
-        */
+        socket.broadcast.to(noteId).emit('cursor blur', out);
     });
 
     //when a new client disconnect
diff --git a/lib/response.js b/lib/response.js
index 4e5fac3..7a75e23 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -3,7 +3,6 @@
 var ejs = require('ejs');
 var fs = require('fs');
 var path = require('path');
-var uuid = require('node-uuid');
 var markdownpdf = require("markdown-pdf");
 var LZString = require('lz-string');
 var S = require('string');
@@ -13,12 +12,9 @@ var querystring = require('querystring');
 var request = require('request');
 
 //core
-var config = require("../config.js");
-
-//others
-var db = require("./db.js");
-var Note = require("./note.js");
-var User = require("./user.js");
+var config = require("./config.js");
+var logger = require("./logger.js");
+var models = require("./models");
 
 //slides
 var md = require('reveal.js/plugin/markdown/markdown');
@@ -26,10 +22,7 @@ var Mustache = require('mustache');
 
 //reveal.js
 var opts = {
-    userBasePath: process.cwd(),
-    revealBasePath: path.resolve(require.resolve('reveal.js'), '..', '..'),
-    template: fs.readFileSync(path.join('.', '/public/views/slide', 'reveal.hbs')).toString(),
-    templateListing: fs.readFileSync(path.join('.', '/public/views/slide', 'listing.hbs')).toString(),
+    template: fs.readFileSync(config.slidepath).toString(),
     theme: 'css/theme/black.css',
     highlightTheme: 'zenburn',
     separator: '^(\r\n?|\n)---(\r\n?|\n)$',
@@ -52,7 +45,6 @@ var response = {
         res.status(503).send("I'm busy right now, try again later.");
     },
     newNote: newNote,
-    showFeatures: showFeatures,
     showNote: showNote,
     showPublishNote: showPublishNote,
     showPublishSlide: showPublishSlide,
@@ -67,8 +59,13 @@ function responseError(res, code, detail, msg) {
         'Content-Type': 'text/html'
     });
     var template = config.errorpath;
-    var content = ejs.render(fs.readFileSync(template, 'utf8'), {
-        url: config.getserverurl(),
+    var options = {
+        cache: !config.debug,
+        filename: template
+    };
+    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
+    var content = compiled({
+        url: config.serverurl,
         title: code + ' ' + detail + ' ' + msg,
         cache: !config.debug,
         filename: template,
@@ -86,193 +83,163 @@ function showIndex(req, res, next) {
         'Content-Type': 'text/html'
     });
     var template = config.indexpath;
-    var content = ejs.render(fs.readFileSync(template, 'utf8'), {
-        url: config.getserverurl(),
-        useCDN: config.usecdn
+    var options = {
+        cache: !config.debug,
+        filename: template
+    };
+    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
+    var content = compiled({
+        url: config.serverurl,
+        useCDN: config.usecdn,
+        facebook: config.facebook,
+        twitter: config.twitter,
+        github: config.github,
+        dropbox: config.dropbox,
     });
     res.write(content);
     res.end();
 }
 
-function responseHackMD(res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var notedata = data.rows[0];
-        var body = LZString.decompressFromBase64(notedata.content);
-        var meta = null;
-        try {
-            meta = metaMarked(body).meta;
-        } catch(err) {
-            //na
-        }
-        var title = Note.decodeTitle(notedata.title);
-        title = Note.generateWebTitle(title);
-        var template = config.hackmdpath;
-        var options = {
-            cache: !config.debug,
-            filename: template
-        };
-        var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
-        var html = compiled({
-            url: config.getserverurl(),
-            title: title,
-			useCDN: config.usecdn,
-            robots: (meta && meta.robots) || false //default allow robots
-        });
-        var buf = html;
-        res.writeHead(200, {
-            'Content-Type': 'text/html; charset=UTF-8',
-            'Cache-Control': 'private',
-            'Content-Length': buf.length
-        });
-        res.end(buf);
+function responseHackMD(res, note) {
+    var body = LZString.decompressFromBase64(note.content);
+    var meta = null;
+    try {
+        meta = metaMarked(body).meta;
+    } catch(err) {
+        //na
+    }
+    var title = models.Note.decodeTitle(note.title);
+    title = models.Note.generateWebTitle(title);
+    var template = config.hackmdpath;
+    var options = {
+        cache: !config.debug,
+        filename: template
+    };
+    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
+    var html = compiled({
+        url: config.serverurl,
+        title: title,
+        useCDN: config.usecdn,
+        robots: (meta && meta.robots) || false, //default allow robots
+        facebook: config.facebook,
+        twitter: config.twitter,
+        github: config.github,
+        dropbox: config.dropbox,
     });
+    var buf = html;
+    res.writeHead(200, {
+        'Content-Type': 'text/html; charset=UTF-8',
+        'Cache-Control': 'private',
+        'Content-Length': buf.length
+    });
+    res.end(buf);
 }
 
 function newNote(req, res, next) {
-    var newId = uuid.v4();
-    var body = fs.readFileSync(config.defaultnotepath, 'utf8');
-    body = LZString.compressToBase64(body);
     var owner = null;
     if (req.isAuthenticated()) {
-        owner = req.user._id;
+        owner = req.user.id;
     }
-    db.newToDB(newId, owner, body, function (err, result) {
-        if (err) {
-            return response.errorInternalError(res);
-        }
-        Note.newNote(newId, owner, function(err, result) {
-            if (err) {
-                return response.errorInternalError(res);
-            }
-            res.redirect(config.getserverurl() + "/" + LZString.compressToBase64(newId));
-        });
+    models.Note.create({
+        ownerId: owner
+    }).then(function (note) {
+        return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id));
+    }).catch(function (err) {
+        logger.error(err);
+        return response.errorInternalError(res);
     });
 }
 
-function showFeatures(req, res, next) {
-    db.readFromDB(config.featuresnotename, function (err, data) {
-        if (err) {
-            var body = fs.readFileSync(config.defaultfeaturespath, 'utf8');
-            body = LZString.compressToBase64(body);
-            db.newToDB(config.featuresnotename, null, body, function (err, result) {
-                if (err) {
-                    return response.errorInternalError(res);
-                }
-                responseHackMD(res, config.featuresnotename);
-            });
-        } else {
-            responseHackMD(res, config.featuresnotename);
-        }
+function checkViewPermission(req, note) {
+    if (note.permission == 'private') {
+        if (!req.isAuthenticated() || note.ownerId != req.user.id)
+            return false;
+        else
+            return true;
+    } else {
+        return true;
+    }
+}
+
+function findNote(req, res, callback, include) {
+    var id = req.params.noteId || req.params.shortid;
+    models.Note.parseNoteId(id, function (err, _id) {
+        models.Note.findOne({
+            where: {
+                id: _id
+            },
+            include: include || null
+        }).then(function (note) {
+            if (!note) {
+                return response.errorNotFound(res);
+            }
+            if (!checkViewPermission(req, note)) {
+                return response.errorForbidden(res);
+            } else {
+                return callback(note);
+            }
+        }).catch(function (err) {
+            logger.error(err);
+            return response.errorInternalError(res);
+        });
     });
 }
 
 function showNote(req, res, next) {
-    var noteId = req.params.noteId;
-    if (noteId != config.featuresnotename) {
-        if (!Note.checkNoteIdValid(noteId)) {
-            return response.errorNotFound(res);
-        }
-        noteId = LZString.decompressFromBase64(noteId);
-        if (!noteId) {
-            return response.errorNotFound(res);
-        }
-    }
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var notedata = data.rows[0];
-        Note.findOrNewNote(noteId, notedata.owner, function (err, note) {
-            if (err || !note) {
-                return response.errorNotFound(res);
-            }
-            //check view permission
-            if (note.permission == 'private') {
-                if (!req.isAuthenticated() || notedata.owner != req.user._id)
-                    return response.errorForbidden(res);
-            }
-            responseHackMD(res, noteId);
-        });
+    findNote(req, res, function (note) {
+        return responseHackMD(res, note);
     });
 }
 
 function showPublishNote(req, res, next) {
-    var shortid = req.params.shortid;
-    if (shortId.isValid(shortid)) {
-        Note.findNote(shortid, function (err, note) {
-            if (err || !note) {
+    var include = [{
+        model: models.User,
+        as: "owner"
+    }, {
+        model: models.User,
+        as: "lastchangeuser"
+    }];
+    findNote(req, res, function (note) {
+        note.increment('viewcount').then(function (note) {
+            if (!note) {
                 return response.errorNotFound(res);
             }
-            db.readFromDB(note.id, function (err, data) {
-                if (err) {
-                    return response.errorNotFound(res);
-                }
-                var notedata = data.rows[0];
-                //check view permission
-                if (note.permission == 'private') {
-                    if (!req.isAuthenticated() || notedata.owner != req.user._id)
-                        return response.errorForbidden(res);
-                }
-                //increase note viewcount
-                Note.increaseViewCount(note, function (err, note) {
-                    if (err || !note) {
-                        return response.errorNotFound(res);
-                    }
-                    var body = LZString.decompressFromBase64(notedata.content);
-                    var meta = null;
-                    try {
-                        meta = metaMarked(body).meta;
-                    } catch(err) {
-                        //na
-                    }
-                    var updatetime = notedata.update_time;
-                    var text = S(body).escapeHTML().s;
-                    var title = Note.decodeTitle(notedata.title);
-                    title = Note.generateWebTitle(title);
-                    var origin = config.getserverurl();
-                    var data = {
-                        title: title,
-                        viewcount: note.viewcount,
-                        updatetime: updatetime,
-                        url: origin,
-                        body: text,
-                        useCDN: config.usecdn,
-                        lastchangeuserprofile: null,
-                        robots: (meta && meta.robots) || false //default allow robots
-                    };
-                    if (note.lastchangeuser) {
-                        //find last change user profile if lastchangeuser exists
-                        User.findUser(note.lastchangeuser, function (err, user) {
-                            if (!err && user && user.profile) {
-                                var profile = JSON.parse(user.profile);
-                                if (profile) {
-                                    data.lastchangeuserprofile = {
-                                        name: profile.displayName || profile.username,
-                                        photo: User.parsePhotoByProfile(profile)
-                                    }
-                                    renderPublish(data, res);
-                                }
-                            }
-                        });
-                    } else {
-                        renderPublish(data, res);
-                    }
-                    
-                });
-            });
+            var body = LZString.decompressFromBase64(note.content);
+            var meta = null;
+            try {
+                meta = metaMarked(body).meta;
+            } catch(err) {
+                //na
+            }
+            var createtime = note.createdAt;
+            var updatetime = note.lastchangeAt;
+            var text = S(body).escapeHTML().s;
+            var title = models.Note.decodeTitle(note.title);
+            title = models.Note.generateWebTitle(title);
+            var origin = config.serverurl;
+            var data = {
+                title: title,
+                viewcount: note.viewcount,
+                createtime: createtime,
+                updatetime: updatetime,
+                url: origin,
+                body: text,
+                useCDN: config.usecdn,
+                lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
+                robots: (meta && meta.robots) || false //default allow robots
+            };
+            return renderPublish(data, res);
+        }).catch(function (err) {
+            logger.error(err);
+            return response.errorInternalError(res);
         });
-    } else {
-        return response.errorNotFound(res);
-    }
+    }, include);
 }
 
 function renderPublish(data, res) {
     var template = config.prettypath;
     var options = {
-        url: config.getserverurl(),
+        url: config.serverurl,
         cache: !config.debug,
         filename: template
     };
@@ -287,343 +254,206 @@ function renderPublish(data, res) {
     res.end(buf);
 }
 
-function actionPublish(req, res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var owner = data.rows[0].owner;
-        Note.findOrNewNote(noteId, owner, function (err, note) {
-            if (err) {
-                return response.errorNotFound(res);
-            }
-            res.redirect(config.getserverurl() + "/s/" + note.shortid);
-        });
-    });
+function actionPublish(req, res, note) {
+    res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid));
 }
 
-function actionSlide(req, res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var owner = data.rows[0].owner;
-        Note.findOrNewNote(noteId, owner, function (err, note) {
-            if (err) {
-                return response.errorNotFound(res);
-            }
-            res.redirect(config.getserverurl() + "/p/" + note.shortid);
-        });
-    });
+function actionSlide(req, res, note) {
+    res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid));
 }
 
-function actionDownload(req, res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var notedata = data.rows[0];
-        var body = LZString.decompressFromBase64(notedata.content);
-        var title = Note.decodeTitle(notedata.title);
+function actionDownload(req, res, note) {
+    var body = LZString.decompressFromBase64(note.content);
+    var title = models.Note.decodeTitle(note.title);
+    var filename = title;
+    filename = encodeURIComponent(filename);
+    res.writeHead(200, {
+        'Access-Control-Allow-Origin': '*', //allow CORS as API
+        'Access-Control-Allow-Headers': 'Range',
+        'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
+        'Content-Type': 'text/markdown; charset=UTF-8',
+        'Cache-Control': 'private',
+        'Content-disposition': 'attachment; filename=' + filename + '.md',
+        'Content-Length': body.length,
+        'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
+    });
+    res.end(body);
+}
+
+function actionPDF(req, res, note) {
+    var body = LZString.decompressFromBase64(note.content);
+    try {
+        body = metaMarked(body).markdown;
+    } catch(err) {
+        //na
+    }
+    var title = models.Note.decodeTitle(note.title);
+
+    if (!fs.existsSync(config.tmppath)) {
+        fs.mkdirSync(config.tmppath);
+    }
+    var path = config.tmppath + Date.now() + '.pdf';
+    markdownpdf().from.string(body).to(path, function () {
+        var stream = fs.createReadStream(path);
         var filename = title;
+        // Be careful of special characters
         filename = encodeURIComponent(filename);
-        res.writeHead(200, {
-            'Access-Control-Allow-Origin': '*', //allow CORS as API
-            'Access-Control-Allow-Headers': 'Range',
-            'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
-            'Content-Type': 'text/markdown; charset=UTF-8',
-            'Cache-Control': 'private',
-            'Content-disposition': 'attachment; filename=' + filename + '.md',
-            'Content-Length': body.length,
-            'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
-        });
-        res.end(body);
+        // Ideally this should strip them
+        res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
+        res.setHeader('Cache-Control', 'private');
+        res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
+        res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
+        stream.pipe(res);
+        fs.unlink(path);
     });
 }
 
-function actionPDF(req, res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var notedata = data.rows[0];
-        var body = LZString.decompressFromBase64(notedata.content);
-        try {
-            body = metaMarked(body).markdown;
-        } catch(err) {
-            //na
-        }
-        var title = Note.decodeTitle(notedata.title);
-
-        if (!fs.existsSync(config.tmppath)) {
-            fs.mkdirSync(config.tmppath);
-        }
-        var path = config.tmppath + Date.now() + '.pdf';
-        markdownpdf().from.string(body).to(path, function () {
-            var stream = fs.createReadStream(path);
-            var filename = title;
-            // Be careful of special characters
-            filename = encodeURIComponent(filename);
-            // Ideally this should strip them
-            res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
-            res.setHeader('Cache-Control', 'private');
-            res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
-            res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
-            stream.pipe(res);
-            fs.unlink(path);
-        });
-    });
-}
-
-function actionGist(req, res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
-        }
-        var owner = data.rows[0].owner;
-        Note.findOrNewNote(noteId, owner, function (err, note) {
-            if (err) {
-                return response.errorNotFound(res);
-            }
-            var data = {
-                client_id: config.github.clientID,
-                redirect_uri: config.getserverurl() + '/auth/github/callback/' + LZString.compressToBase64(noteId) + '/gist',
-                scope: "gist",
-                state: shortId.generate()
-            };
-            var query = querystring.stringify(data);
-            res.redirect("https://github.com/login/oauth/authorize?" + query);
-        });
-    });
+function actionGist(req, res, note) {
+    var data = {
+        client_id: config.github.clientID,
+        redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist',
+        scope: "gist",
+        state: shortId.generate()
+    };
+    var query = querystring.stringify(data);
+    res.redirect("https://github.com/login/oauth/authorize?" + query);
 }
 
 function noteActions(req, res, next) {
     var noteId = req.params.noteId;
-    if (noteId != config.featuresnotename) {
-        if (!Note.checkNoteIdValid(noteId)) {
-            return response.errorNotFound(res);
+    findNote(req, res, function (note) {
+        var action = req.params.action;
+        switch (action) {
+        case "publish":
+        case "pretty": //pretty deprecated
+            actionPublish(req, res, note);
+            break;
+        case "slide":
+            actionSlide(req, res, note);
+            break;
+        case "download":
+            actionDownload(req, res, note);
+            break;
+        case "pdf":
+            actionPDF(req, res, note);
+            break;
+        case "gist":
+            actionGist(req, res, note);
+            break;
+        default:
+            return res.redirect(config.serverurl + '/' + noteId);
+            break;
         }
-        noteId = LZString.decompressFromBase64(noteId);
-        if (!noteId) {
-            return response.errorNotFound(res);
-        }
-    }
-    Note.findNote(noteId, function (err, note) {
-        if (err || !note) {
-            return response.errorNotFound(res);
-        }
-        db.readFromDB(note.id, function (err, data) {
-            if (err) {
-                return response.errorNotFound(res);
-            }
-            var notedata = data.rows[0];
-            //check view permission
-            if (note.permission == 'private') {
-                if (!req.isAuthenticated() || notedata.owner != req.user._id)
-                    return response.errorForbidden(res);
-            }
-            var action = req.params.action;
-            switch (action) {
-            case "publish":
-            case "pretty": //pretty deprecated
-                actionPublish(req, res, noteId);
-                break;
-            case "slide":
-                actionSlide(req, res, noteId);
-                break;
-            case "download":
-                actionDownload(req, res, noteId);
-                break;
-            case "pdf":
-                actionPDF(req, res, noteId);
-                break;
-            case "gist":
-                actionGist(req, res, noteId);
-                break;
-            default:
-                if (noteId != config.featuresnotename)
-                    res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
-                else
-                    res.redirect(config.getserverurl() + '/' + noteId);
-                break;
-            }
-        });
     });
 }
 
 function publishNoteActions(req, res, next) {
-    var shortid = req.params.shortid;
-    if (shortId.isValid(shortid)) {
-        Note.findNote(shortid, function (err, note) {
-            if (err || !note) {
-                return response.errorNotFound(res);
-            }
-            db.readFromDB(note.id, function (err, data) {
-                if (err) {
-                    return response.errorNotFound(res);
-                }
-                var notedata = data.rows[0];
-                //check view permission
-                if (note.permission == 'private') {
-                    if (!req.isAuthenticated() || notedata.owner != req.user._id)
-                        return response.errorForbidden(res);
-                }
-                var action = req.params.action;
-                switch (action) {
-                case "edit":
-                    if (note.id != config.featuresnotename)
-                        res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(note.id));
-                    else
-                        res.redirect(config.getserverurl() + '/' + note.id);
-                    break;
-                }
-            });
-        });
-    }
+    findNote(req, res, function (note) {
+        var action = req.params.action;
+        switch (action) {
+        case "edit":
+            res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
+            break;
+        default:
+            res.redirect(config.serverurl + '/s/' + note.shortid);    
+            break;
+        }
+    });
 }
 
 function githubActions(req, res, next) {
     var noteId = req.params.noteId;
-    if (noteId != config.featuresnotename) {
-        if (!Note.checkNoteIdValid(noteId)) {
-            return response.errorNotFound(res);
+    findNote(req, res, function (note) {
+        var action = req.params.action;
+        switch (action) {
+        case "gist":
+            githubActionGist(req, res, note);
+            break;
+        default:
+            res.redirect(config.serverurl + '/' + noteId);    
+            break;
         }
-        noteId = LZString.decompressFromBase64(noteId);
-        if (!noteId) {
-            return response.errorNotFound(res);
-        }
-    }
-    Note.findNote(noteId, function (err, note) {
-        if (err || !note) {
-            return response.errorNotFound(res);
-        }
-        db.readFromDB(note.id, function (err, data) {
-            if (err) {
-                return response.errorNotFound(res);
-            }
-            var notedata = data.rows[0];
-            //check view permission
-            if (note.permission == 'private') {
-                if (!req.isAuthenticated() || notedata.owner != req.user._id)
-                    return response.errorForbidden(res);
-            }
-            var action = req.params.action;
-            switch (action) {
-            case "gist":
-                githubActionGist(req, res, noteId);
-                break;
-            default:
-                if (noteId != config.featuresnotename)
-                    res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
-                else
-                    res.redirect(config.getserverurl() + '/' + noteId);
-                break;
-            }
-        });
     });
 }
 
-function githubActionGist(req, res, noteId) {
-    db.readFromDB(noteId, function (err, data) {
-        if (err) {
-            return response.errorNotFound(res);
+function githubActionGist(req, res, note) {
+    var code = req.query.code;
+    var state = req.query.state;
+    if (!code || !state) {
+        return response.errorForbidden(res);
+    } else {
+        var data = {
+            client_id: config.github.clientID,
+            client_secret: config.github.clientSecret,
+            code: code,
+            state: state
         }
-        var notedata = data.rows[0];
-        var code = req.query.code;
-        var state = req.query.state;
-        if (!code || !state) {
-            return response.errorForbidden(res);
-        } else {
-            var data = {
-                client_id: config.github.clientID,
-                client_secret: config.github.clientSecret,
-                code: code,
-                state: state
-            }
-            var auth_url = 'https://github.com/login/oauth/access_token';
-            request({
-                    url: auth_url,
-                    method: "POST",
-                    json: data
-                }, function (error, httpResponse, body) {
-                if (!error && httpResponse.statusCode == 200) {
-                    var access_token = body.access_token;
-                    if (access_token) {
-                        var content = LZString.decompressFromBase64(notedata.content);
-                        var title = Note.decodeTitle(notedata.title);
-                        var filename = title.replace('/', ' ') + '.md';
-                        var gist = {
-                            "files": {}
-                        };
-                        gist.files[filename] = {
-                            "content": content
-                        };
-                        var gist_url = "https://api.github.com/gists";
-                        request({
-                            url: gist_url,
-                            headers: {
-                                'User-Agent': 'HackMD',
-                                'Authorization': 'token ' + access_token
-                            },
-                            method: "POST",
-                            json: gist
-                        }, function (error, httpResponse, body) {
-                            if (!error && httpResponse.statusCode == 201) {
-                                res.setHeader('referer', '');
-                                res.redirect(body.html_url);
-                            } else {
-                                return response.errorForbidden(res);
-                            }
-                        });
-                    } else {
-                        return response.errorForbidden(res);
-                    }
+        var auth_url = 'https://github.com/login/oauth/access_token';
+        request({
+                url: auth_url,
+                method: "POST",
+                json: data
+            }, function (error, httpResponse, body) {
+            if (!error && httpResponse.statusCode == 200) {
+                var access_token = body.access_token;
+                if (access_token) {
+                    var content = LZString.decompressFromBase64(note.content);
+                    var title = models.Note.decodeTitle(note.title);
+                    var filename = title.replace('/', ' ') + '.md';
+                    var gist = {
+                        "files": {}
+                    };
+                    gist.files[filename] = {
+                        "content": content
+                    };
+                    var gist_url = "https://api.github.com/gists";
+                    request({
+                        url: gist_url,
+                        headers: {
+                            'User-Agent': 'HackMD',
+                            'Authorization': 'token ' + access_token
+                        },
+                        method: "POST",
+                        json: gist
+                    }, function (error, httpResponse, body) {
+                        if (!error && httpResponse.statusCode == 201) {
+                            res.setHeader('referer', '');
+                            res.redirect(body.html_url);
+                        } else {
+                            return response.errorForbidden(res);
+                        }
+                    });
                 } else {
                     return response.errorForbidden(res);
                 }
-            })
-        }
-    });
+            } else {
+                return response.errorForbidden(res);
+            }
+        })
+    }
 }
 
 function showPublishSlide(req, res, next) {
-    var shortid = req.params.shortid;
-    if (shortId.isValid(shortid)) {
-        Note.findNote(shortid, function (err, note) {
-            if (err || !note) {
+    findNote(req, res, function (note) {
+        note.increment('viewcount').then(function (note) {
+            if (!note) {
                 return response.errorNotFound(res);
             }
-            db.readFromDB(note.id, function (err, data) {
-                if (err) {
-                    return response.errorNotFound(res);
-                }
-                var notedata = data.rows[0];
-                //check view permission
-                if (note.permission == 'private') {
-                    if (!req.isAuthenticated() || notedata.owner != req.user._id)
-                        return response.errorForbidden(res);
-                }
-                //increase note viewcount
-                Note.increaseViewCount(note, function (err, note) {
-                    if (err || !note) {
-                        return response.errorNotFound(res);
-                    }
-                    var body = LZString.decompressFromBase64(notedata.content);
-                    try {
-                        body = metaMarked(body).markdown;
-                    } catch(err) {
-                        //na
-                    }
-                    var title = Note.decodeTitle(notedata.title);
-                    title = Note.generateWebTitle(title);
-                    var text = S(body).escapeHTML().s;
-                    render(res, title, text);
-                });
-            });
+            var body = LZString.decompressFromBase64(note.content);
+            try {
+                body = metaMarked(body).markdown;
+            } catch(err) {
+                //na
+            }
+            var title = models.Note.decodeTitle(note.title);
+            title = models.Note.generateWebTitle(title);
+            var text = S(body).escapeHTML().s;
+            render(res, title, text);
+        }).catch(function (err) {
+            logger.error(err);
+            return response.errorInternalError(res);
         });
-    } else {
-        return response.errorNotFound(res);
-    }
+    });
 }
 
 //reveal.js render
@@ -631,7 +461,7 @@ var render = function (res, title, markdown) {
     var slides = md.slidify(markdown, opts);
 
     res.end(Mustache.to_html(opts.template, {
-        url: config.getserverurl(),
+        url: config.serverurl,
         title: title,
         theme: opts.theme,
         highlightTheme: opts.highlightTheme,
diff --git a/lib/temp.js b/lib/temp.js
deleted file mode 100644
index b635644..0000000
--- a/lib/temp.js
+++ /dev/null
@@ -1,84 +0,0 @@
-//temp
-//external modules
-var mongoose = require('mongoose');
-
-//core
-var config = require("../config.js");
-var logger = require("./logger.js");
-
-// create a temp model
-var model = mongoose.model('temp', {
-    id: String,
-    data: String,
-    created: Date
-});
-
-//public
-var temp = {
-    model: model,
-    findTemp: findTemp,
-    newTemp: newTemp,
-    removeTemp: removeTemp,
-    getTempCount: getTempCount
-};
-
-function getTempCount(callback) {
-    model.count(function(err, count){
-        if(err) callback(err, null);
-        else callback(null, count);
-    });
-}
-
-function findTemp(id, callback) {
-    model.findOne({
-        id: id
-    }, function (err, temp) {
-        if (err) {
-            logger.error('find temp failed: ' + err);
-            callback(err, null);
-        }
-        if (!err && temp) {
-            callback(null, temp);
-        } else {
-            logger.error('find temp failed: ' + err);
-            callback(err, null);
-        };
-    });
-}
-
-function newTemp(id, data, callback) {
-    var temp = new model({
-        id: id,
-        data: data,
-        created: Date.now()
-    });
-    temp.save(function (err) {
-        if (err) {
-            logger.error('new temp failed: ' + err);
-            callback(err, null);
-        } else {
-            logger.info("new temp success: " + temp.id);
-            callback(null, temp);
-        };
-    });
-}
-
-function removeTemp(id, callback) {
-    findTemp(id, function(err, temp) {
-        if(!err && temp) {
-            temp.remove(function(err) {
-                if(err) {
-                    logger.error('remove temp failed: ' + err);
-                    callback(err, null);
-                } else {
-                    callback(null, null);
-                }
-            });
-        } else {
-            logger.error('remove temp failed: ' + err);
-            callback(err, null);
-        }
-    });
-}
-
-module.exports = temp;
\ No newline at end of file
diff --git a/lib/user.js b/lib/user.js
deleted file mode 100644
index 639d66c..0000000
--- a/lib/user.js
+++ /dev/null
@@ -1,110 +0,0 @@
-//user
-//external modules
-var mongoose = require('mongoose');
-var md5 = require("md5");
-
-//core
-var config = require("../config.js");
-var logger = require("./logger.js");
-
-// create a user model
-var model = mongoose.model('user', {
-    id: String,
-    profile: String,
-    history: String,
-    created: Date
-});
-
-//public
-var user = {
-    model: model,
-    findUser: findUser,
-    newUser: newUser,
-    findOrNewUser: findOrNewUser,
-    getUserCount: getUserCount,
-    parsePhotoByProfile: parsePhotoByProfile
-};
-
-function parsePhotoByProfile(profile) {
-    var photo = null;
-    switch (profile.provider) {
-        case "facebook":
-            photo = 'https://graph.facebook.com/' + profile.id + '/picture';
-            break;
-        case "twitter":
-            photo = profile.photos[0].value;
-            break;
-        case "github":
-            photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
-            break;
-        case "dropbox":
-            //no image api provided, use gravatar
-            photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
-            break;
-    }
-    return photo;
-}
-
-function getUserCount(callback) {
-    model.count(function(err, count){
-        if(err) callback(err, null);
-        else callback(null, count);
-    });
-}
-
-function findUser(id, callback) {
-    var rule = {};
-    var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
-    if (checkForHexRegExp.test(id))
-        rule._id = id;
-    else
-        rule.id = id;
-    model.findOne(rule, function (err, user) {
-        if (err) {
-            logger.error('find user failed: ' + err);
-            callback(err, null);
-        }
-        if (!err && user) {
-            callback(null, user);
-        } else {
-            logger.error('find user failed: ' + err);
-            callback(err, null);
-        };
-    });
-}
-
-function newUser(id, profile, callback) {
-    var user = new model({
-        id: id,
-        profile: JSON.stringify(profile),
-        created: Date.now()
-    });
-    user.save(function (err) {
-        if (err) {
-            logger.error('new user failed: ' + err);
-            callback(err, null);
-        } else {
-            logger.info("new user success: " + user.id);
-            callback(null, user);
-        };
-    });
-}
-
-function findOrNewUser(id, profile, callback) {
-    findUser(id, function(err, user) {
-        if(err || !user) {
-            newUser(id, profile, function(err, user) {
-                if(err) {
-                    logger.error('find or new user failed: ' + err);
-                    callback(err, null);
-                } else {
-                    callback(null, user);
-                }
-            });
-        } else {
-            callback(null, user);
-        }
-    });
-}
-
-module.exports = user;
\ No newline at end of file
diff --git a/package.json b/package.json
index 9619c1f..2988c37 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "chance": "^1.0.1",
     "cheerio": "^0.20.0",
     "compression": "^1.6.1",
-    "connect-mongo": "^1.1.0",
+    "connect-session-sequelize": "^3.0.0",
     "cookie": "0.2.3",
     "cookie-parser": "1.4.1",
     "ejs": "^2.4.1",
@@ -25,16 +25,15 @@
     "highlight.js": "^9.2.0",
     "imgur": "^0.1.7",
     "jsdom-nogyp": "^0.8.3",
-    "kerberos": "0.0.19",
     "lz-string": "1.4.4",
     "markdown-pdf": "^7.0.0",
     "marked": "^0.3.5",
     "meta-marked": "^0.4.0",
     "method-override": "^2.3.5",
     "moment": "^2.12.0",
-    "mongoose": "^4.4.7",
     "morgan": "^1.7.0",
     "mustache": "2.2.1",
+    "mysql": "^2.10.2",
     "node-uuid": "^1.4.7",
     "passport": "^0.3.2",
     "passport-dropbox-oauth2": "^1.0.0",
@@ -42,13 +41,17 @@
     "passport-github": "^1.1.0",
     "passport-twitter": "^1.0.4",
     "passport.socketio": "^3.6.1",
-    "pg": "4.x",
     "randomcolor": "^0.4.3",
     "request": "^2.69.0",
+    "pg": "^4.5.3",
+    "pg-hstore": "^2.3.2",
     "reveal.js": "3.2.0",
     "shortid": "2.2.4",
+    "sequelize": "^3.21.0",
     "socket.io": "1.4.5",
+    "sqlite3": "^3.1.3",
     "string": "^3.3.1",
+    "tedious": "^1.14.0",
     "toobusy-js": "^0.4.3",
     "winston": "^2.2.0"
   },
diff --git a/public/features.md b/public/docs/features.md
similarity index 91%
rename from public/features.md
rename to public/docs/features.md
index dde852f..a734f3c 100644
--- a/public/features.md
+++ b/public/docs/features.md
@@ -3,7 +3,7 @@ Features
 
 Introduction
 ===
-<i class="fa fa-file-text"></i> HackMD is a realtime collaborate markdown note in all platforms.
+<i class="fa fa-file-text"></i> **HackMD** is a realtime collaborate markdown note in all platforms.
 This mean you can do some notes with any other in **Desktop, Tablet or even Phone**.
 You can Sign in via **Facebook, Twitter, GitHub, Dropbox** in the **[homepage](/)**.
 
@@ -37,11 +37,11 @@ If you want to share a **editable** note, just copy the url.
 If you want to share a **read-only** note, simply press share button <i class="fa fa-share-alt"></i> and copy the url.
 
 ## Save
-Currently, you can save to **dropbox** <i class="fa fa-dropbox"></i> or save as **.md** <i class="fa fa-file-text"></i> to local.
+Currently, you can save to **Dropbox** <i class="fa fa-dropbox"></i> or save as **.md** <i class="fa fa-file-text"></i> to local.
 
 ## Import
-Like save feature, you can also import **.md** from **dropbox** <i class="fa fa-dropbox"></i>.
-Or import from your **clipboard** <i class="fa fa-clipboard"></i>, and that can parse some **html** which might be useful :smiley:
+Like save feature, you can also import **.md** from **Dropbox** <i class="fa fa-dropbox"></i>.
+Or import from your **Clipboard** <i class="fa fa-clipboard"></i>, and that can parse some **html** which might be useful :smiley:
 
 ## Permission
 There is a little button on the top right of the view.
@@ -60,6 +60,11 @@ It might be one of below:
 <iframe width="100%" height="500" src="http://hackmd.io/features" frameborder="0"></iframe>
 ```
 
+## [Slide Mode](./slide-example)
+You can use some syntax to divide your note into slides.
+Then use **Slide Mode** <i class="fa fa-tv"></i> to made a presentation.
+Visit above link for detail.
+
 View
 ===
 ## Table of content
@@ -93,12 +98,13 @@ This will take the first **level 1 header** as the note title.
 Using tags like below, these will show in your **history**.
 ###### tags: `features` `cool` `updated`
 
-## [YAML metadata](https://hackmd.io/IwFgZgxiBsBGCsBaAnPYAORJm07gDMImGAKYnrwDsUI8QA==)
+## [YAML metadata](./yaml-metadata)
 Provide advanced note information to set the browse behavior, visit above link for detail
-- robots: set search engine to index or not
+- robots: set web robots meta
 - lang: set browse language
 - dir: set text direction
-- breaks: set to use line breaks
+- breaks: set to use line breaks or not
+- mathjax: set to parse mathjax or not
 
 ## Emoji
 You can type any emoji like this :smile: :smiley: :cry: :wink:
@@ -241,9 +247,41 @@ digraph hierarchy {
 }
 ```
 
+### Mermaid
+```mermaid
+gantt
+    title A Gantt Diagram
+
+    section Section
+    A task           :a1, 2014-01-01, 30d
+    Another task     :after a1  , 20d
+    section Another
+    Task in sec      :2014-01-12  , 12d
+    anther task      : 24d
+```
+
 > More information about **Sequence diagrams** syntax [here](http://bramp.github.io/js-sequence-diagrams/).
 > More information about **Flow charts** syntax [here](http://adrai.github.io/flowchart.js/).
 > More information about **Graphviz** syntax [here](http://www.tonyballantyne.com/graphs.html)
+> More information about **Mermaid** syntax [here](http://knsv.github.io/mermaid)
+
+Alert area
+---
+:::success
+Yes :tada:
+:::
+
+:::info
+This is a message :mega:
+:::
+
+:::warning
+Watch out :zap:
+:::
+
+:::danger
+Oh No :fire:
+:::
 
 ## Typography
 
diff --git a/public/docs/slide-example.md b/public/docs/slide-example.md
new file mode 100644
index 0000000..3d9a70c
--- /dev/null
+++ b/public/docs/slide-example.md
@@ -0,0 +1,81 @@
+Slide example
+===
+This feature still in beta, may have some issues.
+
+For details:
+https://github.com/hakimel/reveal.js/
+
+---
+
+## First slide
+
+`---`
+
+Is the divder of slides
+
+----
+
+### First branch of fisrt slide
+
+`----`
+
+Is the divder of branches
+
+----
+
+### Second branch of first slide
+
+`<!-- .element: class="fragment" data-fragment-index="1" -->`
+
+Is the fragment syntax
+
+- Item 1<!-- .element: class="fragment" data-fragment-index="1" -->
+- Item 2<!-- .element: class="fragment" data-fragment-index="2" -->
+
+---
+
+## Second slide
+
+<!-- .slide: data-background="#1A237E" -->
+
+`<!-- .slide: data-background="#1A237E" -->`
+
+Is the background syntax
+
+---
+
+<!-- .slide: data-transition="zoom" -->
+
+`<!-- .slide: data-transition="zoom" -->`
+
+Is the transition syntax
+
+you can use:  
+none/fade/slide/convex/concave/zoom
+
+---
+
+<!-- .slide: data-transition="fade-in convex-out" -->
+
+`<!-- .slide: data-transition="fade-in convex-out" -->`
+
+Also can set different in/out transition
+
+you can use:  
+none/fade/slide/convex/concave/zoom  
+postfix with `-in` or `-out`
+
+---
+
+<!-- .slide: data-transition-speed="fast" -->
+
+`<!-- .slide: data-transition-speed="fast" -->`
+
+Custom the transition speed!
+
+you can use:  
+default/fast/slow
+
+---
+
+# The End
\ No newline at end of file
diff --git a/public/docs/yaml-metadata.md b/public/docs/yaml-metadata.md
new file mode 100644
index 0000000..5fc6e1b
--- /dev/null
+++ b/public/docs/yaml-metadata.md
@@ -0,0 +1,97 @@
+---
+robots: index, follow
+lang: en
+dir: ltr
+breaks: true
+---
+
+Supported YAML metadata
+===
+
+First you need to insert syntax like this at the **start** of the note:
+```
+---
+YAML metas
+---
+```
+
+Replace the "YAML metas" in this section with any YAML options as below.
+You can also refer to this note's source code.
+
+robots
+---
+This option will give below meta in the note head meta:
+```xml
+<meta name="robots" content="your_meta">
+```
+So you can prevent any search engine index your note by set `noindex, nofollow`.
+
+> default: not 
+
+**Example**
+```xml
+robots: noindex, nofollow
+```
+
+lang
+---
+This option will set the language of the note, that might alter some typography of it.
+You can find your the language code in ISO 639-1 standard:
+https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+
+> default: not set (which will be en)
+
+**Example**
+```xml
+langs: ja-jp
+```
+
+dir
+---
+This option provide to describe the direction of the text in this note.
+You can only use whether `rtl` or `ltr`.
+Look more at here:
+http://www.w3.org/International/questions/qa-html-dir
+
+> default: not set (which will be ltr)
+
+**Example**
+```xml
+dir: rtl
+```
+
+breaks
+---
+This option means the hardbreaks in the note will be parsed or be ignore.
+The original markdown syntax breaks only if you put space twice, but HackMD choose to breaks every time you enter a break.
+You can only use whether `true` or `false`.
+
+> default: not set (which will be true)
+
+**Example**
+```xml
+breaks: false
+```
+
+mathjax
+---
+This option let you to choose to parse mathjax syntax or not.
+
+> default: not set (which will be true)
+
+**Example**
+```xml
+mathjax: false
+```
+
+spellcheck
+---
+**Warning: Experimental feature!**
+This option let you to choose to enable spell checking feature or not.
+
+> default: not set (which will be false)
+
+**Example**
+```xml
+spellcheck: true
+```
\ No newline at end of file
diff --git a/public/index.ejs b/public/index.ejs
deleted file mode 100644
index 87e86f2..0000000
--- a/public/index.ejs
+++ /dev/null
@@ -1,237 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <meta name="apple-mobile-web-app-capable" content="yes">
-    <meta name="apple-mobile-web-app-status-bar-style" content="black">
-    <meta name="mobile-web-app-capable" content="yes">
-    <meta name="description" content="Realtime collaborative markdown notes on all platforms.">
-    <meta name="author" content="jackycute">
-    <title>HackMD - Collaborative notes</title>
-    <link rel="icon" type="image/png" href="<%- url %>/favicon.png">
-    <link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
-
-    <!-- Bootstrap core CSS -->
-    <% if(useCDN) { %>
-    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
-    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
-	<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css">
-	<% } else { %>
-	<link rel="stylesheet" href="<%- url %>/vendor/bootstrap/dist/css/bootstrap.min.css">
-	<link rel="stylesheet" href="<%- url %>/vendor/font-awesome/css/font-awesome.min.css">
-	<link rel="stylesheet" href="<%- url %>/css/bootstrap-social.css">
-	<% } %>
-    <link rel="stylesheet" href="<%- url %>/vendor/select2/select2.css">
-    <link rel="stylesheet" href="<%- url %>/vendor/select2/select2-bootstrap.css">
-    <!-- Custom styles for this template -->
-    <link rel="stylesheet" href="<%- url %>/css/cover.css">
-    <link rel="stylesheet" href="<%- url %>/css/site.css">
-</head>
-
-<body>
-    <div class="site-wrapper">
-        <div class="site-wrapper-inner">
-            <div class="cover-container">
-
-                <div class="masthead clearfix">
-                    <div class="inner">
-                        <h3 class="masthead-brand"></h3>
-                        <nav>
-                            <ul class="nav masthead-nav">
-                                <li class="ui-home active"><a href="#">Home</a>
-                                </li>
-                                <li class="ui-history"><a href="#">History</a>
-                                </li>
-                                <li class="ui-releasenotes"><a href="#">Release Notes</a>
-                                </li>
-                            </ul>
-                        </nav>
-                    </div>
-                </div>
-
-                <div id="home" class="section">
-                    <div class="inner cover">
-                        <h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
-                        <p class="lead">
-                            Realtime collaborative markdown notes on all platforms.
-                        </p>
-                        <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
-                        <div class="ui-or" style="display:none;">Or</div>
-                        <p class="lead">
-                            <a href="<%- url %>/new" class="btn btn-lg btn-default">New note</a>
-                        </p>
-                        <h5>Share directly with URL <i class="fa fa-link"></i></h5>
-                        <a class="btn btn-primary" href="<%- url %>/features">More features <i class="fa fa-chevron-right"></i></a>
-                    </div>
-                    <br>
-                </div>
-
-                <div id="history" class="section" style="display:none;">
-                    <div class="ui-signin">
-                        <h4>
-                            <a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
-                        </h4>
-                        <p>Below are history from browser</p>
-                    </div>
-                    <div class="ui-signout" style="display:none;">
-                        <h4 class="ui-welcome">Welcome! <span class="ui-name"></span></h4>
-                        <a href="<%- url %>/new" class="btn btn-default">New note</a> Or
-                        <a href="#" class="btn btn-danger ui-logout">Sign Out</a>
-                    </div>
-                    <hr>
-                    <form class="form-inline">
-                        <div class="form-group" style="vertical-align: bottom;">
-                            <input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
-                        </div>
-                        <div class="form-group">
-                            <input class="search form-control" placeholder="Search anything..." />
-                        </div>
-                        <a href="#" class="sort btn btn-default" data-sort="text" title="Sort by title">
-                        Title
-                        </a>
-                        <a href="#" class="sort btn btn-default" data-sort="timestamp" title="Sort by time">
-                        Time
-                        </a>
-                        <span class="hidden-xs hidden-sm">
-                            <a href="#" class="btn btn-default ui-save-history" title="Export history"><i class="fa fa-save"></i></a>
-                            <span class="btn btn-default btn-file ui-open-history" title="Import history">
-                                <i class="fa fa-folder-open-o"></i><input type="file" />
-                            </span>
-                        <a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
-                        </span>
-                        <a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
-                    </form>
-                    <h4 class="ui-nohistory" style="display:none;">
-                        No history
-                    </h4>
-                    <a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;">Import from browser</a>
-                    <ul id="history-list" class="list">
-                    </ul>
-                </div>
-                <div id="releasenotes" class="section" style="display:none;">
-                    <div id="template" style="display:none;">
-                        {{#each release}}
-                        <div class="inner cover">
-                            <h5 class="cover-heading">
-                                <div class="text-left">
-                                   <i class="fa fa-tag"></i> {{version}}
-                                   &nbsp;<span class="label label-default">{{tag}}</span>
-                                   <div class="pull-right">
-                                       <i class="fa fa-clock-o"></i> {{date}}
-                                   </div>
-                                </div>
-                            </h5>
-                            <hr>{{#each detail}}
-                            <div class="text-left">
-                                <h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
-                                <ul>
-                                    {{#each item}}
-                                    <li>
-                                        {{this}}
-                                    </li>
-                                    {{/each}}
-                                </ul>
-                            </div>
-                            {{/each}}
-                        </div>
-                        {{#unless @last}}
-                        <br>{{/unless}} {{/each}}
-                    </div>
-                </div>
-
-                <div class="mastfoot">
-                    <div class="inner">
-                        <h6>
-                            <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
-                        </h6>
-                        <p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
-                        </p>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- signin modal -->
-    <div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
-        <div class="modal-dialog modal-sm">
-            <div class="modal-content">
-                <div class="modal-header">
-                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
-                    </button>
-                    <h4 class="modal-title" id="mySmallModalLabel">Choose method</h4>
-                </div>
-                <div class="modal-body">
-                    <a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
-                        <i class="fa fa-facebook"></i> Sign in via Facebook
-                    </a>
-                    <a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
-                        <i class="fa fa-twitter"></i> Sign in via Twitter
-                    </a>
-                    <a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
-                        <i class="fa fa-github"></i> Sign in via GitHub
-                    </a>
-                    <a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
-                        <i class="fa fa-dropbox"></i> Sign in via Dropbox
-                    </a>
-                </div>
-            </div>
-        </div>
-    </div>
-    <!-- delete modal -->
-    <div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
-        <div class="modal-dialog modal-sm">
-            <div class="modal-content">
-                <div class="modal-header">
-                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
-                    </button>
-                    <h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
-                </div>
-                <div class="modal-body" style="color:black;">
-                    <h5 class="ui-delete-modal-msg"></h5>
-                    <strong class="ui-delete-modal-item"></strong>
-                </div>
-                <div class="modal-footer">
-                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
-                    <button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- Bootstrap core JavaScript
-    ================================================== -->
-    <!-- Placed at the end of the document so the pages load faster -->
-    <% if(useCDN) { %>
-	<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
-	<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
-	<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
-    <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
-	<script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
-	<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment-with-locales.min.js" defer></script>
-	<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
-	<script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
-	<% } else { %>
-	<script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
-	<script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
-    <script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
-    <script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
-	<script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
-	<script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
-    <script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
-	<script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
-	<% } %>
-    <script src="<%- url %>/vendor/js.cookie.js" defer></script>
-    <script src="<%- url %>/vendor/list.min.js" defer></script>
-    <script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
-    <script src="<%- url %>/vendor/store.min.js" defer></script>
-    <script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
-    <script src="<%- url %>/js/common.js" defer></script>
-    <script src="<%- url %>/js/history.js" defer></script>
-    <script src="<%- url %>/js/cover.js" defer></script>
-</body>
-
-</html>
\ No newline at end of file
diff --git a/public/js/extra.js b/public/js/extra.js
index 41b984d..be454ed 100644
--- a/public/js/extra.js
+++ b/public/js/extra.js
@@ -1,15 +1,24 @@
 //auto update last change
+var createtime = null;
 var lastchangetime = null;
 var lastchangeui = {
+    status: $(".ui-status-lastchange"),
     time: $(".ui-lastchange"),
     user: $(".ui-lastchangeuser"),
     nouser: $(".ui-no-lastchangeuser")
 }
 
 function updateLastChange() {
-    if (lastchangetime && lastchangeui) {
-        lastchangeui.time.html(moment(lastchangetime).fromNow());
-        lastchangeui.time.attr('title', moment(lastchangetime).format('llll'));
+    if (!lastchangeui) return;
+    if (createtime) {
+        if (createtime && !lastchangetime) {
+            lastchangeui.status.text('created');
+        } else {
+            lastchangeui.status.text('changed');
+        }
+        var time = lastchangetime || createtime;
+        lastchangeui.time.html(moment(time).fromNow());
+        lastchangeui.time.attr('title', moment(time).format('llll'));
     }
 }
 setInterval(updateLastChange, 60000);
diff --git a/public/js/history.js b/public/js/history.js
index edecde1..b3bae98 100644
--- a/public/js/history.js
+++ b/public/js/history.js
@@ -93,8 +93,14 @@ function clearDuplicatedHistory(notehistory) {
     for (var i = 0; i < notehistory.length; i++) {
         var found = false;
         for (var j = 0; j < newnotehistory.length; j++) {
-            var id = LZString.decompressFromBase64(notehistory[i].id);
-            var newId = LZString.decompressFromBase64(newnotehistory[j].id);
+            var id = notehistory[i].id;
+            var newId = newnotehistory[j].id;
+            try {
+                id = LZString.decompressFromBase64(id);
+                newId = LZString.decompressFromBase64(newId);
+            } catch (err) {
+                // na
+            }
             if (id == newId || notehistory[i].id == newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) {
                 var time = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a');
                 var newTime = moment(newnotehistory[j].time, 'MMMM Do YYYY, h:mm:ss a');
diff --git a/public/js/index.js b/public/js/index.js
index 6cbaf06..0e4fd21 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1423,7 +1423,7 @@ function updatePermission(newPermission) {
         title = "Only owner can view & edit";
         break;
     }
-    if (personalInfo.userid == owner) {
+    if (personalInfo.userid && personalInfo.userid == owner) {
         label += ' <i class="fa fa-caret-down"></i>';
         ui.infobar.permission.label.removeClass('disabled');
     } else {
@@ -1476,11 +1476,14 @@ socket.emit = function () {
 socket.on('info', function (data) {
     console.error(data);
     switch (data.code) {
+    case 403:
+        location.href = "./403";
+        break;
     case 404:
         location.href = "./404";
         break;
-    case 403:
-        location.href = "./403";
+    case 500:
+        location.href = "./500";
         break;
     }
 });
@@ -1517,11 +1520,15 @@ socket.on('version', function (data) {
 });
 function updateLastInfo(data) {
     //console.log(data);
-    if (lastchangetime !== data.updatetime) {
+    if (data.hasOwnProperty('createtime') && createtime !== data.createtime) {
+        createtime = data.createtime;
+        updateLastChange();
+    }
+    if (data.hasOwnProperty('updatetime') && lastchangetime !== data.updatetime) {
         lastchangetime = data.updatetime;
         updateLastChange();
     }
-    if (lastchangeuser !== data.lastchangeuser) {
+    if (data.hasOwnProperty('lastchangeuser') && lastchangeuser !== data.lastchangeuser) {
         lastchangeuser = data.lastchangeuser;
         lastchangeuserprofile = data.lastchangeuserprofile;
         updateLastChangeUser();
diff --git a/public/js/pretty.js b/public/js/pretty.js
index 44d27e5..2d1f27d 100644
--- a/public/js/pretty.js
+++ b/public/js/pretty.js
@@ -20,7 +20,8 @@ renderTOC(markdown);
 generateToc('toc');
 generateToc('toc-affix');
 smoothHashScroll();
-lastchangetime = lastchangeui.time.text();
+createtime = lastchangeui.time.attr('data-createtime');
+lastchangetime = lastchangeui.time.attr('data-updatetime');
 updateLastChange();
 var url = window.location.pathname;
 $('.ui-edit').attr('href', url + '/edit');
diff --git a/public/views/body.ejs b/public/views/body.ejs
index 54562ea..044f7e6 100644
--- a/public/views/body.ejs
+++ b/public/views/body.ejs
@@ -8,7 +8,7 @@
                 <span>
                 	<span class="ui-lastchangeuser" style="display: none;">&thinsp;<i class="ui-user-icon small" data-toggle="tooltip" data-placement="right"></i></span>
                 	<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
-                	&nbsp;<span class="text-uppercase">changed</span>
+                	&nbsp;<span class="text-uppercase ui-status-lastchange"></span>
                 	<span class="ui-lastchange text-uppercase"></span>
                 </span>
                 <span class="ui-permission dropdown pull-right">
@@ -73,32 +73,6 @@
         </div>
     </div>
 </div>
-<!-- signin modal -->
-<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
-    <div class="modal-dialog modal-sm">
-        <div class="modal-content">
-            <div class="modal-header">
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
-                </button>
-                <h4 class="modal-title" id="mySmallModalLabel">Please sign in to edit</h4>
-            </div>
-            <div class="modal-body">
-                <a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
-                    <i class="fa fa-facebook"></i> Sign in via Facebook
-                </a>
-                <a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
-                    <i class="fa fa-twitter"></i> Sign in via Twitter
-                </a>
-                <a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
-                    <i class="fa fa-github"></i> Sign in via GitHub
-                </a>
-                <a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
-                    <i class="fa fa-dropbox"></i> Sign in via Dropbox
-                </a>
-            </div>
-        </div>
-    </div>
-</div>
 <!-- locked modal -->
 <div class="modal fade locked-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
     <div class="modal-dialog modal-sm">
diff --git a/public/views/hackmd.ejs b/public/views/hackmd.ejs
new file mode 100644
index 0000000..c5778fc
--- /dev/null
+++ b/public/views/hackmd.ejs
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <%- include head %>
+</head>
+
+<body>
+    <%- include header %>
+    <%- include body %>
+    <%- include footer %>
+    <%- include foot %>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public/views/header.ejs b/public/views/header.ejs
index 410886d..bf8f9f6 100644
--- a/public/views/header.ejs
+++ b/public/views/header.ejs
@@ -38,8 +38,10 @@
                 </li>
                 <li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
                 </li>
+                <% if(typeof github !== 'undefined' && github) { %>
                 <li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
                 </li>
+                <% } %>
                 <li class="divider"></li>
                 <li class="dropdown-header">Import</li>
                 <li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
@@ -119,8 +121,10 @@
                     </li>
                     <li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
                     </li>
+                    <% if(typeof github !== 'undefined' && github) { %>
                     <li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
-                </li>
+                    </li>
+                    <% } %>
                     <li class="divider"></li>
                     <li class="dropdown-header">Import</li>
                     <li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
diff --git a/public/views/index.ejs b/public/views/index.ejs
index c5778fc..85d955b 100644
--- a/public/views/index.ejs
+++ b/public/views/index.ejs
@@ -2,14 +2,210 @@
 <html lang="en">
 
 <head>
-    <%- include head %>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black">
+    <meta name="mobile-web-app-capable" content="yes">
+    <meta name="description" content="Realtime collaborative markdown notes on all platforms.">
+    <meta name="author" content="jackycute">
+    <title>HackMD - Collaborative notes</title>
+    <link rel="icon" type="image/png" href="<%- url %>/favicon.png">
+    <link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
+
+    <!-- Bootstrap core CSS -->
+	<% if(useCDN) { %>
+    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
+    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
+	<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css">
+	<% } else { %>
+	<link rel="stylesheet" href="<%- url %>/vendor/bootstrap/dist/css/bootstrap.min.css">
+	<link rel="stylesheet" href="<%- url %>/vendor/font-awesome/css/font-awesome.min.css">
+	<link rel="stylesheet" href="<%- url %>/css/bootstrap-social.css">
+	<% } %>
+    <link rel="stylesheet" href="<%- url %>/vendor/select2/select2.css">
+    <link rel="stylesheet" href="<%- url %>/vendor/select2/select2-bootstrap.css">
+    <!-- Custom styles for this template -->
+    <link rel="stylesheet" href="<%- url %>/css/cover.css">
+    <link rel="stylesheet" href="<%- url %>/css/site.css">
 </head>
 
 <body>
-    <%- include header %>
-    <%- include body %>
-    <%- include footer %>
-    <%- include foot %>
+    <div class="site-wrapper">
+        <div class="site-wrapper-inner">
+            <div class="cover-container">
+
+                <div class="masthead clearfix">
+                    <div class="inner">
+                        <h3 class="masthead-brand"></h3>
+                        <nav>
+                            <ul class="nav masthead-nav">
+                                <li class="ui-home active"><a href="#">Home</a>
+                                </li>
+                                <li class="ui-history"><a href="#">History</a>
+                                </li>
+                                <li class="ui-releasenotes"><a href="#">Release Notes</a>
+                                </li>
+                            </ul>
+                        </nav>
+                    </div>
+                </div>
+
+                <div id="home" class="section">
+                    <div class="inner cover">
+                        <h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
+                        <p class="lead">
+                            Realtime collaborative markdown notes on all platforms.
+                        </p>
+                        <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
+                        <div class="ui-or" style="display:none;">Or</div>
+                        <p class="lead">
+                            <a href="<%- url %>/new" class="btn btn-lg btn-default">New note</a>
+                        </p>
+                        <h5>Share directly with URL <i class="fa fa-link"></i></h5>
+                        <a class="btn btn-primary" href="<%- url %>/features">More features <i class="fa fa-chevron-right"></i></a>
+                    </div>
+                    <br>
+                </div>
+
+                <div id="history" class="section" style="display:none;">
+                    <div class="ui-signin">
+                        <h4>
+                            <a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
+                        </h4>
+                        <p>Below are history from browser</p>
+                    </div>
+                    <div class="ui-signout" style="display:none;">
+                        <h4 class="ui-welcome">Welcome! <span class="ui-name"></span></h4>
+                        <a href="<%- url %>/new" class="btn btn-default">New note</a> Or
+                        <a href="#" class="btn btn-danger ui-logout">Sign Out</a>
+                    </div>
+                    <hr>
+                    <form class="form-inline">
+                        <div class="form-group" style="vertical-align: bottom;">
+                            <input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
+                        </div>
+                        <div class="form-group">
+                            <input class="search form-control" placeholder="Search anything..." />
+                        </div>
+                        <a href="#" class="sort btn btn-default" data-sort="text" title="Sort by title">
+                        Title
+                        </a>
+                        <a href="#" class="sort btn btn-default" data-sort="timestamp" title="Sort by time">
+                        Time
+                        </a>
+                        <span class="hidden-xs hidden-sm">
+                            <a href="#" class="btn btn-default ui-save-history" title="Export history"><i class="fa fa-save"></i></a>
+                            <span class="btn btn-default btn-file ui-open-history" title="Import history">
+                                <i class="fa fa-folder-open-o"></i><input type="file" />
+                            </span>
+                        <a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
+                        </span>
+                        <a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
+                    </form>
+                    <h4 class="ui-nohistory" style="display:none;">
+                        No history
+                    </h4>
+                    <a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;">Import from browser</a>
+                    <ul id="history-list" class="list">
+                    </ul>
+                </div>
+                <div id="releasenotes" class="section" style="display:none;">
+                    <div id="template" style="display:none;">
+                        {{#each release}}
+                        <div class="inner cover">
+                            <h5 class="cover-heading">
+                                <div class="text-left">
+                                   <i class="fa fa-tag"></i> {{version}}
+                                   &nbsp;<span class="label label-default">{{tag}}</span>
+                                   <div class="pull-right">
+                                       <i class="fa fa-clock-o"></i> {{date}}
+                                   </div>
+                                </div>
+                            </h5>
+                            <hr>{{#each detail}}
+                            <div class="text-left">
+                                <h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
+                                <ul>
+                                    {{#each item}}
+                                    <li>
+                                        {{this}}
+                                    </li>
+                                    {{/each}}
+                                </ul>
+                            </div>
+                            {{/each}}
+                        </div>
+                        {{#unless @last}}
+                        <br>{{/unless}} {{/each}}
+                    </div>
+                </div>
+
+                <div class="mastfoot">
+                    <div class="inner">
+                        <h6>
+                            <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
+                        </h6>
+                        <p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <!-- delete modal -->
+    <div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+        <div class="modal-dialog modal-sm">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
+                    </button>
+                    <h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
+                </div>
+                <div class="modal-body" style="color:black;">
+                    <h5 class="ui-delete-modal-msg"></h5>
+                    <strong class="ui-delete-modal-item"></strong>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+                    <button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
+                </div>
+            </div>
+        </div>
+    </div>
+    <%- include modal %>
+
+    <!-- Bootstrap core JavaScript
+    ================================================== -->
+    <!-- Placed at the end of the document so the pages load faster -->
+	<% if(useCDN) { %>
+	<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
+	<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
+	<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
+    <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
+	<script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
+	<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment-with-locales.min.js" defer></script>
+	<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
+	<script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
+	<% } else { %>
+	<script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
+	<script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
+    <script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
+    <script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
+	<script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
+	<script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
+    <script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
+	<script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
+	<% } %>
+    <script src="<%- url %>/vendor/js.cookie.js" defer></script>
+    <script src="<%- url %>/vendor/list.min.js" defer></script>
+    <script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
+    <script src="<%- url %>/vendor/store.min.js" defer></script>
+    <script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
+    <script src="<%- url %>/js/common.js" defer></script>
+    <script src="<%- url %>/js/history.js" defer></script>
+    <script src="<%- url %>/js/cover.js" defer></script>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/public/views/modal.ejs b/public/views/modal.ejs
new file mode 100644
index 0000000..260ff42
--- /dev/null
+++ b/public/views/modal.ejs
@@ -0,0 +1,34 @@
+<!-- signin modal -->
+<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
+                </button>
+                <h4 class="modal-title" id="mySmallModalLabel">Choose method</h4>
+            </div>
+            <div class="modal-body">
+                <% if(facebook) { %>
+                <a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
+                    <i class="fa fa-facebook"></i> Sign in via Facebook
+                </a>
+                <% } %>
+                <% if(twitter) { %>
+                <a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
+                    <i class="fa fa-twitter"></i> Sign in via Twitter
+                </a>
+                <% } %>
+                <% if(github) { %>
+                <a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
+                    <i class="fa fa-github"></i> Sign in via GitHub
+                </a>
+                <% } %>
+                <% if(dropbox) { %>
+                <a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
+                    <i class="fa fa-dropbox"></i> Sign in via Dropbox
+                </a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/public/views/pretty.ejs b/public/views/pretty.ejs
index ed6e638..c8e959f 100644
--- a/public/views/pretty.ejs
+++ b/public/views/pretty.ejs
@@ -49,8 +49,8 @@
                 <% } else { %>
                 	<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
                 <% } %>
-                &nbsp;<span class="text-uppercase">changed</span>
-                <span class="ui-lastchange text-uppercase"><%- updatetime %></span>
+                &nbsp;<span class="text-uppercase ui-status-lastchange"></span>
+                <span class="ui-lastchange text-uppercase" data-createtime="<%- createtime %>" data-updatetime="<%- updatetime %>"></span>
             </span>
             <span class="pull-right"><%- viewcount %> views <a href="#" class="ui-edit" title="Edit this note"><i class="fa fa-fw fa-pencil"></i></a></span>
         </small>
diff --git a/public/views/slide/reveal.hbs b/public/views/slide.hbs
similarity index 98%
rename from public/views/slide/reveal.hbs
rename to public/views/slide.hbs
index 8a93c26..262a5df 100644
--- a/public/views/slide/reveal.hbs
+++ b/public/views/slide.hbs
@@ -20,7 +20,7 @@
         <script>
           document.write( '<link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/css/print/' + ( window.location.search.match( /print-pdf/gi ) ? 'pdf' : 'paper' ) + '.css" type="text/css" media="print">' );
         </script>
-		<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
+		<script src="{{{url}}}/vendor/jquery/dist/jquery.min.js"></script>
     </head>
     <body>
 
diff --git a/public/views/slide/listing.hbs b/public/views/slide/listing.hbs
deleted file mode 100644
index 7da8eba..0000000
--- a/public/views/slide/listing.hbs
+++ /dev/null
@@ -1,22 +0,0 @@
-<!doctype html>
-<html lang="en">
-    <head>
-        <meta charset="utf-8">
-        <title>Directory Listing</title>
-        <link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/{{{theme}}}" id="theme">
-        <style type="text/css">
-            body {
-                margin: 1em;
-            }
-            a {
-                color: white;
-                display: block;
-            }
-        </style>
-		<link rel="icon" href="http://i.imgur.com/IVlU2PU.png" sizes="512x512" />
-    </head>
-
-    <body>
-        {{{listing}}}
-    </body>
-</html>