diff --git a/pkg/database/versions.go b/pkg/database/versions.go
index 5a6f11f..f05d230 100644
--- a/pkg/database/versions.go
+++ b/pkg/database/versions.go
@@ -1,12 +1,44 @@
 package database
 
 import (
+	"database/sql"
+	"errors"
 	"fmt"
 	"time"
 
 	"git.adyxax.org/adyxax/tfstated/pkg/model"
 )
 
+func (db *DB) LoadVersionById(id int) (*model.Version, error) {
+	version := model.Version{
+		Id: id,
+	}
+	var (
+		created       int64
+		encryptedData []byte
+	)
+	err := db.QueryRow(
+		`SELECT account_id, state_id, data, lock, created FROM versions WHERE id = ?;`,
+		id).Scan(
+		&version.AccountId,
+		&version.StateId,
+		&encryptedData,
+		&version.Lock,
+		&created)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("failed to load version id %d from database: %w", id, err)
+	}
+	version.Created = time.Unix(created, 0)
+	version.Data, err = db.dataEncryptionKey.DecryptAES256(encryptedData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decrypt version %d data: %w", id, err)
+	}
+	return &version, nil
+}
+
 func (db *DB) LoadVersionsByState(state *model.State) ([]model.Version, error) {
 	rows, err := db.Query(
 		`SELECT account_id, created, data, id, lock
diff --git a/pkg/webui/html/version.html b/pkg/webui/html/version.html
new file mode 100644
index 0000000..b849783
--- /dev/null
+++ b/pkg/webui/html/version.html
@@ -0,0 +1,10 @@
+{{ define "main" }}
+<main class="responsive" id="main">
+  <p>
+    Created by
+    <a href="/users/{{ .Account.Id }}">{{ .Account.Username }}</a>
+    at {{ .Version.Created }}
+  </p>
+  <pre>{{ .VersionData }}</pre>
+</main>
+{{ end }}
diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go
index 4330630..84017dd 100644
--- a/pkg/webui/routes.go
+++ b/pkg/webui/routes.go
@@ -19,5 +19,6 @@ func addRoutes(
 	mux.Handle("GET /states", requireLogin(handleStatesGET(db)))
 	mux.Handle("GET /state/{id}", requireLogin(handleStateGET(db)))
 	mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS))))
+	mux.Handle("GET /version/{id}", requireLogin(handleVersionGET(db)))
 	mux.Handle("GET /", requireLogin(handleIndexGET()))
 }
diff --git a/pkg/webui/version.go b/pkg/webui/version.go
new file mode 100644
index 0000000..04c3e6d
--- /dev/null
+++ b/pkg/webui/version.go
@@ -0,0 +1,58 @@
+package webui
+
+import (
+	"fmt"
+	"html/template"
+	"net/http"
+	"strconv"
+
+	"git.adyxax.org/adyxax/tfstated/pkg/database"
+	"git.adyxax.org/adyxax/tfstated/pkg/model"
+)
+
+var versionTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/version.html"))
+
+func handleVersionGET(db *database.DB) http.Handler {
+	type VersionsData struct {
+		Page
+		Account     *model.Account
+		State       *model.State
+		Version     *model.Version
+		VersionData string
+	}
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		versionIdStr := r.PathValue("id")
+		versionId, err := strconv.Atoi(versionIdStr)
+		if err != nil {
+			errorResponse(w, http.StatusBadRequest, err)
+			return
+		}
+		version, err := db.LoadVersionById(versionId)
+		if err != nil {
+			errorResponse(w, http.StatusInternalServerError, err)
+			return
+		}
+		state, err := db.LoadStateById(version.StateId)
+		if err != nil {
+			errorResponse(w, http.StatusInternalServerError, err)
+			return
+		}
+		account, err := db.LoadAccountById(version.AccountId)
+		if err != nil {
+			errorResponse(w, http.StatusInternalServerError, err)
+			return
+		}
+		versionData := string(version.Data[:])
+		render(w, versionTemplate, http.StatusOK, VersionsData{
+			Page: Page{
+				Precedent: fmt.Sprintf("/state/%d", state.Id),
+				Section:   "versions",
+				Title:     state.Path,
+			},
+			Account:     account,
+			State:       state,
+			Version:     version,
+			VersionData: versionData,
+		})
+	})
+}