diff --git a/.env b/.env
index 82b15d176cf462dcfa34cb48b3330b8ed01c8094..3958664c25ea7dec0d286c76fdaa96d87af56610 100644
--- a/.env
+++ b/.env
@@ -4,7 +4,7 @@ PORT=8080
 LOGTAIL_TOKEN=
 HTTP_TIMEOUT_SEC=2
 LOG_FLUSH_INTERVAL_MS=1000
-DB_STRING="host=localhost user=postgres password=postgres dbname=ocwdb port=5432 sslmode=disable TimeZone=Asia/Shanghai"
+DB_STRING="host=database user=ocw password=ocw dbname=ocw-db port=5432 sslmode=disable TimeZone=Asia/Shanghai"
 SMTP_USERNAME="noreply@ocw.id"
 SMTP_SERVER=localhost
 SMTP_PORT=1025
\ No newline at end of file
diff --git a/handler/di.go b/handler/di.go
index ef48f67cd3b02eaa4df9d34dc17fb09fc317824b..835b134c53081d8bfb44e80ddda91bb03f7f7eeb 100644
--- a/handler/di.go
+++ b/handler/di.go
@@ -5,6 +5,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/handler/admin"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/auth"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/common"
+	"gitlab.informatika.org/ocw/ocw-backend/handler/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/swagger"
 )
 
@@ -24,4 +25,8 @@ var HandlerSet = wire.NewSet(
 	// Auth
 	wire.Struct(new(auth.AuthHandlerImpl), "*"),
 	wire.Bind(new(auth.AuthHandler), new(*auth.AuthHandlerImpl)),
+
+	// Reset
+	wire.Struct(new(reset.ResetHandlerImpl), "*"),
+	wire.Bind(new(reset.ResetHandler), new(*reset.ResetHandlerImpl)),
 )
