diff --git a/Makefile b/Makefile
index 3792027ea16d8b7e081eb69c2c554752f2d8467b..d4fe896313d45d23a65a2f071540ef8576f399fd 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
+.PHONY: templ-generate
+templ-generate:
+	templ generate
+
 .PHONY: run
 run:
-	go run ./cmd/main.go
\ No newline at end of file
+	go run ./cmd/main.go
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f2bfa4f857acd20822f714913d0c7ca766745dff
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,28 @@
+version: '3.9'
+
+services:
+  db:
+    container_name: db
+    hostname: db
+    image: postgres
+    environment:
+      POSTGRES_USER: postgres
+      POSTGRES_PASSWORD: password
+      POSTGRES_DB: aidb
+      POSTGRES_NAME:
+      PGDATA: /data/postgres
+    volumes:
+      - postgres:/data/postgres
+    ports:
+      - "5432:5432"
+    networks:
+      - airadio
+    restart: unless-stopped
+
+
+networks:
+  airadio:
+    driver: bridge
+
+volumes:
+  postgres:
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9e63b2e80421bb05aea511c6f86becf7c6dadedc..4b295fc53279f9db7d4caa1c091c284a8733ace1 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,21 @@ go 1.22
 
 require (
 	github.com/a-h/templ v0.2.663
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/jmoiron/sqlx v1.3.5
+	github.com/lib/pq v1.2.0
+	github.com/pressly/goose/v3 v3.20.0
 	github.com/rs/zerolog v1.32.0
+	golang.org/x/crypto v0.21.0
 )
 
 require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.22 // indirect
+	github.com/mfridman/interpolate v0.0.2 // indirect
+	github.com/sethvargo/go-retry v0.2.4 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
 	golang.org/x/sys v0.19.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 6da209fd1eb503338c9f46a4ce2a205ac591c564..808333dcc953c5cb2c53e2a3037cbab19a37e724 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,24 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/a-h/templ v0.2.663 h1:aa0WMm27InkYHGjimcM7us6hJ6BLhg98ZbfaiDPyjHE=
 github.com/a-h/templ v0.2.663/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
 github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
@@ -16,14 +29,51 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
+github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pressly/goose/v3 v3.20.0 h1:uPJdOxF/Ipj7ABVNOAMJXSxwFXZGwMGHNqjC8e61VA0=
+github.com/pressly/goose/v3 v3.20.0/go.mod h1:BRfF2GcG4FTG12QfdBVy3q1yveaf4ckL9vWwEcIO3lA=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
 github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
+github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
 golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
+modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
+modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
+modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
+modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/internal/app/app.go b/internal/app/app.go
index 578e0c35c37cece9475b4df9ad3c2ab6a561dc14..d59b88c4a39d0c2ad813ca2aba7d5a40e2d34738 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -2,9 +2,12 @@ package app
 
 import (
 	"context"
+	"embed"
 	"errors"
 	"fmt"
 	"github.com/jmoiron/sqlx"
+	_ "github.com/lib/pq"
+	"github.com/pressly/goose/v3"
 	http2 "github.com/radiologist-ai/web-app/internal/app/http"
 	"github.com/radiologist-ai/web-app/internal/app/http/handlers"
 	"github.com/radiologist-ai/web-app/internal/app/users/usersrepo"
@@ -18,16 +21,19 @@ import (
 	"time"
 )
 
+//go:embed migrations/*.sql
+var embedMigrations embed.FS
+
 func Run(backgroundCtx context.Context, wg *sync.WaitGroup) error {
 	defer wg.Done()
 
 	cfg := config.GetConfig()
 
 	logger := ptr.Pointer(zerolog.New(os.Stderr).With().Timestamp().Caller().Logger())
-	// TODO Unmock
-	db := &sqlx.DB{
-		DB:     nil,
-		Mapper: nil,
+
+	db, err := PreparePostgres(cfg.Database)
+	if err != nil {
+		return err
 	}
 
 	// repository
@@ -43,7 +49,7 @@ func Run(backgroundCtx context.Context, wg *sync.WaitGroup) error {
 	}
 
 	// handlers
-	handle, err := handlers.NewHandlers(logger, usersService)
+	handle, err := handlers.NewHandlers(logger, usersService, cfg.Server.Secret)
 	if err != nil {
 		return err
 	}
@@ -86,4 +92,22 @@ func Run(backgroundCtx context.Context, wg *sync.WaitGroup) error {
 	return nil
 }
 
-//func PreparePostgres(cfg)
+func PreparePostgres(cfg config.Database) (*sqlx.DB, error) {
+	db, err := sqlx.Connect("postgres",
+		fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
+			cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database))
+	if err != nil {
+		return nil, err
+	}
+
+	goose.SetBaseFS(embedMigrations)
+
+	if err := goose.SetDialect("postgres"); err != nil {
+		return nil, err
+	}
+
+	if err := goose.Up(db.DB, "migrations"); err != nil {
+		return nil, err
+	}
+	return db, nil
+}
diff --git a/internal/app/http/handlers/context_manipulations.go b/internal/app/http/handlers/context_manipulations.go
new file mode 100644
index 0000000000000000000000000000000000000000..06653e42502e82af520e3b806cf017adefc8e3a3
--- /dev/null
+++ b/internal/app/http/handlers/context_manipulations.go
@@ -0,0 +1,18 @@
+package handlers
+
+import (
+	"context"
+	"github.com/radiologist-ai/web-app/internal/domain"
+)
+
+func SetCurrentUser(ctx context.Context, user domain.UserRepoModel) error {
+	return nil
+}
+
+func GetCurrentUser(ctx context.Context) (*domain.UserRepoModel, bool) {
+	currentUser, ok := ctx.Value(domain.CurrentUserCtxKey).(domain.UserRepoModel)
+	if !ok {
+		return nil, false
+	}
+	return &currentUser, true
+}
diff --git a/internal/app/http/handlers/handleauth.go b/internal/app/http/handlers/handleauth.go
index 5ac8282f4bcd598a41eaa57a5d16bf36d0005630..fb99f2701bfca4e434cf11d93f6b68c9e67ddf4b 100644
--- a/internal/app/http/handlers/handleauth.go
+++ b/internal/app/http/handlers/handleauth.go
@@ -1 +1,140 @@
 package handlers
+
+import (
+	"context"
+	"errors"
+	"github.com/radiologist-ai/web-app/internal/domain"
+	"github.com/radiologist-ai/web-app/internal/domain/customerrors"
+	"github.com/radiologist-ai/web-app/internal/views"
+	"golang.org/x/crypto/bcrypt"
+	"net/http"
+	"net/mail"
+	"time"
+)
+
+func (h *Handlers) PostLogout(w http.ResponseWriter, r *http.Request) {
+	cookie := http.Cookie{Name: domain.AuthTokenCookieKey, Value: "", Expires: time.Time{}}
+	http.SetCookie(w, &cookie)
+	ctx := context.WithValue(r.Context(), domain.CurrentUserCtxKey, nil)
+	r = r.WithContext(ctx)
+	if err := views.Nav(nil).Render(r.Context(), w); err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+	}
+	return
+}
+
+func (h *Handlers) PostLogin(w http.ResponseWriter, r *http.Request) {
+	email := r.FormValue("email")
+	password := r.FormValue("password")
+	user, ok, err := h.users.GetByEmail(r.Context(), email)
+	if err != nil {
+		http.Redirect(w, r, "/internal_server_error", http.StatusFound)
+		return
+	}
+	if !ok {
+		if err := views.Layout(views.LoginFormUserDoesntExist(), "Radiologist AI").Render(r.Context(), w); err != nil {
+			http.Redirect(w, r, "/internal_server_error", http.StatusFound)
+		}
+		return
+	}
+	if bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)) != nil {
+		if err := views.Layout(views.LoginFormWrongPassword(), "Radiologist AI").Render(r.Context(), w); err != nil {
+			http.Redirect(w, r, "/internal_server_error", http.StatusFound)
+		}
+	}
+
+	token, err := h.users.GenerateToken(h.secret, user.Email)
+	if err != nil {
+		h.logger.Error().Err(err).Any("user", user).Msg("Error generating token")
+		http.Redirect(w, r, "/internal_server_error", http.StatusFound)
+		return
+	}
+
+	expiration := time.Now().Add(365 * 24 * time.Hour)
+	cookie := http.Cookie{Name: domain.AuthTokenCookieKey, Value: token, Expires: expiration}
+	http.SetCookie(w, &cookie)
+	http.Redirect(w, r, "/home", http.StatusFound)
+	return
+}
+
+func (h *Handlers) PostRegister(w http.ResponseWriter, r *http.Request) {
+	var form domain.UserForm
+	form.Email = r.FormValue("email")
+	form.Password = r.FormValue("password")
+	form.FirstName = r.FormValue("firstName")
+	form.LastName = r.FormValue("lastName")
+	form.IsDoctor = r.FormValue("isDoctor") == "on"
+
+	user, err := h.users.CreateOne(r.Context(), form)
+	if err != nil {
+		if errors.Is(err, customerrors.ValidationError) {
+			errTxt := ValidationErrorToResponseText(err)
+			if err := views.Layout(views.RegistrationFormBad(errTxt), "Radiologist AI").Render(r.Context(), w); err != nil {
+				http.Redirect(w, r, "/internal_server_error", http.StatusFound)
+			}
+			return
+		}
+		http.Redirect(w, r, "/internal_server_error", http.StatusFound)
+		return
+	}
+	token, err := h.users.GenerateToken(h.secret, user.Email)
+	if err != nil {
+		h.logger.Error().Err(err).Any("user", user).Msg("Error generating token")
+		http.Redirect(w, r, "/login", http.StatusFound)
+		return
+	}
+
+	expiration := time.Now().Add(365 * 24 * time.Hour)
+	cookie := http.Cookie{Name: domain.AuthTokenCookieKey, Value: token, Expires: expiration}
+	http.SetCookie(w, &cookie)
+	http.Redirect(w, r, "/home", http.StatusFound)
+	return
+}
+
+func (h *Handlers) ValidateEmail(w http.ResponseWriter, r *http.Request) {
+	var email = r.FormValue("email")
+	if email == "" {
+		if err := views.EmailInput("", email, "Invalid Email").Render(r.Context(), w); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+		return
+	}
+	if _, err := mail.ParseAddress(email); err != nil {
+		if err = views.EmailInput("is-invalid", email, "Invalid Email").Render(r.Context(), w); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+		return
+	}
+	if _, ok, _ := h.users.GetByEmail(r.Context(), email); ok {
+		if err := views.EmailInput("is-invalid", email, "User with same email already exists").Render(r.Context(), w); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+		return
+	}
+	if err := views.EmailInput("is-valid", email, "Invalid Email").Render(r.Context(), w); err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+	}
+}
+
+func (h *Handlers) ValidatePassword(w http.ResponseWriter, r *http.Request) {
+	var password = r.FormValue("password")
+	if password == "" {
+		if err := views.PasswordInput("", password, "").Render(r.Context(), w); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+		return
+	}
+	if err := h.users.ValidatePassword(password); err != nil {
+		feedback := "Invalid password" // TODO may be return status 500?
+		if errors.Is(err, customerrors.ValidationErrorPassword) {
+			feedback = ValidationErrorToResponseText(err)
+		}
+		if err := views.PasswordInput("is-invalid", password, feedback).Render(r.Context(), w); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+		}
+		return
+	}
+	if err := views.PasswordInput("is-valid", password, "").Render(r.Context(), w); err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+	}
+}
diff --git a/internal/app/http/handlers/handleindex.go b/internal/app/http/handlers/handleindex.go
index f8640bdeb548142f478285e70ea20b333b73ea76..a073c51de697d2c06ab2d9f914c8105c916c06cd 100644
--- a/internal/app/http/handlers/handleindex.go
+++ b/internal/app/http/handlers/handleindex.go
@@ -6,7 +6,7 @@ import (
 )
 
 func (h *Handlers) HandleIndex(w http.ResponseWriter, r *http.Request) {
-	if err := views.Index().Render(r.Context(), w); err != nil {
+	if err := views.Layout(views.Index(), "Radiologist AI").Render(r.Context(), w); err != nil {
 		w.WriteHeader(http.StatusInternalServerError)
 	}
 }
diff --git a/internal/app/http/handlers/handlers.go b/internal/app/http/handlers/handlers.go
index 39de3f5f03c450f85e4b1cd39be43c3eea0b4294..6ae9dcd3558ea49db357852c6b56f9942c047d6a 100644
--- a/internal/app/http/handlers/handlers.go
+++ b/internal/app/http/handlers/handlers.go
@@ -9,14 +9,18 @@ import (
 type Handlers struct {
 	logger *zerolog.Logger
 	users  domain.UsersService
+	secret []byte
 }
 
-func NewHandlers(logger *zerolog.Logger, users domain.UsersService) (*Handlers, error) {
+func NewHandlers(logger *zerolog.Logger, users domain.UsersService, secret string) (*Handlers, error) {
 	if logger == nil {
 		return nil, errors.New("logger is required")
 	}
 	if users == nil {
 		return nil, errors.New("users is required")
 	}
-	return &Handlers{logger: logger, users: users}, nil
+	if secret == "" {
+		return nil, errors.New("secret is required")
+	}
+	return &Handlers{logger: logger, users: users, secret: []byte(secret)}, nil
 }
diff --git a/internal/app/http/handlers/middlewares.go b/internal/app/http/handlers/middlewares.go
index 74c20d810772db37b486bc8f6ef6ec24522c8ae7..988ddcc34b72b71e54e87b93ef1c78c7ed8f2064 100644
--- a/internal/app/http/handlers/middlewares.go
+++ b/internal/app/http/handlers/middlewares.go
@@ -1,6 +1,10 @@
 package handlers
 
-import "net/http"
+import (
+	"context"
+	"github.com/radiologist-ai/web-app/internal/domain"
+	"net/http"
+)
 
 func (h *Handlers) WithHTMLResponse(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
@@ -8,3 +12,55 @@ func (h *Handlers) WithHTMLResponse(handler func(http.ResponseWriter, *http.Requ
 		handler(w, r)
 	}
 }
+
+func (h *Handlers) WithCurrentUser(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var (
+			token string
+			email string
+			user  domain.UserRepoModel
+			ok    bool
+		)
+		cookie, err := r.Cookie(domain.AuthTokenCookieKey)
+		if err != nil {
+			goto handle
+		}
+
+		token = cookie.Value
+		if token == "" {
+			goto handle
+		}
+		email, err = h.users.ValidateToken(h.secret, token)
+		if err != nil {
+			goto handle
+		}
+		if user, ok, err = h.users.GetByEmail(r.Context(), email); err != nil || !ok {
+			h.logger.Error().Err(err).Bool("userExists", ok).Str("email", email).Str("token", token).Msg("error or user not found")
+			goto handle
+		}
+		r = r.WithContext(context.WithValue(r.Context(), domain.CurrentUserCtxKey, user))
+
+	handle:
+		handler(w, r)
+	}
+}
+
+func (h *Handlers) AnonymousRequired(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if _, ok := GetCurrentUser(r.Context()); ok {
+			http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
+			return
+		}
+		handler(w, r)
+	}
+}
+
+func (h *Handlers) AuthRequired(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if _, ok := GetCurrentUser(r.Context()); !ok {
+			http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
+			return
+		}
+		handler(w, r)
+	}
+}
diff --git a/internal/app/http/handlers/validation.go b/internal/app/http/handlers/validation.go
new file mode 100644
index 0000000000000000000000000000000000000000..7b0ae31492ef49e82d429cd6903e72c06b95714e
--- /dev/null
+++ b/internal/app/http/handlers/validation.go
@@ -0,0 +1,25 @@
+package handlers
+
+import (
+	"errors"
+	"github.com/radiologist-ai/web-app/internal/domain/customerrors"
+)
+
+func ValidationErrorToResponseText(err error) string {
+	switch {
+	case err == nil:
+		return ""
+	case errors.Is(err, customerrors.ValidationErrorPasswordTooShort):
+		return "Password too short, use at least 8 characters."
+	case errors.Is(err, customerrors.ValidationErrorPasswordTooLong):
+		return "Password too long, use at most 64 characters."
+	case errors.Is(err, customerrors.ValidationErrorPasswordNoLetters):
+		return "Password must contain at least one letter."
+	case errors.Is(err, customerrors.ValidationErrorPasswordNoDigits):
+		return "Password must contain at least one digits."
+	case errors.Is(err, customerrors.ValidationErrorPasswordUnacceptableCharacters):
+		return "Password can only contain letters and digits."
+	default:
+		return err.Error()
+	}
+}
diff --git a/internal/app/http/routs.go b/internal/app/http/routs.go
index 0c015a49e74e62a3aab92d293380a6b47434d5c6..f335de4494548740ce26b9fb56f86cbdc362b71a 100644
--- a/internal/app/http/routs.go
+++ b/internal/app/http/routs.go
@@ -2,7 +2,9 @@ package http
 
 import (
 	"errors"
+	"github.com/a-h/templ"
 	"github.com/radiologist-ai/web-app/internal/app/http/handlers"
+	"github.com/radiologist-ai/web-app/internal/views"
 	"net/http"
 )
 
@@ -13,7 +15,65 @@ func NewRouter(handlers *handlers.Handlers) (*http.ServeMux, error) {
 	mux := http.NewServeMux()
 
 	// auth
-	mux.HandleFunc("GET /", handlers.WithHTMLResponse(handlers.HandleIndex))
+	mux.HandleFunc("GET /{$}",
+		handlers.WithHTMLResponse(
+			handlers.WithCurrentUser(
+				handlers.HandleIndex)))
+	mux.HandleFunc("GET /register",
+		handlers.WithHTMLResponse(
+			handlers.WithCurrentUser(
+				handlers.AnonymousRequired(
+					templ.Handler(
+						views.Layout(
+							views.RegistrationForm(),
+							"Radiologist AI. Register.")).
+						ServeHTTP))))
+	mux.HandleFunc("POST /register",
+		handlers.WithCurrentUser(
+			handlers.AnonymousRequired(
+				handlers.PostRegister)))
+	mux.HandleFunc("POST /validate/email",
+		handlers.WithHTMLResponse(
+			handlers.ValidateEmail))
+	mux.HandleFunc("POST /validate/password",
+		handlers.WithHTMLResponse(
+			handlers.ValidatePassword))
 
+	mux.HandleFunc("GET /login",
+		handlers.WithHTMLResponse(
+			handlers.WithCurrentUser(
+				handlers.AnonymousRequired(
+					templ.Handler(
+						views.Layout(
+							views.LoginForm(),
+							"Radiologist AI. Login.")).
+						ServeHTTP))))
+	mux.HandleFunc("POST /login",
+		handlers.WithHTMLResponse(
+			handlers.WithCurrentUser(
+				handlers.AnonymousRequired(
+					handlers.PostLogin))))
+
+	mux.HandleFunc("POST /logout",
+		handlers.WithCurrentUser(
+			handlers.AuthRequired(
+				handlers.PostLogout)))
+
+	// technical
+	mux.HandleFunc("GET /internal_server_error",
+		handlers.WithHTMLResponse(
+			templ.Handler(
+				views.Layout(
+					views.InternalError(),
+					"Internal Error")).
+				ServeHTTP))
+
+	mux.HandleFunc("GET /", handlers.WithHTMLResponse(
+		handlers.WithCurrentUser(
+			templ.Handler(
+				views.Layout(
+					views.NotFound(),
+					"404")).
+				ServeHTTP)))
 	return mux, nil
 }
diff --git a/internal/app/migrations/20240501092910_add_user_models.sql b/internal/app/migrations/20240501092910_add_user_models.sql
new file mode 100644
index 0000000000000000000000000000000000000000..50bc3b40abedde1c9fcf8f07ed09d78f8642579e
--- /dev/null
+++ b/internal/app/migrations/20240501092910_add_user_models.sql
@@ -0,0 +1,58 @@
+-- +goose Up
+-- +goose StatementBegin
+CREATE TABLE IF NOT EXISTS reports(
+                          id bigserial PRIMARY KEY,
+                          patient_id UUID NOT NULL,
+                          image_path TEXT NOT NULL,
+                          report_text TEXT NOT NULL,
+                          approved BOOLEAN NOT NULL,
+                          created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                          updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS patients(
+                           id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+                           user_id BIGINT NULL,
+                           creator_id BIGINT NOT NULL,
+                           "name" TEXT NULL,
+                           patient_identifier TEXT NULL,
+                           created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                           updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS users(
+                        id bigserial PRIMARY KEY,
+                        first_name TEXT NOT NULL,
+                        last_name TEXT NOT NULL,
+                        email TEXT NOT NULL UNIQUE,
+                        password_hash BYTEA NOT NULL,
+                        is_doctor BOOLEAN NOT NULL,
+                        created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                        updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+ALTER TABLE
+    patients ADD CONSTRAINT patients_creator_id_foreign FOREIGN KEY(creator_id) REFERENCES users(id);
+ALTER TABLE
+    reports ADD CONSTRAINT reports_patient_id_foreign FOREIGN KEY(patient_id) REFERENCES patients(id);
+ALTER TABLE
+    patients ADD CONSTRAINT patients_user_id_foreign FOREIGN KEY(user_id) REFERENCES users(id);
+
+CREATE OR REPLACE FUNCTION update_modified_column()
+RETURNS TRIGGER AS $$
+BEGIN
+NEW.updated_at = CURRENT_TIMESTAMP;
+RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+CREATE TRIGGER update_modified_time_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
+CREATE TRIGGER update_modified_time_patients BEFORE UPDATE ON patients FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
+CREATE TRIGGER update_modified_time_reports BEFORE UPDATE ON reports FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+DROP TABLE CASCADE IF EXISTS reports;
+DROP TABLE CASCADE IF EXISTS patients;
+DROP TABLE CASCADE IF EXISTS users;
+-- +goose StatementEnd
diff --git a/internal/app/users/usersrepo/usersrepo.go b/internal/app/users/usersrepo/usersrepo.go
index 200a6dc3623a7c3b90f447d7aeec626cc8184b4c..88a4c04eb63a9652491ad3038d6dc65e05bf2cf2 100644
--- a/internal/app/users/usersrepo/usersrepo.go
+++ b/internal/app/users/usersrepo/usersrepo.go
@@ -2,9 +2,13 @@ package usersrepo
 
 import (
 	"context"
+	"database/sql"
 	"errors"
+	"fmt"
 	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
 	"github.com/radiologist-ai/web-app/internal/domain"
+	"github.com/radiologist-ai/web-app/internal/domain/customerrors"
 	"github.com/rs/zerolog"
 )
 
@@ -26,12 +30,67 @@ func New(logger *zerolog.Logger, db *sqlx.DB) (*Repo, error) {
 	}, nil
 }
 
-func (r *Repo) SelectByEmail(ctx context.Context, email string) (user domain.UserRepoModel, err error) {
-	//TODO implement me
-	panic("implement me")
+func (r *Repo) SelectByEmail(ctx context.Context, email string) (user domain.UserRepoModel, ok bool, err error) {
+	q := `SELECT id, first_name, last_name, email, password_hash, is_doctor, created_at, updated_at FROM users WHERE email = $1 LIMIT 1`
+	if err := r.db.QueryRowxContext(ctx, q, email).StructScan(&user); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return domain.UserRepoModel{}, false, nil
+		}
+		return domain.UserRepoModel{}, false, err
+	}
+	return user, true, nil
 }
 
 func (r *Repo) InsertOne(ctx context.Context, model domain.UserRepoModel) (user domain.UserRepoModel, err error) {
-	//TODO implement me
-	panic("implement me")
+	tx, err := r.db.BeginTxx(ctx, nil)
+	if err != nil {
+		r.logger.Error().Err(err).Msg("start transaction")
+		return user, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err)
+	}
+	defer func() {
+		if err != nil {
+			errTx := tx.Rollback()
+			if errTx != nil {
+				r.logger.Error().Err(errTx).Msg("rollback transaction")
+			}
+		}
+	}()
+	if user, err = r.insertUser(ctx, tx, model); err != nil {
+		var pqErr *pq.Error
+		if errors.As(err, &pqErr) && pqErr.Code == "23505" {
+			return domain.UserRepoModel{}, customerrors.ValidationErrorEmailAlreadyInUse
+		}
+		return
+	}
+	if !model.IsDoctor {
+		if err = r.insertSelfPatient(ctx, tx, user.ID, fmt.Sprintf("%s %s", user.FirstName, user.LastName)); err != nil {
+			return
+		}
+	}
+	if err = tx.Commit(); err != nil {
+		return domain.UserRepoModel{}, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err)
+	}
+
+	return user, nil
+}
+
+func (r *Repo) insertUser(ctx context.Context, tx *sqlx.Tx, model domain.UserRepoModel) (user domain.UserRepoModel, err error) {
+	q := `Insert into users
+    (email, first_name, last_name, password_hash, is_doctor) values 
+    ($1, $2, $3, $4, $5)
+    RETURNING id, email, first_name, last_name, password_hash, is_doctor, created_at, updated_at`
+	if err = tx.QueryRowxContext(ctx, q, model.Email, model.FirstName, model.LastName, model.PasswordHash, model.IsDoctor).StructScan(&user); err != nil {
+		r.logger.Error().Err(err).Msg("insert user")
+		return domain.UserRepoModel{}, fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err)
+	}
+	return user, nil
+}
+
+func (r *Repo) insertSelfPatient(ctx context.Context, tx *sqlx.Tx, userID int, name string) error {
+	q := `INSERT INTO patients (user_id, creator_id, name) VALUES ($1, $2, $3)`
+	_, err := tx.ExecContext(ctx, q, userID, userID, name)
+	if err != nil {
+		return fmt.Errorf("%w%w", customerrors.InternalErrorSQL, err)
+	}
+	return nil
 }
