diff --git a/.env b/.env
index f0f1fecc697b60d78a401477ee01c6c5febbc5a5..43bf5fd412feeda40517d5f5e722333b92271442 100644
--- a/.env
+++ b/.env
@@ -4,9 +4,6 @@ PORT=8080
 LOGTAIL_TOKEN=
 HTTP_TIMEOUT_SEC=2
 LOG_FLUSH_INTERVAL_MS=1000
-DB_STRING="host=localhost user=ocw password=ocw dbname=ocw-db port=5433 sslmode=disable TimeZone=Asia/Shanghai"
 SMTP_USERNAME="noreply@ocw.id"
-SMTP_SERVER=localhost
 SMTP_PORT=1025
-REDIS_STRING="localhost"
-FE_BASE_URL="http://localhost:3000"
\ No newline at end of file
+FE_BASE_URL="http://localhost:3000"
diff --git a/.env.local.example b/.env.local.example
new file mode 100644
index 0000000000000000000000000000000000000000..c251113ad3df4ab91c35cd7202eb9b5158ecc145
--- /dev/null
+++ b/.env.local.example
@@ -0,0 +1,11 @@
+# Sesuaikan dengan environment masing-masing
+BUCKET_ENDPOINT="minio:9000"
+BUCKET_SECRET_KEY="SUTT"
+BUCKET_ACCESS_ID="ACCESS"
+BUKET_TOKEN_KEY=""
+BUCKET_USE_SSL=false
+BUCKET_NAME="ocw"
+
+SMTP_SERVER=localhost
+DB_STRING="host=localhost user=ocw password=ocw dbname=ocw-db port=5433 sslmode=disable TimeZone=Asia/Shanghai"
+REDIS_STRING="localhost"
diff --git a/.gitignore b/.gitignore
index c5c13586f4cb91a1d501686acc635db166cd0398..d3eeb070587bb63e0a455b491181f8fc408c8df1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 bin/
 tmp/
 wire_gen.go
-__debug_bin*
\ No newline at end of file
+__debug_bin*
+.env.local
diff --git a/Makefile b/Makefile
index 948871652a86d5e028ef4f13a7c2571766d27ec1..961e18808da80a896920915b35bbc96e27f321d0 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ build: dependency
 	@go build -o=bin/server.app .
 
 watch: dependency
-	@air --build.cmd="make build" --build.bin="./bin/server.app" --build.exclude_dir="bin,tmp,docs" --build.exclude_file="wire_gen.go"
+	@air --build.cmd="make build" --build.bin="./bin/server.app" --build.exclude_dir="bin,tmp,docs" --build.exclude_file="wire_gen.go" --build.kill_delay="0s"
 
 test: test-dependency
 	@go test ./test/... -v
