diff --git a/api/src/main/java/com/databasir/api/DocumentDiscussionController.java b/api/src/main/java/com/databasir/api/DocumentDiscussionController.java
index dcd323f..50f76bb 100644
--- a/api/src/main/java/com/databasir/api/DocumentDiscussionController.java
+++ b/api/src/main/java/com/databasir/api/DocumentDiscussionController.java
@@ -3,10 +3,10 @@ package com.databasir.api;
 import com.databasir.api.config.security.DatabasirUserDetails;
 import com.databasir.common.JsonData;
 import com.databasir.core.domain.log.annotation.Operation;
-import com.databasir.core.domain.remark.data.DiscussionCreateRequest;
-import com.databasir.core.domain.remark.data.DiscussionListCondition;
-import com.databasir.core.domain.remark.data.DiscussionResponse;
-import com.databasir.core.domain.remark.service.DocumentDiscussionService;
+import com.databasir.core.domain.discussion.data.DiscussionCreateRequest;
+import com.databasir.core.domain.discussion.data.DiscussionListCondition;
+import com.databasir.core.domain.discussion.data.DiscussionResponse;
+import com.databasir.core.domain.discussion.service.DocumentDiscussionService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
diff --git a/api/src/main/java/com/databasir/api/UserController.java b/api/src/main/java/com/databasir/api/UserController.java
index 9203894..f464781 100644
--- a/api/src/main/java/com/databasir/api/UserController.java
+++ b/api/src/main/java/com/databasir/api/UserController.java
@@ -1,5 +1,6 @@
 package com.databasir.api;
 
+import com.databasir.api.common.LoginUserContext;
 import com.databasir.api.validator.UserOperationValidator;
 import com.databasir.common.JsonData;
 import com.databasir.common.exception.Forbidden;
@@ -7,6 +8,7 @@ import com.databasir.core.domain.DomainErrors;
 import com.databasir.core.domain.log.annotation.Operation;
 import com.databasir.core.domain.user.data.*;
 import com.databasir.core.domain.user.service.UserService;