diff --git a/internal/app/users/usersservice/dto.go b/internal/app/users/usersservice/dto.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca9e6cc5f653e388e1b23b3ec496aeeab927e5db
--- /dev/null
+++ b/internal/app/users/usersservice/dto.go
@@ -0,0 +1,20 @@
+package usersservice
+
+import (
+	"github.com/radiologist-ai/web-app/internal/domain"
+	"golang.org/x/crypto/bcrypt"
+)
+
+func (s *Service) userFormToUserRepoModel(in domain.UserForm) (domain.UserRepoModel, error) {
+	var out domain.UserRepoModel
+	out.FirstName = in.FirstName
+	out.LastName = in.LastName
+	out.Email = in.Email
+	out.IsDoctor = in.IsDoctor
+	var err error
+	out.PasswordHash, err = bcrypt.GenerateFromPassword([]byte(in.Password), 10)
+	if err != nil {
+		return domain.UserRepoModel{}, err
+	}
+	return out, nil
+}
diff --git a/internal/app/users/usersservice/usersservice.go b/internal/app/users/usersservice/usersservice.go
index 1721ed7bbaf70d0950386bad60fd96c92981ec2a..05eb48df7669f788c4e52bff1b9568ef2de8b85f 100644
--- a/internal/app/users/usersservice/usersservice.go
+++ b/internal/app/users/usersservice/usersservice.go
@@ -3,8 +3,11 @@ package usersservice
 import (
 	"context"
 	"errors"
+	"github.com/golang-jwt/jwt/v5"
 	"github.com/radiologist-ai/web-app/internal/domain"
+	"github.com/radiologist-ai/web-app/internal/domain/customerrors"
 	"github.com/rs/zerolog"
+	"time"
 )
 
 type Service struct {
@@ -22,22 +25,55 @@ func New(logger *zerolog.Logger, repo domain.UsersRepository) (*Service, error)
 	return &Service{logger: logger, repo: repo}, nil
 }
 
