diff --git a/pkg/database/sessions.go b/pkg/database/sessions.go
index decba8e..43f9d50 100644
--- a/pkg/database/sessions.go
+++ b/pkg/database/sessions.go
@@ -27,6 +27,14 @@ func (db *DB) CreateSession(account *model.Account) (string, error) {
 	return sessionId.String(), nil
 }
 
+func (db *DB) DeleteSession(session *model.Session) error {
+	_, err := db.Exec(`DELETE FROM sessions WHERE id = ?`, session.Id)
+	if err != nil {
+		return fmt.Errorf("failed to delete session %s: %w", session.Id, err)
+	}
+	return nil
+}
+
 func (db *DB) LoadSessionById(id string) (*model.Session, error) {
 	session := model.Session{
 		Id: id,
diff --git a/pkg/webui/html/logout.html b/pkg/webui/html/logout.html
new file mode 100644
index 0000000..58191c0
--- /dev/null
+++ b/pkg/webui/html/logout.html
@@ -0,0 +1,5 @@
+{{ define "main" }}
+<article>
+  <p>Logout successful</p>
+</article>
+{{ end }}
diff --git a/pkg/webui/logout.go b/pkg/webui/logout.go
new file mode 100644
index 0000000..6a281bb
--- /dev/null
+++ b/pkg/webui/logout.go
@@ -0,0 +1,24 @@
+package webui
+
+import (
+	"html/template"
+	"net/http"
+
+	"git.adyxax.org/adyxax/tfstated/pkg/database"
+	"git.adyxax.org/adyxax/tfstated/pkg/model"
+)
+
+var logoutTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/logout.html"))
+
+func handleLogoutGET(db *database.DB) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session := r.Context().Value(model.SessionContextKey{})
+		err := db.DeleteSession(session.(*model.Session))
+		if err != nil {
+			errorResponse(w, http.StatusInternalServerError, err)
+			return
+		}
+		unsetSesssionCookie(w)
+		render(w, logoutTemplate, http.StatusOK, nil)
+	})
+}
diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go
index 5cf31c0..6ebe90b 100644
--- a/pkg/webui/routes.go
+++ b/pkg/webui/routes.go
@@ -15,6 +15,7 @@ func addRoutes(
 	mux.Handle("GET /healthz", handleHealthz())
 	mux.Handle("GET /login", session(handleLoginGET()))
 	mux.Handle("POST /login", session(handleLoginPOST(db)))
+	mux.Handle("GET /logout", session(requireLogin(handleLogoutGET(db))))
 	mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS))))
 	mux.Handle("GET /", session(requireLogin(handleIndexGET())))
 }
diff --git a/pkg/webui/sessions.go b/pkg/webui/sessions.go
index 6d492d5..2d99871 100644
--- a/pkg/webui/sessions.go
+++ b/pkg/webui/sessions.go
@@ -22,16 +22,7 @@ func sessionsMiddleware(db *database.DB) func(http.Handler) http.Handler {
 			}
 			if err == nil {
 				if len(cookie.Value) != 36 {
-					http.SetCookie(w, &http.Cookie{
-						Name:     cookieName,
-						Value:    "",
-						Quoted:   false,
-						Path:     "/",
-						MaxAge:   0, // remove invalid cookie
-						HttpOnly: true,
-						SameSite: http.SameSiteStrictMode,
-						Secure:   true,
-					})
+					unsetSesssionCookie(w)
 				} else {
 					session, err := db.LoadSessionById(cookie.Value)
 					if err != nil {
@@ -53,3 +44,16 @@ func sessionsMiddleware(db *database.DB) func(http.Handler) http.Handler {
 		})
 	}
 }
+
+func unsetSesssionCookie(w http.ResponseWriter) {
+	http.SetCookie(w, &http.Cookie{
+		Name:     cookieName,
+		Value:    "",
+		Quoted:   false,
+		Path:     "/",
+		MaxAge:   0, // remove invalid cookie
+		HttpOnly: true,
+		SameSite: http.SameSiteStrictMode,
+		Secure:   true,
+	})
+}