+import com.databasir.core.infrastructure.event.EventPublisher;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -27,6 +29,8 @@ public class UserController {
 
     private final UserOperationValidator userOperationValidator;
 
+    private final EventPublisher eventPublisher;
+
     @GetMapping(Routes.User.LIST)
     public JsonData<Page<UserPageResponse>> list(@PageableDefault(sort = "id", direction = Sort.Direction.DESC)
                                                          Pageable pageable,
@@ -60,7 +64,7 @@ public class UserController {
     @PreAuthorize("hasAnyAuthority('SYS_OWNER')")
     @Operation(module = Operation.Modules.USER, name = "创建用户")
     public JsonData<Void> create(@RequestBody @Valid UserCreateRequest request) {
-        userService.create(request);
+        userService.create(request, UserSource.MANUAL);
         return JsonData.ok();
     }
 
@@ -73,7 +77,8 @@ public class UserController {
     @PreAuthorize("hasAnyAuthority('SYS_OWNER')")
     @Operation(module = Operation.Modules.USER, name = "重置用户密码", involvedUserId = "#userId")
     public JsonData<Void> renewPassword(@PathVariable Integer userId) {
-        userService.renewPassword(userId);
+        Integer operatorUserId = LoginUserContext.getLoginUserId();
+        userService.renewPassword(operatorUserId, userId);
         return JsonData.ok();
     }
 
diff --git a/api/src/main/java/com/databasir/api/common/LoginUserContext.java b/api/src/main/java/com/databasir/api/common/LoginUserContext.java
new file mode 100644
index 0000000..28a010c
--- /dev/null
+++ b/api/src/main/java/com/databasir/api/common/LoginUserContext.java
@@ -0,0 +1,14 @@
+package com.databasir.api.common;
+
+import com.databasir.api.config.security.DatabasirUserDetails;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+public class LoginUserContext {
+
+    public static Integer getLoginUserId() {
+        DatabasirUserDetails principal = (DatabasirUserDetails) SecurityContextHolder.getContext()
+                .getAuthentication()
+                .getPrincipal();
+        return principal.getUserPojo().getId();
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/app/OpenAuthAppService.java b/core/src/main/java/com/databasir/core/domain/app/OpenAuthAppService.java
index c2b4c17..d34b834 100644
--- a/core/src/main/java/com/databasir/core/domain/app/OpenAuthAppService.java
+++ b/core/src/main/java/com/databasir/core/domain/app/OpenAuthAppService.java
@@ -51,7 +51,7 @@ public class OpenAuthAppService {
                     user.setAvatar(result.getAvatar());
                     user.setEnabled(true);
                     user.setPassword(UUID.randomUUID().toString().substring(0, 6));
-                    Integer id = userService.create(user);
+                    Integer id = userService.create(user, registrationId);
                     return userService.get(id);
                 });
     }
diff --git a/core/src/main/java/com/databasir/core/domain/remark/converter/DiscussionResponseConverter.java b/core/src/main/java/com/databasir/core/domain/discussion/converter/DiscussionResponseConverter.java
similarity index 81%
rename from core/src/main/java/com/databasir/core/domain/remark/converter/DiscussionResponseConverter.java
rename to core/src/main/java/com/databasir/core/domain/discussion/converter/DiscussionResponseConverter.java
index 1aafc2a..4532b83 100644
--- a/core/src/main/java/com/databasir/core/domain/remark/converter/DiscussionResponseConverter.java
+++ b/core/src/main/java/com/databasir/core/domain/discussion/converter/DiscussionResponseConverter.java
@@ -1,6 +1,6 @@
-package com.databasir.core.domain.remark.converter;
+package com.databasir.core.domain.discussion.converter;
 
-import com.databasir.core.domain.remark.data.DiscussionResponse;
+import com.databasir.core.domain.discussion.data.DiscussionResponse;
 import com.databasir.dao.tables.pojos.DocumentDiscussionPojo;
 import com.databasir.dao.tables.pojos.UserPojo;
 import org.mapstruct.Mapper;
diff --git a/core/src/main/java/com/databasir/core/domain/remark/data/DiscussionCreateRequest.java b/core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionCreateRequest.java
similarity index 84%
rename from core/src/main/java/com/databasir/core/domain/remark/data/DiscussionCreateRequest.java
rename to core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionCreateRequest.java
index 90ccb6d..6df0e0a 100644
--- a/core/src/main/java/com/databasir/core/domain/remark/data/DiscussionCreateRequest.java
+++ b/core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionCreateRequest.java
@@ -1,4 +1,4 @@
-package com.databasir.core.domain.remark.data;
+package com.databasir.core.domain.discussion.data;
 
 import lombok.Data;
 
diff --git a/core/src/main/java/com/databasir/core/domain/remark/data/DiscussionListCondition.java b/core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionListCondition.java
similarity index 95%
rename from core/src/main/java/com/databasir/core/domain/remark/data/DiscussionListCondition.java
rename to core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionListCondition.java
index 5748201..b912e39 100644
--- a/core/src/main/java/com/databasir/core/domain/remark/data/DiscussionListCondition.java
+++ b/core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionListCondition.java
@@ -1,4 +1,4 @@
-package com.databasir.core.domain.remark.data;
+package com.databasir.core.domain.discussion.data;
 
 import com.databasir.dao.Tables;
 import lombok.Data;
diff --git a/core/src/main/java/com/databasir/core/domain/remark/data/DiscussionResponse.java b/core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionResponse.java
similarity index 89%
rename from core/src/main/java/com/databasir/core/domain/remark/data/DiscussionResponse.java
rename to core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionResponse.java
index 4703b23..4ed5d16 100644
--- a/core/src/main/java/com/databasir/core/domain/remark/data/DiscussionResponse.java
+++ b/core/src/main/java/com/databasir/core/domain/discussion/data/DiscussionResponse.java
@@ -1,4 +1,4 @@
-package com.databasir.core.domain.remark.data;
+package com.databasir.core.domain.discussion.data;
 
 import lombok.Data;
 
diff --git a/core/src/main/java/com/databasir/core/domain/discussion/event/DiscussionCreated.java b/core/src/main/java/com/databasir/core/domain/discussion/event/DiscussionCreated.java
new file mode 100644
index 0000000..695c595
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/discussion/event/DiscussionCreated.java
@@ -0,0 +1,28 @@
+package com.databasir.core.domain.discussion.event;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+@Data
+public class DiscussionCreated {
+
+    private Integer discussionId;
+
+    private Integer createByUserId;
+
+    private String content;
+
+    private Integer projectId;
+
+    private String tableName;
+
+    private String columnName;
+
+    private LocalDateTime createAt;
+
+    public Optional<String> getColumnName() {
+        return Optional.of(columnName);
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/discussion/event/converter/DiscussionEventConverter.java b/core/src/main/java/com/databasir/core/domain/discussion/event/converter/DiscussionEventConverter.java
new file mode 100644
index 0000000..120e7c5
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/discussion/event/converter/DiscussionEventConverter.java
@@ -0,0 +1,14 @@
+package com.databasir.core.domain.discussion.event.converter;
+
+import com.databasir.core.domain.discussion.event.DiscussionCreated;
+import com.databasir.dao.tables.pojos.DocumentDiscussionPojo;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = "spring")
+public interface DiscussionEventConverter {
+
+    @Mapping(target = "createByUserId", source = "pojo.userId")
+    DiscussionCreated of(DocumentDiscussionPojo pojo, Integer discussionId);
+
+}
diff --git a/core/src/main/java/com/databasir/core/domain/remark/service/DocumentDiscussionService.java b/core/src/main/java/com/databasir/core/domain/discussion/service/DocumentDiscussionService.java
similarity index 72%
rename from core/src/main/java/com/databasir/core/domain/remark/service/DocumentDiscussionService.java
rename to core/src/main/java/com/databasir/core/domain/discussion/service/DocumentDiscussionService.java
index 49edae4..89e5c9b 100644
--- a/core/src/main/java/com/databasir/core/domain/remark/service/DocumentDiscussionService.java
+++ b/core/src/main/java/com/databasir/core/domain/discussion/service/DocumentDiscussionService.java
@@ -1,10 +1,13 @@
-package com.databasir.core.domain.remark.service;
+package com.databasir.core.domain.discussion.service;
 
 import com.databasir.common.exception.Forbidden;
-import com.databasir.core.domain.remark.converter.DiscussionResponseConverter;
-import com.databasir.core.domain.remark.data.DiscussionCreateRequest;
-import com.databasir.core.domain.remark.data.DiscussionListCondition;
-import com.databasir.core.domain.remark.data.DiscussionResponse;
+import com.databasir.core.domain.discussion.converter.DiscussionResponseConverter;
+import com.databasir.core.domain.discussion.data.DiscussionCreateRequest;
+import com.databasir.core.domain.discussion.data.DiscussionListCondition;
+import com.databasir.core.domain.discussion.data.DiscussionResponse;
+import com.databasir.core.domain.discussion.event.DiscussionCreated;
+import com.databasir.core.domain.discussion.event.converter.DiscussionEventConverter;
+import com.databasir.core.infrastructure.event.EventPublisher;
 import com.databasir.dao.impl.DocumentDiscussionDao;
 import com.databasir.dao.impl.ProjectDao;
 import com.databasir.dao.impl.UserDao;
@@ -32,6 +35,10 @@ public class DocumentDiscussionService {
 
     private final DiscussionResponseConverter discussionResponseConverter;
 
+    private final DiscussionEventConverter discussionEventConverter;
+
+    private final EventPublisher eventPublisher;
+
     public void deleteById(Integer groupId,
                            Integer projectId,
                            Integer discussionId) {
@@ -57,9 +64,9 @@ public class DocumentDiscussionService {
                     .stream()
                     .collect(Collectors.toMap(UserPojo::getId, Function.identity()));
             return data
-                    .map(dicussionPojo -> {
-                        UserPojo userPojo = userMapById.get(dicussionPojo.getUserId());
-                        return discussionResponseConverter.of(dicussionPojo, userPojo);
+                    .map(discussionPojo -> {
+                        UserPojo userPojo = userMapById.get(discussionPojo.getUserId());
+                        return discussionResponseConverter.of(discussionPojo, userPojo);
                     });
         } else {
             throw new Forbidden();
@@ -74,7 +81,9 @@ public class DocumentDiscussionService {
             pojo.setContent(request.getContent());
             pojo.setTableName(request.getTableName());
             pojo.setColumnName(request.getColumnName());
-            documentDiscussionDao.insertAndReturnId(pojo);
+            Integer discussionId = documentDiscussionDao.insertAndReturnId(pojo);
+            DiscussionCreated event = discussionEventConverter.of(pojo, discussionId);
+            eventPublisher.publish(event);
         } else {
             throw new Forbidden();
         }
diff --git a/core/src/main/java/com/databasir/core/domain/document/event/DocumentUpdated.java b/core/src/main/java/com/databasir/core/domain/document/event/DocumentUpdated.java
new file mode 100644
index 0000000..637685f
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/document/event/DocumentUpdated.java
@@ -0,0 +1,30 @@
+package com.databasir.core.domain.document.event;
+
+import com.databasir.core.diff.data.RootDiff;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Optional;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DocumentUpdated {
+
+    private RootDiff diff;
+
+    private Long newVersion;
+
+    private Long oldVersion;
+
+    private Integer projectId;
+
+    public Optional<Long> getOldVersion() {
+        return Optional.ofNullable(oldVersion);
+    }
+
+    public Optional<RootDiff> getDiff() {
+        return Optional.ofNullable(diff);
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
index 571259b..ed7ad48 100644
--- a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
+++ b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
@@ -14,10 +14,12 @@ import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
 import com.databasir.core.domain.document.data.DatabaseDocumentSimpleResponse;
 import com.databasir.core.domain.document.data.DatabaseDocumentVersionResponse;
 import com.databasir.core.domain.document.data.TableDocumentResponse;
+import com.databasir.core.domain.document.event.DocumentUpdated;
 import com.databasir.core.domain.document.generator.DocumentFileGenerator;
 import com.databasir.core.domain.document.generator.DocumentFileType;
 import com.databasir.core.infrastructure.connection.DatabaseConnectionService;
 import com.databasir.core.infrastructure.converter.JsonConverter;
+import com.databasir.core.infrastructure.event.EventPublisher;
 import com.databasir.core.meta.data.DatabaseMeta;
 import com.databasir.dao.impl.*;
 import com.databasir.dao.tables.pojos.*;
@@ -84,6 +86,8 @@ public class DocumentService {
 
     private final List<DocumentFileGenerator> documentFileGenerators;
 
+    private final EventPublisher eventPublisher;
+
     @Transactional
     public void syncByProjectId(Integer projectId) {
         projectDao.selectOptionalById(projectId)
@@ -103,9 +107,12 @@ public class DocumentService {
             Integer previousDocumentId = original.getId();
             // archive old version
             databaseDocumentDao.updateIsArchiveById(previousDocumentId, true);
-            saveNewDocument(current, original.getVersion() + 1, original.getProjectId());
+            Long version = original.getVersion();
+            saveNewDocument(current, version + 1, original.getProjectId());
+            eventPublisher.publish(new DocumentUpdated(diff, version + 1, version, projectId));
         } else {
             saveNewDocument(current, 1L, projectId);
+            eventPublisher.publish(new DocumentUpdated(null, 1L, null, projectId));
         }
     }
 
diff --git a/core/src/main/java/com/databasir/core/domain/user/data/UserCreateRequest.java b/core/src/main/java/com/databasir/core/domain/user/data/UserCreateRequest.java
index ae68290..8e74f5b 100644
--- a/core/src/main/java/com/databasir/core/domain/user/data/UserCreateRequest.java
+++ b/core/src/main/java/com/databasir/core/domain/user/data/UserCreateRequest.java
@@ -25,4 +25,5 @@ public class UserCreateRequest {
     @NotNull
     private Boolean enabled;
 
+
 }
diff --git a/core/src/main/java/com/databasir/core/domain/user/data/UserSource.java b/core/src/main/java/com/databasir/core/domain/user/data/UserSource.java
new file mode 100644
index 0000000..1b2ea7f
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/user/data/UserSource.java
@@ -0,0 +1,12 @@
+package com.databasir.core.domain.user.data;
+
+import java.util.Objects;
+
+public interface UserSource {
+
+    String MANUAL = "manual";
+
+    static boolean isManual(String source) {
+        return Objects.equals(source, MANUAL);
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/user/event/UserCreated.java b/core/src/main/java/com/databasir/core/domain/user/event/UserCreated.java
new file mode 100644
index 0000000..cdd1f2c
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/user/event/UserCreated.java
@@ -0,0 +1,23 @@
+package com.databasir.core.domain.user.event;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@Builder
+@ToString
+public class UserCreated {
+
+    private Integer userId;
+
+    private String nickname;
+
+    private String email;
+
+    private String username;
+
+    private String rawPassword;
+
+    private String source;
+}
diff --git a/core/src/main/java/com/databasir/core/domain/user/event/UserPasswordRenewed.java b/core/src/main/java/com/databasir/core/domain/user/event/UserPasswordRenewed.java
new file mode 100644
index 0000000..ca9a6f3
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/user/event/UserPasswordRenewed.java
@@ -0,0 +1,21 @@
+package com.databasir.core.domain.user.event;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+public class UserPasswordRenewed {
+
+    private Integer renewByUserId;
+
+    private String nickname;
+
+    private String email;
+
+    private String newPassword;
+
+    private LocalDateTime renewTime;
+}
diff --git a/core/src/main/java/com/databasir/core/domain/user/event/converter/UserEventConverter.java b/core/src/main/java/com/databasir/core/domain/user/event/converter/UserEventConverter.java
new file mode 100644
index 0000000..be92577
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/user/event/converter/UserEventConverter.java
@@ -0,0 +1,21 @@
+package com.databasir.core.domain.user.event.converter;
+
+import com.databasir.core.domain.user.event.UserCreated;
+import com.databasir.core.domain.user.event.UserPasswordRenewed;
+import com.databasir.dao.tables.pojos.UserPojo;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+import java.time.LocalDateTime;
+
+@Mapper(componentModel = "spring")
+public interface UserEventConverter {
+
+    UserPasswordRenewed userPasswordRenewed(UserPojo pojo,
+                                            Integer renewByUserId,
+                                            LocalDateTime renewTime,
+                                            String newPassword);
+
+    @Mapping(target = "userId", source = "userId")
+    UserCreated userCreated(UserPojo pojo, String source, String rawPassword, Integer userId);
+}
diff --git a/core/src/main/java/com/databasir/core/domain/user/service/UserService.java b/core/src/main/java/com/databasir/core/domain/user/service/UserService.java
index fc2639a..a4f05e6 100644
--- a/core/src/main/java/com/databasir/core/domain/user/service/UserService.java
+++ b/core/src/main/java/com/databasir/core/domain/user/service/UserService.java
@@ -4,8 +4,14 @@ import com.databasir.core.domain.DomainErrors;
 import com.databasir.core.domain.user.converter.UserPojoConverter;
 import com.databasir.core.domain.user.converter.UserResponseConverter;
 import com.databasir.core.domain.user.data.*;
-import com.databasir.core.infrastructure.mail.MailSender;
-import com.databasir.dao.impl.*;
+import com.databasir.core.domain.user.event.UserCreated;
+import com.databasir.core.domain.user.event.UserPasswordRenewed;
+import com.databasir.core.domain.user.event.converter.UserEventConverter;
+import com.databasir.core.infrastructure.event.EventPublisher;
+import com.databasir.dao.impl.GroupDao;
+import com.databasir.dao.impl.LoginDao;
+import com.databasir.dao.impl.UserDao;
+import com.databasir.dao.impl.UserRoleDao;
 import com.databasir.dao.tables.pojos.GroupPojo;
 import com.databasir.dao.tables.pojos.UserPojo;
 import com.databasir.dao.tables.pojos.UserRolePojo;
@@ -17,6 +23,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
 import java.util.*;
 
 import static java.util.stream.Collectors.*;
@@ -31,15 +38,15 @@ public class UserService {
 
     private final GroupDao groupDao;
 
-    private final SysMailDao sysMailDao;
-
     private final LoginDao loginDao;
 
     private final UserPojoConverter userPojoConverter;
 
     private final UserResponseConverter userResponseConverter;
 
-    private final MailSender mailSender;
+    private final UserEventConverter userEventConverter;
+
+    private final EventPublisher eventPublisher;
 
     @SuppressWarnings("checkstyle:all")
     private BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
@@ -64,14 +71,18 @@ public class UserService {
     }
 
     @Transactional
-    public Integer create(UserCreateRequest userCreateRequest) {
+    public Integer create(UserCreateRequest userCreateRequest, String source) {
         userDao.selectByEmailOrUsername(userCreateRequest.getUsername()).ifPresent(data -> {
             throw DomainErrors.USERNAME_OR_EMAIL_DUPLICATE.exception();
         });
         String hashedPassword = bCryptPasswordEncoder.encode(userCreateRequest.getPassword());
         UserPojo pojo = userPojoConverter.of(userCreateRequest, hashedPassword);
         try {
-            return userDao.insertAndReturnId(pojo);
+            Integer id = userDao.insertAndReturnId(pojo);
+            // publish event
+            UserCreated event = userEventConverter.userCreated(pojo, source, userCreateRequest.getPassword(), id);
+            eventPublisher.publish(event);
+            return id;
         } catch (DuplicateKeyException e) {
             throw DomainErrors.USERNAME_OR_EMAIL_DUPLICATE.exception();
         }
@@ -106,19 +117,20 @@ public class UserService {
     }
 
     @Transactional
-    public String renewPassword(Integer userId) {
-        UserPojo userPojo = userDao.selectById(userId);
+    public String renewPassword(Integer renewByUserId, Integer userId) {
+        UserPojo pojo = userDao.selectById(userId);
         String randomPassword = UUID.randomUUID().toString()
                 .replace("-", "")
                 .substring(0, 8);
         String hashedPassword = bCryptPasswordEncoder.encode(randomPassword);
         userDao.updatePassword(userId, hashedPassword);
-        sysMailDao.selectOptionTopOne()
-                .ifPresent(mailPojo -> {
-                    String subject = "Databasir 密码重置提醒";
-                    String message = "您的密码已被重置,新密码为:" + randomPassword;
-                    mailSender.send(mailPojo, userPojo.getEmail(), subject, message);
-                });
+
+        // publish event
+        UserPasswordRenewed event = userEventConverter.userPasswordRenewed(pojo,
+                renewByUserId,
+                LocalDateTime.now(),
+                randomPassword);
+        eventPublisher.publish(event);
         return randomPassword;
     }
 
diff --git a/core/src/main/java/com/databasir/core/infrastructure/event/EventPublisher.java b/core/src/main/java/com/databasir/core/infrastructure/event/EventPublisher.java
new file mode 100644
index 0000000..4bc06b6
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/infrastructure/event/EventPublisher.java
@@ -0,0 +1,16 @@
+package com.databasir.core.infrastructure.event;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class EventPublisher {
+
+    private final ApplicationEventPublisher applicationEventPublisher;
+
+    public void publish(Object event) {
+        applicationEventPublisher.publishEvent(event);
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/DiscussionEventSubscriber.java b/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/DiscussionEventSubscriber.java
new file mode 100644
index 0000000..0651433
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/DiscussionEventSubscriber.java
@@ -0,0 +1,18 @@
+package com.databasir.core.infrastructure.event.subscriber;
+
+import com.databasir.core.domain.discussion.event.DiscussionCreated;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class DiscussionEventSubscriber {
+
+    @EventListener(classes = DiscussionCreated.class)
+    public void onDiscussionCreated(DiscussionCreated created) {
+        // TODO notification group member who subscribe this event
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/DocumentEventSubscriber.java b/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/DocumentEventSubscriber.java
new file mode 100644
index 0000000..87a5d4a
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/DocumentEventSubscriber.java
@@ -0,0 +1,84 @@
+package com.databasir.core.infrastructure.event.subscriber;
+
+import com.databasir.core.diff.data.DiffType;
+import com.databasir.core.diff.data.RootDiff;
+import com.databasir.core.domain.document.event.DocumentUpdated;
+import com.databasir.core.infrastructure.mail.MailSender;
+import com.databasir.dao.impl.ProjectDao;
+import com.databasir.dao.impl.SysMailDao;
+import com.databasir.dao.impl.UserDao;
+import com.databasir.dao.impl.UserRoleDao;
+import com.databasir.dao.tables.pojos.ProjectPojo;
+import com.databasir.dao.tables.pojos.UserPojo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class DocumentEventSubscriber {
+
+    private final ProjectDao projectDao;
+
+    private final MailSender mailSender;
+
+    private final UserRoleDao userRoleDao;
+
+    private final UserDao userDao;
+
+    private final SysMailDao sysMailDao;
+
+    @EventListener(classes = DocumentUpdated.class)
+    public void onDocumentUpdated(DocumentUpdated created) {
+        ProjectPojo project = projectDao.selectById(created.getProjectId());
+        List<String> to = userDao.selectEnabledGroupMembers(project.getGroupId())
+                .stream()
+                .map(UserPojo::getEmail)
+                .filter(mail -> mail.contains("@"))
+                .collect(Collectors.toList());
+        sysMailDao.selectOptionTopOne().ifPresent(mail -> {
+            String subject = project.getName() + " 文档有新的版本";
+            String message = created.getDiff()
+                    .map(diff -> build(diff))
+                    .orElseGet(() -> "首次文档同步常规");
+            mailSender.batchSend(mail, to, subject, message);
+        });
+    }
+
+    private String build(RootDiff diff) {
+        if (diff.getDiffType() == DiffType.NONE) {
+            return "";
+        } else {
+            return diff.getFields()
+                    .stream()
+                    .filter(field -> field.getFieldName().equals("tables"))
+                    .flatMap(f -> f.getFields().stream())
+                    .map(table -> {
+                        String tableName = table.getFieldName();
+                        String change = toDescription(table.getDiffType());
+                        return tableName + " " + change;
+                    })
+                    .collect(Collectors.joining("\n"));
+        }
+    }
+
+    private String toDescription(DiffType diffType) {
+        switch (diffType) {
+            case NONE:
+                return "无变化";
+            case ADDED:
+                return "新增";
+            case REMOVED:
+                return "删除";
+            case MODIFIED:
+                return "修改";
+            default:
+                return diffType.name();
+        }
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/UserEventSubscriber.java b/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/UserEventSubscriber.java
new file mode 100644
index 0000000..adc01b9
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/infrastructure/event/subscriber/UserEventSubscriber.java
@@ -0,0 +1,61 @@
+package com.databasir.core.infrastructure.event.subscriber;
+
+import com.databasir.core.domain.user.data.UserSource;
+import com.databasir.core.domain.user.event.UserCreated;
+import com.databasir.core.domain.user.event.UserPasswordRenewed;
+import com.databasir.core.infrastructure.mail.MailSender;
+import com.databasir.dao.impl.SysMailDao;
+import com.databasir.dao.impl.UserDao;
+import com.databasir.dao.tables.pojos.UserPojo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * TODO use html template instead of simple message
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class UserEventSubscriber {
+
+    private final MailSender mailSender;
+
+    private final SysMailDao sysMailDao;
+
+    private final UserDao userDao;
+
+    @EventListener(classes = UserPasswordRenewed.class)
+    public void onPasswordRenewed(UserPasswordRenewed event) {
+        UserPojo operator = userDao.selectById(event.getRenewByUserId());
+        sysMailDao.selectOptionTopOne()
+                .ifPresent(mailPojo -> {
+                    String renewBy = operator.getNickname();
+                    String subject = "Databasir 密码重置提醒";
+                    String message = String.format("Hi %s,\r\n 您的密码已被 %s 重置,新密码为 %s",
+                            event.getNickname(),
+                            renewBy,
+                            event.getNewPassword());
+                    mailSender.send(mailPojo, event.getEmail(), subject, message);
+                });
+    }
+
+    @EventListener(classes = UserCreated.class)
+    public void onUserCreated(UserCreated event) {
+        String subject = "Databasir 账户创建成功";
+        String message;
+        if (UserSource.isManual(event.getSource())) {
+            message = String.format("Hi %s\r\n您的 Databasir 账户已创建成功,用户名:%s,密码:%s",
+                    event.getNickname(), event.getUsername(), event.getRawPassword());
+        } else {
+            message = String.format("Hi %s\r\n您的 Databasir 账户已创建成功,用户名:%s",
+                    event.getNickname(), event.getUsername());
+        }
+        sysMailDao.selectOptionTopOne()
+                .ifPresent(mailPojo -> {
+                    mailSender.send(mailPojo, event.getEmail(), subject, message);
+                });
+    }
+
+}
diff --git a/core/src/main/java/com/databasir/core/infrastructure/mail/MailSender.java b/core/src/main/java/com/databasir/core/infrastructure/mail/MailSender.java
index a3d835b..bc53664 100644
--- a/core/src/main/java/com/databasir/core/infrastructure/mail/MailSender.java
+++ b/core/src/main/java/com/databasir/core/infrastructure/mail/MailSender.java
@@ -7,21 +7,26 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
 import org.springframework.stereotype.Component;
 
 import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
 
 @Component
 public class MailSender {
 
-    public void send(SysMailPojo mail, String to, String subject, String content) {
+    public void batchSend(SysMailPojo mail, Collection<String> to, String subject, String content) {
         SimpleMailMessage message = new SimpleMailMessage();
         message.setFrom(mail.getUsername());
-        message.setTo(to);
+        message.setTo(to.toArray(new String[0]));
         message.setSubject(subject);
         message.setText(content);
-
         JavaMailSender sender = initJavaMailSender(mail);
         sender.send(message);
     }
 
+    public void send(SysMailPojo mail, String to, String subject, String content) {
+        this.batchSend(mail, Collections.singleton(to), subject, content);
+    }
+
     private JavaMailSender initJavaMailSender(SysMailPojo properties) {
         JavaMailSenderImpl sender = new JavaMailSenderImpl();
         sender.setHost(properties.getSmtpHost());
diff --git a/dao/src/main/java/com/databasir/dao/impl/UserDao.java b/dao/src/main/java/com/databasir/dao/impl/UserDao.java
index 6463630..cd8426b 100644
--- a/dao/src/main/java/com/databasir/dao/impl/UserDao.java
+++ b/dao/src/main/java/com/databasir/dao/impl/UserDao.java
@@ -78,6 +78,13 @@ public class UserDao extends BaseDao<UserPojo> {
                 .fetchOptionalInto(UserPojo.class);
     }
 
+    public List<UserPojo> selectEnabledGroupMembers(Integer groupId) {
+        return dslContext.select(USER.fields()).from(USER)
+                .innerJoin(USER_ROLE).on(USER.ID.eq(USER_ROLE.USER_ID))
+                .where(USER_ROLE.GROUP_ID.eq(groupId).and(USER.ENABLED.eq(true)))
+                .fetchInto(UserPojo.class);
+    }
+
     public Page<GroupMemberDetailPojo> selectGroupMembers(Integer groupId, Pageable request, Condition condition) {
         // total
         Integer count = dslContext