diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go
index 2c1dc6d..dcd7159 100644
--- a/pkg/database/accounts.go
+++ b/pkg/database/accounts.go
@@ -54,6 +54,42 @@ func (db *DB) InitAdminAccount() error {
 	})
 }
 
+func (db *DB) LoadAccounts() ([]model.Account, error) {
+	rows, err := db.Query(
+		`SELECT id, username, salt, password_hash, is_admin, created, last_login, settings FROM accounts;`)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load accounts from database: %w", err)
+	}
+	defer rows.Close()
+	accounts := make([]model.Account, 0)
+	for rows.Next() {
+		var (
+			account   model.Account
+			created   int64
+			lastLogin int64
+		)
+		err = rows.Scan(
+			&account.Id,
+			&account.Username,
+			&account.Salt,
+			&account.PasswordHash,
+			&account.IsAdmin,
+			&created,
+			&lastLogin,
+			&account.Settings)
+		if err != nil {
+			return nil, fmt.Errorf("failed to load account from row: %w", err)
+		}
+		account.Created = time.Unix(created, 0)
+		account.LastLogin = time.Unix(lastLogin, 0)
+		accounts = append(accounts, account)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, fmt.Errorf("failed to load accounts from rows: %w", err)
+	}
+	return accounts, nil
+}
+
 func (db *DB) LoadAccountUsernames() (map[string]string, error) {
 	rows, err := db.Query(
 		`SELECT id, username FROM accounts;`)
diff --git a/pkg/database/states.go b/pkg/database/states.go
index d637b14..e213d2c 100644
--- a/pkg/database/states.go
+++ b/pkg/database/states.go
@@ -122,8 +122,8 @@ func (db *DB) LoadStates() ([]model.State, error) {
 	defer rows.Close()
 	states := make([]model.State, 0)
 	for rows.Next() {
-		var state model.State
 		var (
+			state   model.State
 			created int64
 			updated int64
 		)
diff --git a/pkg/webui/accounts.go b/pkg/webui/accounts.go
new file mode 100644
index 0000000..7ca9d39
--- /dev/null
+++ b/pkg/webui/accounts.go
@@ -0,0 +1,31 @@
+package webui
+
+import (
+	"html/template"
+	"net/http"
+
+	"git.adyxax.org/adyxax/tfstated/pkg/database"
+	"git.adyxax.org/adyxax/tfstated/pkg/model"
+)
+
+type AccountsPage struct {
+	ActiveTab int
+	Page      *Page
+	Accounts  []model.Account
+}
+
+var accountsTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/accounts.html"))
+
+func handleAccountsGET(db *database.DB) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		accounts, err := db.LoadAccounts()
+		if err != nil {
+			errorResponse(w, http.StatusInternalServerError, err)
+			return
+		}
+		render(w, accountsTemplates, http.StatusOK, AccountsPage{
+			Page:     makePage(r, &Page{Title: "User Accounts", Section: "accounts"}),
+			Accounts: accounts,
+		})
+	})
+}
diff --git a/pkg/webui/admin.go b/pkg/webui/admin.go
new file mode 100644
index 0000000..54682d7
--- /dev/null
+++ b/pkg/webui/admin.go
@@ -0,0 +1,28 @@
+package webui
+
+import (
+	"fmt"
+	"net/http"
+
+	"git.adyxax.org/adyxax/tfstated/pkg/database"
+	"git.adyxax.org/adyxax/tfstated/pkg/model"
+)
+
+func adminMiddleware(db *database.DB, requireLogin func(http.Handler) http.Handler) func(http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return requireLogin(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			account := r.Context().Value(model.AccountContextKey{})
+			if account == nil {
+				// this could happen if the account was deleted in the short
+				// time between retrieving the session and here
+				http.Redirect(w, r, "/login", http.StatusFound)
+				return
+			}
+			if !account.(*model.Account).IsAdmin {
+				errorResponse(w, http.StatusForbidden, fmt.Errorf("Only administrators can perform this request."))
+				return
+			}
+			next.ServeHTTP(w, r)
+		}))
+	}
+}
diff --git a/pkg/webui/html/accounts.html b/pkg/webui/html/accounts.html
new file mode 100644
index 0000000..f1ded93
--- /dev/null
+++ b/pkg/webui/html/accounts.html
@@ -0,0 +1,35 @@
+{{ define "main" }}
+<main class="responsive" id="main">
+  <div>
+    <div class="tabs">
+      <a data-ui="#explorer"{{ if eq .ActiveTab 0 }} class="active"{{ end }}>User Accounts</a>
+      <a data-ui="#new"{{ if eq .ActiveTab 1 }} class="active"{{ end }}>Create New User Account</a>
+    </div>
+    <div id="explorer" class="page padding{{ if eq .ActiveTab 0 }} active{{ end }}">
+      <table class="clickable-rows no-space">
+        <thead>
+          <tr>
+            <th>Username</th>
+            <th>Created</th>
+            <th>Last Login</th>
+            <th>Is Admin</th>
+          </tr>
+        </thead>
+        <tbody>
+          {{ range .Accounts }}
+          <tr>
+            <td><a href="/accounts/{{ .Id }}">{{ .Username }}</a></td>
+            <td><a href="/accounts/{{ .Id }}">{{ .Created }}</a></td>
+            <td><a href="/accounts/{{ .Id }}">{{ .LastLogin }}</a></td>
+            <td><a href="/accounts/{{ .Id }}">{{ .IsAdmin }}</a></td>
+          </tr>
+          {{ end }}
+        </tbody>
+      </table>
+    </div>
+    <div id="new" class="page padding{{ if eq .ActiveTab 1 }} active{{ end }}">
+      <p>TODO</p>
+    </div>
+  </div>
+</main>
+{{ end }}
diff --git a/pkg/webui/html/base.html b/pkg/webui/html/base.html
index 4ec6565..0d19cf5 100644
--- a/pkg/webui/html/base.html
+++ b/pkg/webui/html/base.html
@@ -20,6 +20,12 @@
   <i>settings</i>
   <span>Settings</span>
 </a>
