diff --git a/lib/migrations/20160703062241-support-authorship.js b/lib/migrations/20160703062241-support-authorship.js
new file mode 100644
index 000000000..239327ecf
--- /dev/null
+++ b/lib/migrations/20160703062241-support-authorship.js
@@ -0,0 +1,28 @@
+'use strict';
+
+module.exports = {
+  up: function (queryInterface, Sequelize) {
+    queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT);
+    queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT);
+    queryInterface.createTable('Authors', {
+      id: {
+        type: Sequelize.INTEGER,
+        primaryKey: true,
+        autoIncrement: true
+      },
+      color: Sequelize.STRING,
+      noteId: Sequelize.UUID,
+      userId: Sequelize.UUID,
+      createdAt: Sequelize.DATE,
+      updatedAt: Sequelize.DATE
+    });
+    return;
+  },
+
+  down: function (queryInterface, Sequelize) {
+    queryInterface.dropTable('Authors');
+    queryInterface.removeColumn('Revisions', 'authorship');
+    queryInterface.removeColumn('Notes', 'authorship');
+    return;
+  }
+};
diff --git a/lib/models/author.js b/lib/models/author.js
new file mode 100644
index 000000000..0b0f149d9
--- /dev/null
+++ b/lib/models/author.js
@@ -0,0 +1,43 @@
+"use strict";
+
+// external modules
+var Sequelize = require("sequelize");
+
+// core
+var logger = require("../logger.js");
+
+module.exports = function (sequelize, DataTypes) {
+    var Author = sequelize.define("Author", {
+        id: {
+            type: Sequelize.INTEGER,
+            primaryKey: true,
+            autoIncrement: true
+        },
+        color: {
+            type: DataTypes.STRING
+        }
+    }, {
+        indexes: [
+            {
+                unique: true,
+                fields: ['noteId', 'userId']
+            }
+        ],
+        classMethods: {
+            associate: function (models) {
+                Author.belongsTo(models.Note, {
+                    foreignKey: "noteId",
+                    as: "note",
+                    constraints: false
+                });
+                Author.belongsTo(models.User, {
+                    foreignKey: "userId",
+                    as: "user",
+                    constraints: false
+                });
+            }
+        }
+    });
+    
+    return Author;
+};
\ No newline at end of file
diff --git a/lib/models/note.js b/lib/models/note.js
index f9a8ec613..5ee5c7de4 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -51,6 +51,9 @@ module.exports = function (sequelize, DataTypes) {
         content: {
             type: DataTypes.TEXT
         },
+        authorship: {
+            type: DataTypes.TEXT
+        },
         lastchangeAt: {
             type: DataTypes.DATE
         },
@@ -74,6 +77,11 @@ module.exports = function (sequelize, DataTypes) {
                     foreignKey: "noteId",
                     constraints: false
                 });