diff --git a/handler/reset/confirm.go b/handler/reset/confirm.go
new file mode 100644
index 0000000000000000000000000000000000000000..e68ba0568c58bacb594068f183d79c7d5a20f94b
--- /dev/null
+++ b/handler/reset/confirm.go
@@ -0,0 +1,91 @@
+package reset
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/go-playground/validator/v10"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm"
+)
+
+func (rs ResetHandlerImpl) Confirm(w http.ResponseWriter, r *http.Request) {
+	payload := confirm.ConfirmRequestPayload{}
+	validate := validator.New()
+
+	// Validate payload
+	if r.Header.Get("Content-Type") != "application/json" {
+		payload := rs.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload)
+		return
+	}
+
+	if err := rs.HttpUtil.ParseJson(r, &payload); err != nil {
+		payload := rs.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error())
+		rs.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload)
+		return
+	}
+
+	if err := validate.Struct(payload); err != nil {
+		if _, ok := err.(*validator.InvalidValidationError); ok {
+			payload := rs.WrapperUtil.ErrorResponseWrap(err.Error(), nil)
+			rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			return
+		}
+
+		errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors))
+		payload := rs.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload)
+		rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	// Confirm Valid Website Token
+	confirmTokenHeader := r.Header.Get("Authorization")
+
+	if confirmTokenHeader == "" {
+		payload := rs.WrapperUtil.ErrorResponseWrap("token is required", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+		return
+	}
+
+	token := strings.Split(confirmTokenHeader, " ")
+
+	if len(token) != 2 {
+		payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	if token[0] != "Bearer" {
+		payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	// Service to handle
+	payload.ConfirmToken = token[1]
+	err := rs.ResetService.Confirm(payload)
+
+	if err != nil {
+		if strings.Contains(err.Error(), "expired/not exist") {
+			err = web.NewResponseErrorFromError(err, web.EmailNotExist)
+		}
+
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := rs.WrapperUtil.ErrorResponseWrap("email was not found", respErr)
+			rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		} else {
+			rs.Logger.Error(
+				fmt.Sprintf("[RESET] some error happened when requesting password reset: %s", err.Error()),
+			)
+			payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil)
+	rs.HttpUtil.WriteSuccessJson(w, responsePayload)
+}
\ No newline at end of file
diff --git a/handler/reset/handler.go b/handler/reset/handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..b9ee5874d324580d9f1ceebeb7c296c0aa1b09a4
--- /dev/null
+++ b/handler/reset/handler.go
@@ -0,0 +1,15 @@
+package reset
+
+import (
+	"gitlab.informatika.org/ocw/ocw-backend/service/reset"
+	"gitlab.informatika.org/ocw/ocw-backend/service/logger"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/httputil"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/wrapper"
+)
+
+type ResetHandlerImpl struct {
+	reset.ResetService
+	httputil.HttpUtil
+	wrapper.WrapperUtil
+	logger.Logger
+}
diff --git a/handler/reset/request.go b/handler/reset/request.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2dc0d450e58ca4c26d33f574a403e3e3269c221
--- /dev/null
+++ b/handler/reset/request.go
@@ -0,0 +1,65 @@
+package reset
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/go-playground/validator/v10"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request"
+)
+
+func (rs ResetHandlerImpl) Request(w http.ResponseWriter, r *http.Request) {
+	payload := request.RequestRequestPayload{}
+	validate := validator.New()
+
+	if r.Header.Get("Content-Type") != "application/json" {
+		payload := rs.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload)
+		return
+	}
+
+	if err := rs.HttpUtil.ParseJson(r, &payload); err != nil {
+		payload := rs.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error())
+		rs.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload)
+		return
+	}
+
+	if err := validate.Struct(payload); err != nil {
+		if _, ok := err.(*validator.InvalidValidationError); ok {
+			payload := rs.WrapperUtil.ErrorResponseWrap(err.Error(), nil)
+			rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			return
+		}
+
+		errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors))
+		payload := rs.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload)
+		rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	err := rs.ResetService.Request(payload)
+
+	if err != nil {
+		if strings.Contains(err.Error(), "unknown key") {
+			err = web.NewResponseErrorFromError(err, web.EmailNotExist)
+		}
+
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := rs.WrapperUtil.ErrorResponseWrap("email was not found", respErr)
+			rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		} else {
+			rs.Logger.Error(
+				fmt.Sprintf("[RESET] some error happened when requesting reset email: %s", err.Error()),
+			)
+			payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil)
+	rs.HttpUtil.WriteSuccessJson(w, responsePayload)
+}
\ No newline at end of file
diff --git a/handler/reset/types.go b/handler/reset/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f0a8dbcfb276d41ad8067d6848d857535d739c4
--- /dev/null
+++ b/handler/reset/types.go
@@ -0,0 +1,9 @@
+package reset
+
+import "net/http"
+
+type ResetHandler interface {
+	Request(w http.ResponseWriter, r *http.Request)
+	Confirm(w http.ResponseWriter, r *http.Request)
+	Validate(w http.ResponseWriter, r *http.Request)
+}
diff --git a/handler/reset/validate.go b/handler/reset/validate.go
new file mode 100644
index 0000000000000000000000000000000000000000..19d48dbfdd2e9cf42e5c874779a1f03c8db100f9
--- /dev/null
+++ b/handler/reset/validate.go
@@ -0,0 +1,57 @@
+package reset
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate"
+)
+
+func (rs ResetHandlerImpl) Validate(w http.ResponseWriter, r *http.Request) {
+	payload := validate.ValidateRequestPayload{}
+	validateTokenHeader := r.Header.Get("Authorization")
+
+	if validateTokenHeader == "" {
+		payload := rs.WrapperUtil.ErrorResponseWrap("token is required", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+		return
+	}
+
+	token := strings.Split(validateTokenHeader, " ")
+
+	if len(token) != 2 {
+		payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	if token[0] != "Bearer" {
+		payload := rs.WrapperUtil.ErrorResponseWrap("invalid token", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	payload.ValidateToken = token[1]
+	err := rs.ResetService.Validate(payload)
+
+	if err != nil {
+		if errData, ok := err.(web.ResponseError); ok {
+			payload := rs.WrapperUtil.ErrorResponseWrap(errData.Error(), errData)
+			rs.HttpUtil.WriteJson(w, http.StatusUnauthorized, payload)
+			return
+		}
+
+		rs.Logger.Error(
+			fmt.Sprintf("[RESET] some error happened when validating URL: %s", err.Error()),
+		)
+		payload := rs.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+		rs.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	responsePayload := rs.WrapperUtil.SuccessResponseWrap(nil)
+	rs.HttpUtil.WriteSuccessJson(w, responsePayload)
+
+}
\ No newline at end of file
diff --git a/model/web/error_code.go b/model/web/error_code.go
index 0999a12b14af2383a1aea2f79dd7aa490754a88a..e610d20e8224716d3e4ff4ea6daa8c66948eb093 100644
--- a/model/web/error_code.go
+++ b/model/web/error_code.go
@@ -7,6 +7,8 @@ const (
 	UnauthorizedAccess string = "UNAUTHORIZED"
 	InactiveUser       string = "INACTIVE_ACCOUNT"
 	EmailExist         string = "EMAIL_EXIST"
+	EmailNotExist      string = "EMAIL_NOT_EXIST"
+	LinkNotAvailable   string = "LINK_NOT_AVAILABLE"
 
 	TokenError string = "TOKEN_ERROR"
 )
diff --git a/model/web/reset/confirm/request.go b/model/web/reset/confirm/request.go
new file mode 100644
index 0000000000000000000000000000000000000000..6d864a35e49373198dfff3fe18da82094519c052
--- /dev/null
+++ b/model/web/reset/confirm/request.go
@@ -0,0 +1,14 @@
+package confirm
+
+// Confirm Request Payload
+// @Description Information that should be available when you confirm a password reset
+type ConfirmRequestPayload struct {
+	// Web Token that was appended to the link
+	ConfirmToken string
+
+	// User Password
+	Password string `json:"password" validate:"required" example:"secret"`
+
+	// User Password Validation, must be same as user
+	PasswordValidation string `json:"password_validation" validate:"required,eqfield=Password" example:"secret"`
+}
diff --git a/model/web/reset/request/request.go b/model/web/reset/request/request.go
new file mode 100644
index 0000000000000000000000000000000000000000..cdd5d15bfbbf4cdccbe381d1af776bf58a299b36
--- /dev/null
+++ b/model/web/reset/request/request.go
@@ -0,0 +1,8 @@
+package request
+
+// Request Request Payload
+// @Description Information that should be available when password reset is requested
+type RequestRequestPayload struct {
+	// User Email
+	Email string `json:"email" validate:"required,email" example:"someone@example.com"`
+}
diff --git a/model/web/reset/validate/request.go b/model/web/reset/validate/request.go
new file mode 100644
index 0000000000000000000000000000000000000000..f13973a086b4048d637a660d08c75076a5ec3378
--- /dev/null
+++ b/model/web/reset/validate/request.go
@@ -0,0 +1,8 @@
+package validate
+
+// Validate Request Payload
+// @Description Information that should be available when link validation is done
+type ValidateRequestPayload struct {
+	// Web Token that was appended to the link
+	ValidateToken string
+}
diff --git a/routes/di.go b/routes/di.go
index 51ee85b71e73dbda4683d1e4a8cc620d088c8e3a..b44b2ed0ae71dda868755ffdf82ecf3c636a7a1e 100644
--- a/routes/di.go
+++ b/routes/di.go
@@ -5,6 +5,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/routes/admin"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/auth"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/common"
+	"gitlab.informatika.org/ocw/ocw-backend/routes/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/swagger"
 )
 
@@ -13,6 +14,7 @@ var routesCollectionSet = wire.NewSet(
 	wire.Struct(new(swagger.SwaggerRoutes), "*"),
 	wire.Struct(new(auth.AuthRoutes), "*"),
 	wire.Struct(new(admin.AdminRoutes), "*"),
+	wire.Struct(new(reset.ResetRoutes), "*"),
 )
 
 var RoutesSet = wire.NewSet(
diff --git a/routes/reset/route.go b/routes/reset/route.go
new file mode 100644
index 0000000000000000000000000000000000000000..46b32e0f9f389bfe8f3ff730d2c683225dfcee91
--- /dev/null
+++ b/routes/reset/route.go
@@ -0,0 +1,18 @@
+package reset
+
+import (
+	"github.com/go-chi/chi/v5"
+	"gitlab.informatika.org/ocw/ocw-backend/handler/reset"
+)
+
+type ResetRoutes struct {
+	reset.ResetHandler
+}
+
+func (rr ResetRoutes) Register(r chi.Router) {
+	r.Route("/reset", func(r chi.Router) {
+		r.Post("/request", rr.ResetHandler.Request)
+		r.Post("/confirm", rr.ResetHandler.Confirm)
+		r.Post("/validate", rr.ResetHandler.Validate)
+	})
+}
diff --git a/routes/routes.go b/routes/routes.go
index b29a111f5ffa31d00b6ad6615d0d72f399966a79..4c358d7a37e29d0c2e62fb603f5d5aa70469f061 100644
--- a/routes/routes.go
+++ b/routes/routes.go
@@ -4,6 +4,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/routes/admin"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/auth"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/common"
+	"gitlab.informatika.org/ocw/ocw-backend/routes/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/swagger"
 
 	"gitlab.informatika.org/ocw/ocw-backend/service/logger"
@@ -15,6 +16,7 @@ type AppRouter struct {
 	admin.AdminRoutes
 	common.CommonRoutes
 	auth.AuthRoutes
+	reset.ResetRoutes
 
 	// Utility
 	Logger logger.Logger
diff --git a/service/di.go b/service/di.go
index c8ca39249a908a62ba8ed3742e3215cce1bfe4e4..763b0502928cbdacbe020c1c3d1e4ba64e590989 100644
--- a/service/di.go
+++ b/service/di.go
@@ -8,6 +8,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/service/logger"
 	"gitlab.informatika.org/ocw/ocw-backend/service/logger/hooks"
 	"gitlab.informatika.org/ocw/ocw-backend/service/reporter"
+	"gitlab.informatika.org/ocw/ocw-backend/service/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/service/verification"
 )
 
@@ -36,6 +37,12 @@ var ServiceTestSet = wire.NewSet(
 		wire.Bind(new(admin.AdminService), new(*admin.AdminServiceImpl)),
 	),
 
+	// reset service
+	wire.NewSet(
+		wire.Struct(new(reset.ResetServiceImpl), "*"),
+		wire.Bind(new(reset.ResetService), new(*reset.ResetServiceImpl)),
+	),
+
 	// verification service
 	wire.NewSet(
 		wire.Struct(new(verification.VerificationServiceImpl), "*"),
diff --git a/service/reset/confirm.go b/service/reset/confirm.go
new file mode 100644
index 0000000000000000000000000000000000000000..426af52f5514eec16fd0a6c4553bd817974177fd
--- /dev/null
+++ b/service/reset/confirm.go
@@ -0,0 +1,11 @@
+package reset
+
+import (
+	// "gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm"
+)
+
+func (rs ResetServiceImpl) Confirm(payload confirm.ConfirmRequestPayload) error {
+	// TODO replace dummy
+	return nil
+}
\ No newline at end of file
diff --git a/service/reset/impl.go b/service/reset/impl.go
new file mode 100644
index 0000000000000000000000000000000000000000..b9986e2509f4af7f8c0deb9053bf8d30640e4168
--- /dev/null
+++ b/service/reset/impl.go
@@ -0,0 +1,17 @@
+package reset
+
+import (
+	"gitlab.informatika.org/ocw/ocw-backend/repository/user"
+	"gitlab.informatika.org/ocw/ocw-backend/service/verification"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/env"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/password"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/token"
+)
+
+type ResetServiceImpl struct {
+	user.UserRepository
+	password.PasswordUtil
+	*env.Environment
+	token.TokenUtil
+	verification.VerificationService
+}
diff --git a/service/reset/request.go b/service/reset/request.go
new file mode 100644
index 0000000000000000000000000000000000000000..05584e6a9bb3dcf0a7a78c4ec5e12e1c4502732d
--- /dev/null
+++ b/service/reset/request.go
@@ -0,0 +1,11 @@
+package reset
+
+import (
+	// "gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request"
+)
+
+func (rs ResetServiceImpl) Request(payload request.RequestRequestPayload) error {
+	// TODO replace dummy
+	return nil
+}
\ No newline at end of file
diff --git a/service/reset/type.go b/service/reset/type.go
new file mode 100644
index 0000000000000000000000000000000000000000..f1658d2fa25575421a24bcf7fa8ac331a88b5dbe
--- /dev/null
+++ b/service/reset/type.go
@@ -0,0 +1,13 @@
+package reset
+
+import (
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/request"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/confirm"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate"
+)
+
+type ResetService interface {
+	Request(payload request.RequestRequestPayload) error
+	Confirm(payload confirm.ConfirmRequestPayload) error
+	Validate(payload validate.ValidateRequestPayload) error
+}
diff --git a/service/reset/validate.go b/service/reset/validate.go
new file mode 100644
index 0000000000000000000000000000000000000000..7cd09fe9bc83b23a2e4d0bbc0abf95fe1758b5cc
--- /dev/null
+++ b/service/reset/validate.go
@@ -0,0 +1,11 @@
+package reset
+
+import (
+	// "gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/reset/validate"
+)
+
+func (rs ResetServiceImpl) Validate(payload validate.ValidateRequestPayload) error {
+	// TODO replace dummy
+	return nil
+}
\ No newline at end of file