+{{ if .Page.IsAdmin }}
+<a href="/accounts"{{ if eq .Page.Section "accounts" }} class="fill"{{ end}}>
+  <i>person</i>
+  <span>User Accounts</span>
+</a>
+{{ end }}
 <a href="/logout">
   <i>logout</i>
   <span>Logout</span>
diff --git a/pkg/webui/html/versions.html b/pkg/webui/html/versions.html
index ea65647..8adab56 100644
--- a/pkg/webui/html/versions.html
+++ b/pkg/webui/html/versions.html
@@ -2,7 +2,7 @@
 <main class="responsive" id="main">
   <p>
     Created by
-    <a href="/users/{{ .Account.Id }}">{{ .Account.Username }}</a>
+      <a href="/accounts/{{ .Account.Id }}" class="link underline">{{ .Account.Username }}</a>
     at {{ .Version.Created }}
   </p>
   <div>
diff --git a/pkg/webui/index.go b/pkg/webui/index.go
index 1168098..28a4964 100644
--- a/pkg/webui/index.go
+++ b/pkg/webui/index.go
@@ -8,6 +8,7 @@ import (
 )
 
 type Page struct {
+	IsAdmin   bool
 	LightMode bool
 	Precedent string
 	Section   string
@@ -15,6 +16,8 @@ type Page struct {
 }
 
 func makePage(r *http.Request, page *Page) *Page {
+	account := r.Context().Value(model.AccountContextKey{}).(*model.Account)
+	page.IsAdmin = account.IsAdmin
 	settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings)
 	page.LightMode = settings.LightMode
 	return page
diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go
index 1fce700..2037df6 100644
--- a/pkg/webui/routes.go
+++ b/pkg/webui/routes.go
@@ -12,6 +12,8 @@ func addRoutes(
 ) {
 	requireSession := sessionsMiddleware(db)
 	requireLogin := loginMiddleware(db, requireSession)
+	requireAdmin := adminMiddleware(db, requireLogin)
+	mux.Handle("GET /accounts", requireAdmin(handleAccountsGET(db)))
 	mux.Handle("GET /healthz", handleHealthz())
 	mux.Handle("GET /login", requireSession(handleLoginGET()))
 	mux.Handle("POST /login", requireSession(handleLoginPOST(db)))