+                Note.hasMany(models.Author, {
+                    foreignKey: "noteId",
+                    as: "authors",
+                    constraints: false
+                });
             },
             checkFileExist: function (filePath) {
                 try {
diff --git a/lib/models/revision.js b/lib/models/revision.js
index bb89782fc..f525ea552 100644
--- a/lib/models/revision.js
+++ b/lib/models/revision.js
@@ -30,6 +30,9 @@ module.exports = function (sequelize, DataTypes) {
         },
         length: {
             type: DataTypes.INTEGER
+        },
+        authorship: {
+            type: DataTypes.TEXT
         }
     }, {
         classMethods: {
diff --git a/lib/ot/editor-socketio-server.js b/lib/ot/editor-socketio-server.js
index 9e4ddf965..45ed5036b 100755
--- a/lib/ot/editor-socketio-server.js
+++ b/lib/ot/editor-socketio-server.js
@@ -10,7 +10,7 @@ var util = require('util');
 var LZString = require('lz-string');
 var logger = require('../logger');
 
-function EditorSocketIOServer(document, operations, docId, mayWrite) {
+function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
     EventEmitter.call(this);
     Server.call(this, document, operations);
     this.users = {};
@@ -18,6 +18,7 @@ function EditorSocketIOServer(document, operations, docId, mayWrite) {
     this.mayWrite = mayWrite || function (_, cb) {
         cb(true);
     };
+    this.operationCallback = operationCallback;
 }
 
 util.inherits(EditorSocketIOServer, Server);
@@ -51,6 +52,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
             }
             try {
                 self.onOperation(socket, revision, operation, selection);
+                if (typeof self.operationCallback === 'function')
+                    self.operationCallback(socket, operation);
             } catch (err) {
 				socket.disconnect(true);
             }
diff --git a/lib/realtime.js b/lib/realtime.js
index 0e9af7407..680895700 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -151,6 +151,7 @@ function finishUpdateNote(note, _note, callback) {
     var values = {
         title: title,
         content: body,
+        authorship: LZString.compressToBase64(JSON.stringify(note.authorship)),
         lastchangeuserId: note.lastchangeuser,
         lastchangeAt: Date.now()
     };
@@ -404,6 +405,13 @@ function startConnection(socket) {
         }, {
             model: models.User,
             as: "lastchangeuser"
+        }, {
+            model: models.Author,
+            as: "authors",
+            include: [{
+                model: models.User,
+                as: "user"
+            }]
         }];
 
         models.Note.findOne({
@@ -424,7 +432,19 @@ function startConnection(socket) {
             var body = LZString.decompressFromBase64(note.content);
             var createtime = note.createdAt;
             var updatetime = note.lastchangeAt;
-            var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
+            var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback);
+
+            var authors = {};
+            for (var i = 0; i < note.authors.length; i++) {
+                var author = note.authors[i];
+                var profile = models.User.parseProfile(author.user.profile);
+                authors[author.userId] = {
+                    userid: author.userId,
+                    color: author.color,
+                    photo: profile.photo,
+                    name: profile.name
+                };
+            }
 
             notes[noteId] = {
                 id: noteId,
@@ -437,7 +457,9 @@ function startConnection(socket) {
                 users: {},
                 createtime: moment(createtime).valueOf(),
                 updatetime: moment(updatetime).valueOf(),
-                server: server
+                server: server,
+                authors: authors,
+                authorship: note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : []
             };
 
             return finishConnection(socket, notes[noteId], users[socket.id]);
@@ -581,6 +603,136 @@ function ifMayEdit(socket, callback) {
     return callback(mayEdit);
 }
 
+function operationCallback(socket, operation) {
+    var noteId = socket.noteId;
+    if (!noteId || !notes[noteId]) return;
+    var note = notes[noteId];
+    var userId = null;
+    // save authors
+    if (socket.request.user && socket.request.user.logged_in) {
+        var socketId = socket.id;
+        var user = users[socketId]; 
+        userId = socket.request.user.id;
+        if (!note.authors[userId]) {
+            models.Author.create({
+                noteId: noteId,
+                userId: userId,
+                color: users[socketId].color
+            }).then(function (author) {
+                note.authors[author.userId] = {
+                    userid: author.userId,
+                    color: author.color,
+                    photo: user.photo,
+                    name: user.name
+                };
+            }).catch(function (err) {
+                return logger.error('operation callback failed: ' + err);
+            });
+        }
+    }
+    // save authorship
+    var index = 0;
+    var authorships = note.authorship;
+    var timestamp = Date.now();
+    for (var i = 0; i < operation.length; i++) {
+        var op = operation[i];
+        if (ot.TextOperation.isRetain(op)) {
+            index += op;
+        } else if (ot.TextOperation.isInsert(op)) {
+            var opStart = index;
+            var opEnd = index + op.length;
+            var inserted = false;
+            // authorship format: [userId, startPos, endPos, createdAt, updatedAt]
+            if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]);
+            else {
+                for (var j = 0; j < authorships.length; j++) {
+                    var authorship = authorships[j];
+                    if (!inserted) {
+                        var nextAuthorship = authorships[j + 1] || -1;
+                        if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) {
+                            if (authorship[1] < opStart && authorship[2] > opStart) {
+                                // divide
+                                var postLength = authorship[2] - opStart;
+                                authorship[2] = opStart;
+                                authorship[4] = timestamp;
+                                authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+                                authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]);
+                                j += 2;
+                                inserted = true;
+                            } else if (authorship[1] >= opStart) {
+                                authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+                                j += 1;
+                                inserted = true;
+                            } else if (authorship[2] <= opStart) {
+                                authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+                                j += 1;
+                                inserted = true;
+                            }
+                        }
+                    }
+                    if (authorship[1] >= opStart) {
+                        authorship[1] += op.length;
+                        authorship[2] += op.length;
+                    }
+                }
+            }
+            index += op.length;
+        } else if (ot.TextOperation.isDelete(op)) {
+            var opStart = index;
+            var opEnd = index - op;
+            if (operation.length == 1) {
+                authorships = [];
+            } else if (authorships.length > 0) {
+                for (var j = 0; j < authorships.length; j++) {
+                    var authorship = authorships[j];
+                    if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
+                        authorships.splice(j, 1);
+                        j -= 1;
+                    } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
+                        authorship[2] += op;
+                        authorship[4] = timestamp;
+                    } else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
+                        authorship[2] = opStart;
+                        authorship[4] = timestamp;
+                    } else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
+                        authorship[1] = opEnd;
+                        authorship[4] = timestamp;
+                    }
+                    if (authorship[1] >= opEnd) {
+                        authorship[1] += op;
+                        authorship[2] += op;
+                    }
+                }
+            }
+            index += op;
+        }
+    }
+    // merge
+    for (var j = 0; j < authorships.length; j++) {
+        var authorship = authorships[j];
+        for (var k = j + 1; k < authorships.length; k++) {
+            var nextAuthorship = authorships[k];
+            if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
+                var minTimestamp = Math.min(authorship[3], nextAuthorship[3]);
+                var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]);
+                authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]);
+                authorships.splice(k, 1);
+                j -= 1;
+                break;
+            }
+        }
+    }
+    // clear
+    for (var j = 0; j < authorships.length; j++) {
+        var authorship = authorships[j];
+        if (!authorship[0]) {
+            authorships.splice(j, 1);
+            j -= 1;
+        }
+    }
+    note.authorship = authorships;
+}
+
 function connection(socket) {
     if (config.maintenance) return;
     parseNoteIdFromSocket(socket, function (err, noteId) {