diff --git a/models/issue.go b/models/issue.go
index 86becdbba..630391180 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -443,8 +443,16 @@ func (issue *Issue) GetAssignee() (err error) {
 }
 
 // ReadBy sets issue to be read by given user.
-func (issue *Issue) ReadBy(uid int64) error {
-	return UpdateIssueUserByRead(uid, issue.ID)
+func (issue *Issue) ReadBy(userID int64) error {
+	if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
+		return err
+	}
+
+	if err := setNotificationStatusRead(x, userID, issue.ID); err != nil {
+		return err
+	}
+
+	return nil
 }
 
 func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
diff --git a/models/models.go b/models/models.go
index 20812d0eb..1bebaa602 100644
--- a/models/models.go
+++ b/models/models.go
@@ -71,15 +71,41 @@ var (
 
 func init() {
 	tables = append(tables,
-		new(User), new(PublicKey), new(AccessToken),
-		new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload),
-		new(Watch), new(Star), new(Follow), new(Action),
-		new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
-		new(Label), new(IssueLabel), new(Milestone),
-		new(Mirror), new(Release), new(LoginSource), new(Webhook),
-		new(UpdateTask), new(HookTask),
-		new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
-		new(Notice), new(EmailAddress), new(LFSMetaObject))
+		new(User),
+		new(PublicKey),
+		new(AccessToken),
+		new(Repository),
+		new(DeployKey),
+		new(Collaboration),
+		new(Access),
+		new(Upload),
+		new(Watch),
+		new(Star),
+		new(Follow),
+		new(Action),
+		new(Issue),
+		new(PullRequest),
+		new(Comment),
+		new(Attachment),
+		new(Label),
+		new(IssueLabel),
+		new(Milestone),
+		new(Mirror),
+		new(Release),
+		new(LoginSource),
+		new(Webhook),
+		new(UpdateTask),
+		new(HookTask),
+		new(Team),
+		new(OrgUser),
+		new(TeamUser),
+		new(TeamRepo),
+		new(Notice),
+		new(EmailAddress),
+		new(Notification),
+		new(IssueUser),
+		new(LFSMetaObject),
+	)
 
 	gonicNames := []string{"SSL", "UID"}
 	for _, name := range gonicNames {
diff --git a/models/notification.go b/models/notification.go
new file mode 100644
index 000000000..46d63b482
--- /dev/null
+++ b/models/notification.go
@@ -0,0 +1,249 @@
+// Copyright 2016 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"time"
+)
+
+type (
+	// NotificationStatus is the status of the notification (read or unread)
+	NotificationStatus uint8
+	// NotificationSource is the source of the notification (issue, PR, commit, etc)
+	NotificationSource uint8
+)
+
+const (
+	// NotificationStatusUnread represents an unread notification
+	NotificationStatusUnread NotificationStatus = iota + 1
+	// NotificationStatusRead represents a read notification
+	NotificationStatusRead
+)
+
+const (
+	// NotificationSourceIssue is a notification of an issue
+	NotificationSourceIssue NotificationSource = iota + 1
+	// NotificationSourcePullRequest is a notification of a pull request
+	NotificationSourcePullRequest
+	// NotificationSourceCommit is a notification of a commit
+	NotificationSourceCommit
+)
+
+// Notification represents a notification
+type Notification struct {
+	ID     int64 `xorm:"pk autoincr"`
+	UserID int64 `xorm:"INDEX NOT NULL"`
+	RepoID int64 `xorm:"INDEX NOT NULL"`
+
+	Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"`
+	Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"`
+
+	IssueID  int64  `xorm:"INDEX NOT NULL"`
+	CommitID string `xorm:"INDEX"`
+
+	UpdatedBy int64 `xorm:"INDEX NOT NULL"`
+
+	Issue      *Issue      `xorm:"-"`
+	Repository *Repository `xorm:"-"`
+
+	Created     time.Time `xorm:"-"`
+	CreatedUnix int64     `xorm:"INDEX NOT NULL"`
+	Updated     time.Time `xorm:"-"`
+	UpdatedUnix int64     `xorm:"INDEX NOT NULL"`
+}
+
+// BeforeInsert runs while inserting a record
+func (n *Notification) BeforeInsert() {
+	var (
+		now     = time.Now()
+		nowUnix = now.Unix()
+	)
+	n.Created = now
+	n.CreatedUnix = nowUnix
+	n.Updated = now
+	n.UpdatedUnix = nowUnix
+}
+
+// BeforeUpdate runs while updateing a record
+func (n *Notification) BeforeUpdate() {
+	var (
+		now     = time.Now()
+		nowUnix = now.Unix()
+	)
+	n.Updated = now
+	n.UpdatedUnix = nowUnix
+}
+
+// CreateOrUpdateIssueNotifications creates an issue notification
+// for each watcher, or updates it if already exists
+func CreateOrUpdateIssueNotifications(issue *Issue, notificationAuthorID int64) error {
+	sess := x.NewSession()
+	defer sess.Close()
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+
+	if err := createOrUpdateIssueNotifications(sess, issue, notificationAuthorID); err != nil {
+		return err
+	}
+
+	return sess.Commit()
+}
+
+func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthorID int64) error {
+	watches, err := getWatchers(e, issue.RepoID)
+	if err != nil {
+		return err
+	}
+
+	notifications, err := getNotificationsByIssueID(e, issue.ID)
+	if err != nil {
+		return err
+	}
+
+	for _, watch := range watches {
+		// do not send notification for the own issuer/commenter
+		if watch.UserID == notificationAuthorID {
+			continue
+		}
+
+		if notificationExists(notifications, issue.ID, watch.UserID) {
+			err = updateIssueNotification(e, watch.UserID, issue.ID, notificationAuthorID)
+		} else {
+			err = createIssueNotification(e, watch.UserID, issue, notificationAuthorID)
+		}
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func getNotificationsByIssueID(e Engine, issueID int64) (notifications []*Notification, err error) {
+	err = e.
+		Where("issue_id = ?", issueID).
+		Find(&notifications)
+	return
+}
+
+func notificationExists(notifications []*Notification, issueID, userID int64) bool {
+	for _, notification := range notifications {
+		if notification.IssueID == issueID && notification.UserID == userID {
+			return true
+		}
+	}
+
+	return false
+}
+
+func createIssueNotification(e Engine, userID int64, issue *Issue, updatedByID int64) error {
+	notification := &Notification{
+		UserID:    userID,
+		RepoID:    issue.RepoID,
+		Status:    NotificationStatusUnread,
+		IssueID:   issue.ID,
+		UpdatedBy: updatedByID,
+	}
+
+	if issue.IsPull {
+		notification.Source = NotificationSourcePullRequest
+	} else {
+		notification.Source = NotificationSourceIssue
+	}
+
+	_, err := e.Insert(notification)
+	return err
+}
+
+func updateIssueNotification(e Engine, userID, issueID, updatedByID int64) error {
+	notification, err := getIssueNotification(e, userID, issueID)
+	if err != nil {
+		return err
+	}
+
+	notification.Status = NotificationStatusUnread
+	notification.UpdatedBy = updatedByID
+
+	_, err = e.Id(notification.ID).Update(notification)
+	return err
+}
+
+func getIssueNotification(e Engine, userID, issueID int64) (*Notification, error) {
+	notification := new(Notification)
+	_, err := e.
+		Where("user_id = ?", userID).
+		And("issue_id = ?", issueID).
+		Get(notification)
+	return notification, err
+}
+
+// NotificationsForUser returns notifications for a given user and status
+func NotificationsForUser(user *User, status NotificationStatus) ([]*Notification, error) {
+	return notificationsForUser(x, user, status)
+}
+func notificationsForUser(e Engine, user *User, status NotificationStatus) (notifications []*Notification, err error) {
+	err = e.
+		Where("user_id = ?", user.ID).
+		And("status = ?", status).
+		OrderBy("updated_unix DESC").
+		Find(&notifications)
+	return
+}
+
+// GetRepo returns the repo of the notification
+func (n *Notification) GetRepo() (*Repository, error) {
+	n.Repository = new(Repository)
+	_, err := x.
+		Where("id = ?", n.RepoID).
+		Get(n.Repository)
+	return n.Repository, err
+}
+
+// GetIssue returns the issue of the notification
+func (n *Notification) GetIssue() (*Issue, error) {
+	n.Issue = new(Issue)
+	_, err := x.
+		Where("id = ?", n.IssueID).
+		Get(n.Issue)
+	return n.Issue, err
+}
+
+// GetNotificationReadCount returns the notification read count for user
+func GetNotificationReadCount(user *User) (int64, error) {
+	return GetNotificationCount(user, NotificationStatusRead)
+}
+
+// GetNotificationUnreadCount returns the notification unread count for user
+func GetNotificationUnreadCount(user *User) (int64, error) {
+	return GetNotificationCount(user, NotificationStatusUnread)
+}
+
+// GetNotificationCount returns the notification count for user
+func GetNotificationCount(user *User, status NotificationStatus) (int64, error) {
+	return getNotificationCount(x, user, status)
+}
+
+func getNotificationCount(e Engine, user *User, status NotificationStatus) (count int64, err error) {
+	count, err = e.
+		Where("user_id = ?", user.ID).
+		And("status = ?", status).
+		Count(&Notification{})
+	return
+}
+
+func setNotificationStatusRead(e Engine, userID, issueID int64) error {
+	notification, err := getIssueNotification(e, userID, issueID)
+	// ignore if not exists
+	if err != nil {
+		return nil
+	}
+
+	notification.Status = NotificationStatusRead
+
+	_, err = e.Id(notification.ID).Update(notification)
+	return err
+}
diff --git a/modules/notification/notification.go b/modules/notification/notification.go
new file mode 100644
index 000000000..ffe885240
--- /dev/null
+++ b/modules/notification/notification.go
@@ -0,0 +1,50 @@
+// Copyright 2016 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package notification
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+)
+
+type (
+	notificationService struct {
+		issueQueue chan issueNotificationOpts
+	}
+
+	issueNotificationOpts struct {
+		issue                *models.Issue
+		notificationAuthorID int64
+	}
+)
+
+var (
+	// Service is the notification service
+	Service = &notificationService{
+		issueQueue: make(chan issueNotificationOpts, 100),
+	}
+)
+
+func init() {
+	go Service.Run()
+}
+
+func (ns *notificationService) Run() {
+	for {
+		select {
+		case opts := <-ns.issueQueue:
+			if err := models.CreateOrUpdateIssueNotifications(opts.issue, opts.notificationAuthorID); err != nil {
+				log.Error(4, "Was unable to create issue notification: %v", err)
+			}
+		}
+	}
+}
+
+func (ns *notificationService) NotifyIssue(issue *models.Issue, notificationAuthorID int64) {
+	ns.issueQueue <- issueNotificationOpts{
+		issue,
+		notificationAuthorID,
+	}
+}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 6eeb5bef5..049eac06c 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markdown"
+	"code.gitea.io/gitea/modules/notification"
 	"code.gitea.io/gitea/modules/setting"
 )
 
@@ -467,6 +468,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
 		return
 	}
 
+	notification.Service.NotifyIssue(issue, ctx.User.ID)
+
 	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
 }
@@ -931,6 +934,8 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
 		return
 	}
 
+	notification.Service.NotifyIssue(issue, ctx.User.ID)
+
 	log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
 }