diff --git a/model/domain/course/course.go b/model/domain/course/course.go
new file mode 100644
index 0000000000000000000000000000000000000000..b80e0b4067f2d8b69b452949bedcf2a323992387
--- /dev/null
+++ b/model/domain/course/course.go
@@ -0,0 +1,19 @@
+package course
+
+import (
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+)
+
+type Course struct {
+	Id           string `gorm:"primaryKey"`
+	Name         string
+	MajorId      uuid.UUID
+	Description  string
+	Major        Major       `gorm:"foreignKey:MajorId;references:Id"`
+	Contributors []user.User `gorm:"many2many:course_contributor;foreignKey:Id;joinForeignKey:CourseId;references:Email;joinReferences:Email"`
+}
+
+func (Course) TableName() string {
+	return "course"
+}
diff --git a/model/domain/course/major.go b/model/domain/course/major.go
new file mode 100644
index 0000000000000000000000000000000000000000..7b19ab02cc2421bd4e10d5be327c73009f96daac
--- /dev/null
+++ b/model/domain/course/major.go
@@ -0,0 +1,12 @@
+package course
+
+import "github.com/google/uuid"
+
+type Major struct {
+	Id   uuid.UUID `gorm:"type:uuid;primaryKey"`
+	Name string
+}
+
+func (Major) TableName() string {
+	return "major"
+}
diff --git a/model/domain/material/content.go b/model/domain/material/content.go
new file mode 100644
index 0000000000000000000000000000000000000000..0e181045ba1141a363e07b196bf985c9fc0f4adf
--- /dev/null
+++ b/model/domain/material/content.go
@@ -0,0 +1,14 @@
+package material
+
+import "github.com/google/uuid"
+
+type Content struct {
+	Id         uuid.UUID `gorm:"primaryKey"`
+	Type       MaterialType
+	Link       string
+	MaterialId uuid.UUID
+}
+
+func (Content) TableName() string {
+	return "material_data"
+}
diff --git a/model/domain/material/material.go b/model/domain/material/material.go
new file mode 100644
index 0000000000000000000000000000000000000000..55d6bfbf7d002af41a7edc6bdb50beed800650a0
--- /dev/null
+++ b/model/domain/material/material.go
@@ -0,0 +1,20 @@
+package material
+
+import (
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/course"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+)
+
+type Material struct {
+	Id           uuid.UUID `gorm:"primaryKey"`
+	CourseId     string
+	CreatorEmail string
+	Creator      user.User     `gorm:"foreignKey:CreatorEmail;references:Email"`
+	Course       course.Course `gorm:"foreignKey:CourseId;references:Id"`
+	Contents     []Content     `gorm:"foreignKey:MaterialId;references:Id"`
+}
+
+func (Material) TableName() string {
+	return "material"
+}
diff --git a/model/domain/material/material_type.go b/model/domain/material/material_type.go
new file mode 100644
index 0000000000000000000000000000000000000000..a4ed9ba30cf209e6a4655d05fa6e3ca3208e68ec
--- /dev/null
+++ b/model/domain/material/material_type.go
@@ -0,0 +1,69 @@
+package material
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"errors"
+	"fmt"
+)
+
+type MaterialType int
+
+const (
+	Video MaterialType = iota
+	Handout
+)
+
+var roleMapping = map[MaterialType]string{
+	Video:   "video",
+	Handout: "handout",
+}
+
+func (ur *MaterialType) Scan(value interface{}) error {
+	val := value.(string)
+
+	for key, label := range roleMapping {
+		if label == val {
+			*ur = key
+			return nil
+		}
+	}
+
+	return fmt.Errorf("invalid user role")
+}
+
+func (u MaterialType) Value() (driver.Value, error) {
+	value, ok := roleMapping[u]
+
+	if !ok {
+		return nil, fmt.Errorf("invalid user role")
+	}
+
+	return value, nil
+}
+
+func (u *MaterialType) UnmarshalJSON(b []byte) error {
+	var s string
+	if err := json.Unmarshal(b, &s); err != nil {
+		return err
+	}
+
+	for key, label := range roleMapping {
+		if label == s {
+			*u = key
+			return nil
+		}
+	}
+
+	return fmt.Errorf("unkown role, given %s", s)
+}
+
+func (u MaterialType) MarshalJSON() ([]byte, error) {
+	s, ok := roleMapping[u]
+
+	if !ok {
+		return nil, errors.New("unkown user role")
+	}
+
+	return json.Marshal(s)
+}
diff --git a/model/domain/quiz/options.go b/model/domain/quiz/options.go
new file mode 100644
index 0000000000000000000000000000000000000000..de062c3cc531f34b4572016248e2085a2d1f1d99
--- /dev/null
+++ b/model/domain/quiz/options.go
@@ -0,0 +1,14 @@
+package quiz
+
+import "github.com/google/uuid"
+
+type AnswerOption struct {
+	Id            uuid.UUID `gorm:"primaryKey"`
+	QuizProblemId uuid.UUID `gorm:"primaryKey"`
+	Statement     string
+	IsAnswer      bool
+}
+
+func (AnswerOption) TableName() string {
+	return "quiz_choice_answer"
+}
diff --git a/model/domain/quiz/problem_type.go b/model/domain/quiz/problem_type.go
new file mode 100644
index 0000000000000000000000000000000000000000..c03621526c635dc524f1caee1518665931b80d10
--- /dev/null
+++ b/model/domain/quiz/problem_type.go
@@ -0,0 +1,67 @@
+package quiz
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"errors"
+	"fmt"
+)
+
+type ProblemType int
+
+const (
+	Choice ProblemType = iota
+)
+
+var roleMapping = map[ProblemType]string{
+	Choice: "choice",
+}
+
+func (ur *ProblemType) Scan(value interface{}) error {
+	val := value.(string)
+
+	for key, label := range roleMapping {
+		if label == val {
+			*ur = key
+			return nil
+		}
+	}
+
+	return fmt.Errorf("invalid user role")
+}
+
+func (u ProblemType) Value() (driver.Value, error) {
+	value, ok := roleMapping[u]
+
+	if !ok {
+		return nil, fmt.Errorf("invalid user role")
+	}
+
+	return value, nil
+}
+
+func (u *ProblemType) UnmarshalJSON(b []byte) error {
+	var s string
+	if err := json.Unmarshal(b, &s); err != nil {
+		return err
+	}
+
+	for key, label := range roleMapping {
+		if label == s {
+			*u = key
+			return nil
+		}
+	}
+
+	return fmt.Errorf("unkown role, given %s", s)
+}
+
+func (u ProblemType) MarshalJSON() ([]byte, error) {
+	s, ok := roleMapping[u]
+
+	if !ok {
+		return nil, errors.New("unkown user role")
+	}
+
+	return json.Marshal(s)
+}
diff --git a/model/domain/quiz/quiz.go b/model/domain/quiz/quiz.go
new file mode 100644
index 0000000000000000000000000000000000000000..6caade417ae857ec2a78fdf8c13b5ef007a8529a
--- /dev/null
+++ b/model/domain/quiz/quiz.go
@@ -0,0 +1,21 @@
+package quiz
+
+import (
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/course"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+)
+
+type Quiz struct {
+	Id           uuid.UUID `gorm:"primaryKey"`
+	Name         string
+	CourseId     string
+	CreatorEmail string
+	Creator      user.User     `gorm:"foreignKey:CreatorEmail;references:Email"`
+	Course       course.Course `gorm:"foreignKey:CourseId;references:Id"`
+	Problems     []QuizProblem `gorm:"foreignKey:QuizId;references:Id"`
+}
+
+func (Quiz) TableName() string {
+	return "quiz"
+}
diff --git a/model/domain/quiz/quiz_problem.go b/model/domain/quiz/quiz_problem.go
new file mode 100644
index 0000000000000000000000000000000000000000..3ce4195465d2fd477567f18858296d949c60020c
--- /dev/null
+++ b/model/domain/quiz/quiz_problem.go
@@ -0,0 +1,15 @@
+package quiz
+
+import "github.com/google/uuid"
+
+type QuizProblem struct {
+	Id        uuid.UUID `gorm:"primaryKey"`
+	Statement string
+	Type      ProblemType
+	QuizId    uuid.UUID
+	Options   []AnswerOption `gorm:"foreignKey:QuizProblemId;references:Id"`
+}
+
+func (QuizProblem) TableName() string {
+	return "quiz_problem"
+}
diff --git a/model/domain/quiz/take.go b/model/domain/quiz/take.go
new file mode 100644
index 0000000000000000000000000000000000000000..c66263f60f1e4009019e5dcd428e7c905784f243
--- /dev/null
+++ b/model/domain/quiz/take.go
@@ -0,0 +1,24 @@
+package quiz
+
+import (
+	"os/user"
+	"time"
+
+	"github.com/google/uuid"
+)
+
+type QuizTake struct {
+	Id            uuid.UUID `gorm:"primaryKey"`
+	QuizId        uuid.UUID
+	Email         string
+	StartTime     time.Time
+	IsFinished    bool
+	Score         int
+	Quiz          `gorm:"foreignKey:QuizId;references:Id"`
+	user.User     `gorm:"foreignKey:Email;references:Email"`
+	ChoiceAnswers []TakeChoiceAnswer `gorm:"foreignKey:QuizTakeId;references:Id"`
+}
+
+func (QuizTake) TableName() string {
+	return "quiz_take"
+}
diff --git a/model/domain/quiz/take_choice_answer.go b/model/domain/quiz/take_choice_answer.go
new file mode 100644
index 0000000000000000000000000000000000000000..25e76746d8951251d3dbdee694986205ce364d53
--- /dev/null
+++ b/model/domain/quiz/take_choice_answer.go
@@ -0,0 +1,14 @@
+package quiz
+
+import "github.com/google/uuid"
+
+type TakeChoiceAnswer struct {
+	QuizTakeId    uuid.UUID `gorm:"primaryKey"`
+	AnswerChoice  uuid.UUID
+	QuizProblemId uuid.UUID `gorm:"primaryKey"`
+	AnswerOption  `gorm:"foreignKey:AnswerChoice,QuizProblemId;references:Id,QuizProblemId"`
+}
+
+func (TakeChoiceAnswer) TableName() string {
+	return "quiz_take_choice_answer"
+}
diff --git a/provider/storage/s3.go b/provider/storage/s3.go
index 435b0bf2a2d44e985d3c5bf8d2be965035ea8921..3f746712f3fa34c1ae77ea87fe4491282e4a6609 100644
--- a/provider/storage/s3.go
+++ b/provider/storage/s3.go
@@ -15,8 +15,8 @@ func NewS3(
 	env *env.Environment,
 ) (*S3Storage, error) {
 	client, err := minio.New(env.BucketEndpoint, &minio.Options{
-		Creds:  credentials.NewStaticV4(env.BucketKeyId, env.BucketAccessKey, ""),
-		Secure: true,
+		Creds:  credentials.NewStaticV4(env.BucketAccessKey, env.BucketSecretKey, env.BucketTokenKey),
+		Secure: env.BucketUseSSL,
 	})
 
 	if err != nil {
diff --git a/utils/env/env.go b/utils/env/env.go
index 754b1ebb5ac87aa4e4649a6e7d6002196d37b55b..617338eb52e2950ad7720e131e3f60ad2e897f48 100644
--- a/utils/env/env.go
+++ b/utils/env/env.go
@@ -52,16 +52,17 @@ type Environment struct {
 	RedisUseAuth    bool   `env:"REDIS_USE_AUTH" envDefault:"false"`
 	RedisPrefixKey  string `env:"REDIS_PREFIX_KEY" envDefault:"app:"`
 
-  BucketEndpoint   string `env:"BUCKET_ENDPOINT"`
-  BucketAccessKey  string `env:"BUCKET_ACCESS_KEY"`
-  BucketKeyId      string `env:"BUCKET_KEY_ID"`
-  BucketUseSSL     bool   `env:"BUCKET_USE_SSL" envDefault:"true"`
-  BucketName       string `env:"BUCKET_NAME"`
+	BucketEndpoint  string `env:"BUCKET_ENDPOINT"`
+	BucketSecretKey string `env:"BUCKET_SECRET_KEY"`
+	BucketAccessKey string `env:"BUCKET_ACCESS_KEY"`
+	BucketTokenKey  string `env:"BUCKET_TOKEN_KEY"`
+	BucketUseSSL    bool   `env:"BUCKET_USE_SSL" envDefault:"true"`
+	BucketName      string `env:"BUCKET_NAME"`
 
-  BucketSignedPutDuration   int64  `env:"BUCKET_SIGNED_PUT_DURATION_S" envDefault:"36000"`
-  BucketSignedGetDuration   int64  `env:"BUCKET_SIGNED_GET_DURATION_S" envDefault:"1800"`
+	BucketSignedPutDuration int64 `env:"BUCKET_SIGNED_PUT_DURATION_S" envDefault:"36000"`
+	BucketSignedGetDuration int64 `env:"BUCKET_SIGNED_GET_DURATION_S" envDefault:"1800"`
 
-  BucketMaterialBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"materials/"`
+	BucketMaterialBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"materials/"`
 }
 
 func New() (*Environment, error) {
@@ -83,7 +84,10 @@ func NewEnv() (*Environment, error) {
 }
 
 func NewDotEnv() (*Environment, error) {
-	err := godotenv.Load()
+	err := godotenv.Load(
+		".env",
+		".env.local",
+	)
 
 	if err != nil {
 		return nil, err