-func (s *Service) GenerateToken(ctx context.Context, email string) (token string, err error) {
-	//TODO implement me
-	panic("implement me")
+func (s *Service) GenerateToken(secret []byte, email string) (token string, err error) {
+	payload := jwt.MapClaims{
+		"sub": email,
+		"exp": time.Now().Add(time.Hour * 24 * 365).Unix(),
+	}
+
+	jwToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
+
+	token, err = jwToken.SignedString(secret)
+	if err != nil {
+		return "", err
+	}
+	return token, nil
 }
 
-func (s *Service) ValidateToken(ctx context.Context, token string) (email string, err error) {
-	//TODO implement me
-	panic("implement me")
+func (s *Service) ValidateToken(secret []byte, token string) (email string, err error) {
+	jwToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { return secret, nil })
+	if err != nil {
+		return "", err
+	}
+	claims, ok := jwToken.Claims.(jwt.MapClaims)
+	if !ok || !jwToken.Valid {
+		return "", customerrors.ValidationErrorJWT
+	}
+	email, err = claims.GetSubject()
+	if err != nil {
+		return "", err
+	}
+	return email, nil
 }
 
-func (s *Service) GetByEmail(ctx context.Context, email string) (user domain.UserRepoModel, err error) {
-	//TODO implement me
-	panic("implement me")
+func (s *Service) GetByEmail(ctx context.Context, email string) (user domain.UserRepoModel, ok bool, err error) {
+	if user, ok, err = s.repo.SelectByEmail(ctx, email); err != nil {
+		return domain.UserRepoModel{}, false, err
+	}
+	return
 }
 
-func (s *Service) CreateOne(ctx context.Context, user domain.UserRepoModel) (domain.UserRepoModel, error) {
-	//TODO implement me
-	panic("implement me")
+func (s *Service) CreateOne(ctx context.Context, user domain.UserForm) (domain.UserRepoModel, error) {
+	if err := s.validateRegisterForm(user); err != nil {
+		return domain.UserRepoModel{}, err
+	}
+	repoModel, err := s.userFormToUserRepoModel(user)
+	if err != nil {
+		return domain.UserRepoModel{}, err
+	}
+	newUser, err := s.repo.InsertOne(ctx, repoModel)
+	if err != nil {
+		return domain.UserRepoModel{}, err
+	}
+	return newUser, nil
 }
diff --git a/internal/app/users/usersservice/validation.go b/internal/app/users/usersservice/validation.go
new file mode 100644
index 0000000000000000000000000000000000000000..066f69db8ef1328c78d3fe5652f8453f4658aa0b
--- /dev/null
+++ b/internal/app/users/usersservice/validation.go
@@ -0,0 +1,56 @@
+package usersservice
+
+import (
+	"github.com/radiologist-ai/web-app/internal/domain"
+	"github.com/radiologist-ai/web-app/internal/domain/customerrors"
+	"net/mail"
+	"unicode"
+)
+
+func (s *Service) ValidatePassword(pwd string) error {
+	runes := []rune(pwd)
+	if len(runes) < 8 {
+		return customerrors.ValidationErrorPasswordTooShort
+	}
+	if len(runes) > 64 {
+		return customerrors.ValidationErrorPasswordTooLong
+	}
+	var (
+		containsLetter bool
+		containsDigit  bool
+	)
+	for _, r := range runes {
+		if unicode.IsDigit(r) {
+			containsDigit = true
+		} else if unicode.IsLetter(r) {
+			containsLetter = true
+		} else {
+			return customerrors.ValidationErrorPasswordUnacceptableCharacters
+		}
+	}
+	if !containsLetter {
+		return customerrors.ValidationErrorPasswordNoLetters
+	}
+	if !containsDigit {
+		return customerrors.ValidationErrorPasswordNoDigits
+	}
+	return nil
+}
+
+// TODO check if email already in use
+func (s *Service) validateRegisterForm(form domain.UserForm) error {
+	if form.LastName == "" {
+		return customerrors.ValidationErrorLastNameEmpty
+	}
+	if form.FirstName == "" {
+		return customerrors.ValidationErrorFirstNameEmpty
+	}
+	if _, err := mail.ParseAddress(form.Email); err != nil {
+		return customerrors.ValidationErrorEmailInvalid
+	}
+	if err := s.ValidatePassword(form.Password); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index f48b74914b8a59470f636d661af54cee3789f21e..bb8680b00d3734115870e109366660cd4553b1bc 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,21 +1,38 @@
 package config
 
 type Config struct {
-	Server Server
+	Server   Server
+	Database Database
 }
 
-type Server struct {
-	ListenAddr string `yaml:"listen_addr"`
-	Port       int    `yaml:"port"`
-	Secret     string `yaml:"secret"`
-}
+type (
+	Server struct {
+		ListenAddr string `yaml:"listen_addr"`
+		Port       int    `yaml:"port"`
+		Secret     string `yaml:"secret"`
+	}
+	Database struct {
+		Host     string `yaml:"db_host"`
+		Port     int    `yaml:"db_port"`
+		Username string `yaml:"db_username"`
+		Password string `yaml:"db_password"`
+		Database string `yaml:"db_database"`
+	}
+)
 
 func GetConfig() Config {
 	return Config{
 		Server: Server{
 			ListenAddr: "0.0.0.0",
-			Port:       80,
+			Port:       5000,
 			Secret:     "secret",
 		},
+		Database: Database{
+			Host:     "localhost",
+			Port:     5432,
+			Username: "postgres",
+			Password: "password",
+			Database: "aidb",
+		},
 	}
 }
diff --git a/internal/domain/const.go b/internal/domain/const.go
new file mode 100644
index 0000000000000000000000000000000000000000..f0b88b831d4c34b177375bdf29dbbbfe19b87565
--- /dev/null
+++ b/internal/domain/const.go
@@ -0,0 +1,6 @@
+package domain
+
+const (
+	CurrentUserCtxKey  = "current_user"
+	AuthTokenCookieKey = "auth_token"
+)
diff --git a/internal/domain/customerrors/internal.go b/internal/domain/customerrors/internal.go
new file mode 100644
index 0000000000000000000000000000000000000000..623613667b094b8552b7292d114b32d9bed9ce6d
--- /dev/null
+++ b/internal/domain/customerrors/internal.go
@@ -0,0 +1,11 @@
+package customerrors
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	InternalError    = errors.New("internal error. ")
+	InternalErrorSQL = fmt.Errorf("%wfailed to execute sql query. ", InternalError)
+)
diff --git a/internal/domain/customerrors/validation.go b/internal/domain/customerrors/validation.go
new file mode 100644
index 0000000000000000000000000000000000000000..29f679492d6060dd95ff1a98981473f28a56efed
--- /dev/null
+++ b/internal/domain/customerrors/validation.go
@@ -0,0 +1,23 @@
+package customerrors
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	ValidationError                               = errors.New("validation error. ")
+	ValidationErrorPassword                       = fmt.Errorf("%winvalid password. ", ValidationError)
+	ValidationErrorEmailInvalid                   = fmt.Errorf("%winvalid email. ", ValidationError)
+	ValidationErrorEmailAlreadyInUse              = fmt.Errorf("%wemail already in use. ", ValidationError)
+	ValidationErrorPasswordTooShort               = fmt.Errorf("%wpassword must contain at least 8 characters. ", ValidationErrorPassword)
+	ValidationErrorPasswordTooLong                = fmt.Errorf("%wpassword can't contain more than 64 characters. ", ValidationErrorPassword)
+	ValidationErrorPasswordUnacceptableCharacters = fmt.Errorf("%wpassword can contain only latin letters and digits. ", ValidationErrorPassword)
+	ValidationErrorPasswordNoLetters              = fmt.Errorf("%wpassword must containat least 1 letter. ", ValidationErrorPassword)
+	ValidationErrorPasswordNoDigits               = fmt.Errorf("%wpassword must contain at least 1 digit. ", ValidationErrorPassword)
+	ValidationErrorFirstName                      = fmt.Errorf("%winvalid first name. ", ValidationError)
+	ValidationErrorLastName                       = fmt.Errorf("%winvalid last name. ", ValidationError)
+	ValidationErrorFirstNameEmpty                 = fmt.Errorf("%wempty first name. ", ValidationErrorFirstName)
+	ValidationErrorLastNameEmpty                  = fmt.Errorf("%wempty last name. ", ValidationErrorLastName)
+	ValidationErrorJWT                            = fmt.Errorf("%winvalid JWT. ", ValidationError)
+)
diff --git a/internal/domain/users.go b/internal/domain/users.go
index 28a1a099a84555e00566b9d903f0df38f3399b2f..2b4a130496a6e4b6af78074e7071427d82f29492 100644
--- a/internal/domain/users.go
+++ b/internal/domain/users.go
@@ -1,29 +1,40 @@
 package domain
 
-import "context"
+import (
+	"context"
+	"time"
+)
 
 type UsersService interface {
 	AuthService
-	GetByEmail(ctx context.Context, email string) (user UserRepoModel, err error)
-	CreateOne(ctx context.Context, user UserRepoModel) (UserRepoModel, error)
+	UsersValidator
+	GetByEmail(ctx context.Context, email string) (user UserRepoModel, ok bool, err error)
+	CreateOne(ctx context.Context, user UserForm) (UserRepoModel, error)
 }
 
 type UsersRepository interface {
-	SelectByEmail(ctx context.Context, email string) (user UserRepoModel, err error)
+	SelectByEmail(ctx context.Context, email string) (user UserRepoModel, ok bool, err error)
 	InsertOne(ctx context.Context, model UserRepoModel) (user UserRepoModel, err error)
 }
 
 type AuthService interface {
-	GenerateToken(ctx context.Context, email string) (token string, err error)
-	ValidateToken(ctx context.Context, token string) (email string, err error)
+	GenerateToken(secret []byte, email string) (token string, err error)
+	ValidateToken(secret []byte, token string) (email string, err error)
+}
+
+type UsersValidator interface {
+	ValidatePassword(password string) error
 }
 
 type UserRepoModel struct {
-	ID           int
-	FirstName    string
-	LastName     string
-	Email        string
-	PasswordHash []byte
+	ID           int       `db:"id"`
+	FirstName    string    `db:"first_name"`
+	LastName     string    `db:"last_name"`
+	Email        string    `db:"email"`
+	PasswordHash []byte    `db:"password_hash"`
+	IsDoctor     bool      `db:"is_doctor"`
+	CreatedAt    time.Time `db:"created_at"`
+	UpdatedAt    time.Time `db:"updated_at"`
 }
 
 type UserForm struct {
@@ -31,4 +42,5 @@ type UserForm struct {
 	LastName  string
 	Email     string
 	Password  string
+	IsDoctor  bool
 }
diff --git a/internal/views/auth.templ b/internal/views/auth.templ
new file mode 100644
index 0000000000000000000000000000000000000000..6d1ec0dd54c8c354daa74ac1721976196b617f0d
--- /dev/null
+++ b/internal/views/auth.templ
@@ -0,0 +1,151 @@
+package views
+
+
+templ EmailInput(isValidClass, val, feedback string) {
+    <div class="col-md-6" id="emailInput">
+            <label for="inputEmail" class="form-label">Email</label>
+            <input type="email" class={ "form-control", isValidClass } name="email" id="inputEmail" value={ val }
+            hx-post="/validate/email"
+            hx-trigger="change"
+            hx-target="#emailInput"
+            hx-swap="outerHTML"
+            required/>
+            <div class="invalid-feedback" id="feedbackEmail">
+                 { feedback }
+            </div>
+    </div>
+}
+
+templ PasswordInput(isValidClass, val, feedback string) {
+    <div class="col-md-6" id="passwordInput">
+        <label for="inputPassword" class="form-label">Password</label>
+        <input type="password" class={ "form-control", isValidClass } name="password" id="inputPassword" value={ val }
+        hx-post="/validate/password"
+        hx-trigger="change"
+        hx-target="#passwordInput"
+        hx-swap="outerHTML"
+        required/>
+        <div class="invalid-feedback" id="feedbackPassword">
+            { feedback }
+        </div>
+    </div>
+}
+
+
+templ RegistrationForm() {
+    { children... }
+    <form id="registerForm" class="row g-3 needs-validation"
+    action="/register"
+    method="POST"
+    novalidate>
+      @EmailInput("", "", "Invalid Email")
+      @PasswordInput("", "", "Invalid Password")
+      <div class="col-md-6">
+        <label for="validationCustom01" class="form-label">First name</label>
+        <input type="text" class="form-control" name="firstName" id="validationCustom01" value="" required/>
+        <div class="invalid-feedback">
+          Field cannot be empty
+        </div>
+
+      </div>
+      <div class="col-md-6">
+        <label for="validationCustom02" class="form-label">Last name</label>
+        <input type="text" class="form-control" name="lastName" id="validationCustom02" value="" required/>
+        <div class="invalid-feedback">
+          Field cannot be empty
+        </div>
+      </div>
+      <div class="col-md-6">
+      <div class="form-check form-switch">
+        <input class="form-check-input" name="isDoctor" type="checkbox" id="flexSwitchCheckDefault"/>
+        <label class="form-check-label" for="flexSwitchCheckDefault">I am a Doctor</label>
+      </div>
+      </div>
+      <div class="col-12">
+        <button type="submit" class="btn btn-primary">Sign up</button>
+      </div>
+    </form>
+    <script>
+      (() => {
+        'use strict'
+
+        // Fetch all the forms we want to apply custom Bootstrap validation styles to
+        const forms = document.querySelectorAll('.needs-validation')
+
+        // Loop over them and prevent submission
+        Array.from(forms).forEach(form => {
+          form.addEventListener('submit', event => {
+            const form = event.target;
+
+            let flag = false;
+            Array.from(form.elements).forEach(el => {
+                if (el.classList.contains('is-invalid')) {
+                   el.classList.add('border-danger')
+                   flag = true
+                }
+            })
+
+            if (!form.checkValidity() || flag) {
+              event.preventDefault();
+              event.stopPropagation();
+            }
+
+            form.classList.add('was-validated');
+          }, false)
+        })
+      })()
+    </script>
+}
+
+templ RegistrationFormBad(errs ...string) {
+    @RegistrationForm() {
+        <div>
+            <ul>
+            for _, err := range errs {
+                <li> { err } </li>
+            }
+            </ul>
+        </div>
+    }
+}
+
+templ LoginForm() {
+    { children... }
+    <form id="loginForm" class="row g-3 needs-validation"
+    action="/login"
+    method="POST">
+        <div class="col-md-12" id="emailInputLogin">
+                <label for="inputEmailLogin" class="form-label">Email</label>
+                <input type="email" class="form-control" name="email" id="inputEmailLogin" required/>
+                <div class="invalid-feedback" id="feedbackEmail">
+                     Invalid Email
+                </div>
+        </div>
+        <div class="col-md-12" id="passwordInputLogin">
+            <label for="inputPasswordLogin" class="form-label">Password</label>
+            <input type="password" class="form-control" name="password" id="inputPasswordLogin" required/>
+            <div class="invalid-feedback" id="feedbackPassword">
+                Invalid Password
+            </div>
+        </div>
+        <div class="col-12">
+            <button type="submit" class="btn btn-primary">Log in</button>
+        </div>
+    </form>
+}
+
+templ LoginFormUserDoesntExist() {
+    @LoginForm() {
+        <div class="alert alert-warning" role="alert">
+          User for provided credentials doesn't exist. <a href="/register" class="alert-link">Sign up?</a>
+        </div>
+    }
+}
+
+templ LoginFormWrongPassword() {
+    @LoginForm() {
+        <div class="alert alert-danger" role="alert">
+          Wrong Password! Try again.
+        </div>
+    }
+}
diff --git a/internal/views/auth_templ.go b/internal/views/auth_templ.go
new file mode 100644
index 0000000000000000000000000000000000000000..4b26060b35414be978736758958a715c0153f936
--- /dev/null
+++ b/internal/views/auth_templ.go
@@ -0,0 +1,363 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.663
+package views
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+func EmailInput(isValidClass, val, feedback string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var1 == nil {
+			templ_7745c5c3_Var1 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-md-6\" id=\"emailInput\"><label for=\"inputEmail\" class=\"form-label\">Email</label> ")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var2 = []any{"form-control", isValidClass}
+		templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<input type=\"email\" class=\"")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var3 string
+		templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 1, Col: 0}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" name=\"email\" id=\"inputEmail\" value=\"")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var4 string
+		templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(val)
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 7, Col: 111}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-post=\"/validate/email\" hx-trigger=\"change\" hx-target=\"#emailInput\" hx-swap=\"outerHTML\" required><div class=\"invalid-feedback\" id=\"feedbackEmail\">")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var5 string
+		templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(feedback)
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 14, Col: 27}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func PasswordInput(isValidClass, val, feedback string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var6 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var6 == nil {
+			templ_7745c5c3_Var6 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-md-6\" id=\"passwordInput\"><label for=\"inputPassword\" class=\"form-label\">Password</label> ")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var7 = []any{"form-control", isValidClass}
+		templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<input type=\"password\" class=\"")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var8 string
+		templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 1, Col: 0}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" name=\"password\" id=\"inputPassword\" value=\"")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var9 string
+		templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(val)
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 22, Col: 116}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-post=\"/validate/password\" hx-trigger=\"change\" hx-target=\"#passwordInput\" hx-swap=\"outerHTML\" required><div class=\"invalid-feedback\" id=\"feedbackPassword\">")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var10 string
+		templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(feedback)
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 29, Col: 22}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func RegistrationForm() templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var11 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var11 == nil {
+			templ_7745c5c3_Var11 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Err = templ_7745c5c3_Var11.Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form id=\"registerForm\" class=\"row g-3 needs-validation\" action=\"/register\" method=\"POST\" novalidate>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = EmailInput("", "", "Invalid Email").Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = PasswordInput("", "", "Invalid Password").Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-md-6\"><label for=\"validationCustom01\" class=\"form-label\">First name</label> <input type=\"text\" class=\"form-control\" name=\"firstName\" id=\"validationCustom01\" value=\"\" required><div class=\"invalid-feedback\">Field cannot be empty\r</div></div><div class=\"col-md-6\"><label for=\"validationCustom02\" class=\"form-label\">Last name</label> <input type=\"text\" class=\"form-control\" name=\"lastName\" id=\"validationCustom02\" value=\"\" required><div class=\"invalid-feedback\">Field cannot be empty\r</div></div><div class=\"col-md-6\"><div class=\"form-check form-switch\"><input class=\"form-check-input\" name=\"isDoctor\" type=\"checkbox\" id=\"flexSwitchCheckDefault\"> <label class=\"form-check-label\" for=\"flexSwitchCheckDefault\">I am a Doctor</label></div></div><div class=\"col-12\"><button type=\"submit\" class=\"btn btn-primary\">Sign up</button></div></form><script>\r\n      (() => {\r\n        'use strict'\r\n\r\n        // Fetch all the forms we want to apply custom Bootstrap validation styles to\r\n        const forms = document.querySelectorAll('.needs-validation')\r\n\r\n        // Loop over them and prevent submission\r\n        Array.from(forms).forEach(form => {\r\n          form.addEventListener('submit', event => {\r\n            const form = event.target;\r\n\r\n            let flag = false;\r\n            Array.from(form.elements).forEach(el => {\r\n                if (el.classList.contains('is-invalid')) {\r\n                   el.classList.add('border-danger')\r\n                   flag = true\r\n                }\r\n            })\r\n\r\n            if (!form.checkValidity() || flag) {\r\n              event.preventDefault();\r\n              event.stopPropagation();\r\n            }\r\n\r\n            form.classList.add('was-validated');\r\n          }, false)\r\n        })\r\n      })()\r\n    </script>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func RegistrationFormBad(errs ...string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var12 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var12 == nil {
+			templ_7745c5c3_Var12 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Var13 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+			if !templ_7745c5c3_IsBuffer {
+				templ_7745c5c3_Buffer = templ.GetBuffer()
+				defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><ul>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			for _, err := range errs {
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li>")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				var templ_7745c5c3_Var14 string
+				templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(err)
+				if templ_7745c5c3_Err != nil {
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\auth.templ`, Line: 105, Col: 26}
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			if !templ_7745c5c3_IsBuffer {
+				_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+			}
+			return templ_7745c5c3_Err
+		})
+		templ_7745c5c3_Err = RegistrationForm().Render(templ.WithChildren(ctx, templ_7745c5c3_Var13), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func LoginForm() templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var15 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var15 == nil {
+			templ_7745c5c3_Var15 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Err = templ_7745c5c3_Var15.Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form id=\"loginForm\" class=\"row g-3 needs-validation\" action=\"/login\" method=\"POST\"><div class=\"col-md-12\" id=\"emailInputLogin\"><label for=\"inputEmailLogin\" class=\"form-label\">Email</label> <input type=\"email\" class=\"form-control\" name=\"email\" id=\"inputEmailLogin\" required><div class=\"invalid-feedback\" id=\"feedbackEmail\">Invalid Email\r</div></div><div class=\"col-md-12\" id=\"passwordInputLogin\"><label for=\"inputPasswordLogin\" class=\"form-label\">Password</label> <input type=\"password\" class=\"form-control\" name=\"password\" id=\"inputPasswordLogin\" required><div class=\"invalid-feedback\" id=\"feedbackPassword\">Invalid Password\r</div></div><div class=\"col-12\"><button type=\"submit\" class=\"btn btn-primary\">Log in</button></div></form>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func LoginFormUserDoesntExist() templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var16 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var16 == nil {
+			templ_7745c5c3_Var16 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Var17 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+			if !templ_7745c5c3_IsBuffer {
+				templ_7745c5c3_Buffer = templ.GetBuffer()
+				defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"alert alert-warning\" role=\"alert\">User for provided credentials doesn't exist. <a href=\"/register\" class=\"alert-link\">Sign up?</a></div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			if !templ_7745c5c3_IsBuffer {
+				_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+			}
+			return templ_7745c5c3_Err
+		})
+		templ_7745c5c3_Err = LoginForm().Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func LoginFormWrongPassword() templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var18 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var18 == nil {
+			templ_7745c5c3_Var18 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Var19 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+			if !templ_7745c5c3_IsBuffer {
+				templ_7745c5c3_Buffer = templ.GetBuffer()
+				defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"alert alert-danger\" role=\"alert\">Wrong Password! Try again.\r</div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			if !templ_7745c5c3_IsBuffer {
+				_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+			}
+			return templ_7745c5c3_Err
+		})
+		templ_7745c5c3_Err = LoginForm().Render(templ.WithChildren(ctx, templ_7745c5c3_Var19), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
diff --git a/internal/views/base_layout.templ b/internal/views/base_layout.templ
new file mode 100644
index 0000000000000000000000000000000000000000..33ed9cb411ed1f798eaf307ea1abfcce890d7799
--- /dev/null
+++ b/internal/views/base_layout.templ
@@ -0,0 +1,84 @@
+package views
+
+import "github.com/radiologist-ai/web-app/internal/domain"
+
+func GetCurrentUser(ctx context.Context) *domain.UserRepoModel {
+	currentUser, ok := ctx.Value(domain.CurrentUserCtxKey).(domain.UserRepoModel)
+	if !ok {
+		return nil
+	}
+	return &currentUser
+}
+
+
+templ header(title string) {
+	<head>
+		<title>{ title }</title>
+		<meta charset="UTF-8"/>
+		<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+		<script src="https://unpkg.com/htmx.org@1.9.12"></script>
+		<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
+		<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"/>
+        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/> <!-- load fontawesome -->
+	</head>
+}
+
+templ footer() {
+	<footer class="bg-blue-600 p-4"></footer>
+}
+
+templ Nav(user *domain.UserRepoModel){
+    <nav id="mainNav" class="navbar navbar-expand-lg navbar-light bg-light">
+      <div class="container-fluid">
+        <a class="navbar-brand" href="/">Radiologist AI</a>
+        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
+          <span class="navbar-toggler-icon"></span>
+        </button>
+        <div class="collapse navbar-collapse justify-content-end" id="navbarNavDropdown">
+          <ul class="navbar-nav">
+            <li class="nav-item">
+              <a class="nav-link active" aria-current="page" href="/home">Home</a>
+            </li>
+            if user == nil {
+                <li class="nav-item">
+                  <a class="nav-link" href="/register">Register</a>
+                </li>
+                <li class="nav-item">
+                  <a class="nav-link" href="/login">Log In</a>
+                </li>
+            } else {
+                <li class="nav-item dropdown">
+                    <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+                        { user.FirstName }
+                    </a>
+                    <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
+                    <li><a class="dropdown-item" href="/users/{user.ID}/reports">Library</a></li>
+                    <li><a class="dropdown-item" href="/settings">Settings</a></li>
+                    <li><a class="dropdown-item btn-outline-danger" href="#"
+                        hx-post="/logout"
+                        hx-trigger="click"
+                        hx-swap="outerHTML"
+                        hx-target="#mainNav"
+                        >Log out</a></li>
+                    </ul>
+                </li>
+            }
+          </ul>
+        </div>
+      </div>
+    </nav>
+
+}
+
+templ Layout(contents templ.Component, title string) {
+	@header(title)
+	<body class="flex flex-col h-full">
+	@Nav(GetCurrentUser(ctx))
+	<main class="flex-1">
+	<div class="container-fluid">
+		@contents
+	</div>
+	</main>
+	@footer()
+	</body>
+}
\ No newline at end of file
diff --git a/internal/views/base_layout_templ.go b/internal/views/base_layout_templ.go
new file mode 100644
index 0000000000000000000000000000000000000000..87c27e3c5e0248a531cab29f3cf5778e61f1165b
--- /dev/null
+++ b/internal/views/base_layout_templ.go
@@ -0,0 +1,186 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.663
+package views
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+import "github.com/radiologist-ai/web-app/internal/domain"
+
+func GetCurrentUser(ctx context.Context) *domain.UserRepoModel {
+	currentUser, ok := ctx.Value(domain.CurrentUserCtxKey).(domain.UserRepoModel)
+	if !ok {
+		return nil
+	}
+	return &currentUser
+}
+
+func header(title string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var1 == nil {
+			templ_7745c5c3_Var1 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><title>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		var templ_7745c5c3_Var2 string
+		templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+		if templ_7745c5c3_Err != nil {
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\base_layout.templ`, Line: 16, Col: 16}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><script src=\"https://unpkg.com/htmx.org@1.9.12\"></script><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM\" crossorigin=\"anonymous\"></script><link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC\" crossorigin=\"anonymous\"><link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css\"><!-- load fontawesome --></head>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func footer() templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var3 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var3 == nil {
+			templ_7745c5c3_Var3 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"bg-blue-600 p-4\"></footer>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func Nav(user *domain.UserRepoModel) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var4 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var4 == nil {
+			templ_7745c5c3_Var4 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav id=\"mainNav\" class=\"navbar navbar-expand-lg navbar-light bg-light\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"/\">Radiologist AI</a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNavDropdown\" aria-controls=\"navbarNavDropdown\" aria-expanded=\"false\" aria-label=\"Toggle navigation\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse justify-content-end\" id=\"navbarNavDropdown\"><ul class=\"navbar-nav\"><li class=\"nav-item\"><a class=\"nav-link active\" aria-current=\"page\" href=\"/home\">Home</a></li>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if user == nil {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"nav-item\"><a class=\"nav-link\" href=\"/register\">Register</a></li><li class=\"nav-item\"><a class=\"nav-link\" href=\"/login\">Log In</a></li>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+		} else {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"nav-item dropdown\"><a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"navbarDropdownMenuLink\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var5 string
+			templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.FirstName)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal\views\base_layout.templ`, Line: 52, Col: 40}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><ul class=\"dropdown-menu\" aria-labelledby=\"navbarDropdownMenuLink\"><li><a class=\"dropdown-item\" href=\"/users/{user.ID}/reports\">Library</a></li><li><a class=\"dropdown-item\" href=\"/settings\">Settings</a></li><li><a class=\"dropdown-item btn-outline-danger\" href=\"#\" hx-post=\"/logout\" hx-trigger=\"click\" hx-swap=\"outerHTML\" hx-target=\"#mainNav\">Log out</a></li></ul></li>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div></div></nav>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func Layout(contents templ.Component, title string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var6 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var6 == nil {
+			templ_7745c5c3_Var6 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Err = header(title).Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"flex flex-col h-full\">")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = Nav(GetCurrentUser(ctx)).Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"flex-1\"><div class=\"container-fluid\">")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = contents.Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></main>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
diff --git a/internal/views/index.templ b/internal/views/index.templ
index affefd43145ef7313b627a20eac2912ccb14f1b3..dd4eeeee136f332ec9b4a2b23193840e2420374c 100644
--- a/internal/views/index.templ
+++ b/internal/views/index.templ
@@ -1,5 +1,15 @@
 package views
 
+import "github.com/radiologist-ai/web-app/internal/domain"
+
+templ UnauthenticatedIndex() {
+<div>asd</div>
+}
+
+templ AuthenticatedIndex(user *domain.UserRepoModel) {
+<div>asd</div>
+}
+
 templ Index() {
     <h1>AI Radiologist</h1>
 }
\ No newline at end of file
diff --git a/internal/views/index_templ.go b/internal/views/index_templ.go
index 44fefa2d06fb49ea223bd6ab8b0ac7941c506fa5..d9116e9e80539ab56d3bff8397ea853873077e2b 100644
--- a/internal/views/index_templ.go
+++ b/internal/views/index_templ.go
@@ -1,6 +1,6 @@
 // Code generated by templ - DO NOT EDIT.
 
-// templ: version: v0.2.648
+// templ: version: v0.2.663
 package views
 
 //lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -10,7 +10,9 @@ import "context"
 import "io"
 import "bytes"
 
-func Index() templ.Component {
+import "github.com/radiologist-ai/web-app/internal/domain"
+
+func UnauthenticatedIndex() templ.Component {
 	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
 		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
 		if !templ_7745c5c3_IsBuffer {
@@ -23,6 +25,54 @@ func Index() templ.Component {
 			templ_7745c5c3_Var1 = templ.NopComponent
 		}
 		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div>asd</div>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func AuthenticatedIndex(user *domain.UserRepoModel) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var2 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var2 == nil {
+			templ_7745c5c3_Var2 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div>asd</div>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		if !templ_7745c5c3_IsBuffer {
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+		}
+		return templ_7745c5c3_Err
+	})
+}
+
+func Index() templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+		if !templ_7745c5c3_IsBuffer {
+			templ_7745c5c3_Buffer = templ.GetBuffer()
+			defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var3 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var3 == nil {
+			templ_7745c5c3_Var3 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1>AI Radiologist</h1>")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
diff --git a/internal/views/internal_error.templ b/internal/views/internal_error.templ
new file mode 100644
index 0000000000000000000000000000000000000000..2213340cc64d06d53e1fe68a3e349a4de18412cd
--- /dev/null
+++ b/internal/views/internal_error.templ
@@ -0,0 +1,7 @@
+package views
+
+templ InternalError() {
+	<p class="h1 text-center">An Error has been occurred on server.</p>
+	<p class="h2 text-center">Try Again Later.</p>
+	<p class="h3 text-center">We are sorry...</p>
+}
diff --git a/internal/views/not_found.templ b/internal/views/not_found.templ
new file mode 100644
index 0000000000000000000000000000000000000000..d4e090dfebbad74934cfc0f046a11415df9b7172
--- /dev/null
+++ b/internal/views/not_found.templ
@@ -0,0 +1,5 @@
+package views
+
+templ NotFound() {
+    <p class="h1 text-center">Requested Page doesn't exist.</p>
+}
\ No newline at end of file