diff --git a/NOTES.md b/NOTES.md
index 89823c4d5612ba6ae688ddf05c59664541e7a700..46182bb7a426f075ca98b93fb46eb6efe662dfff 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -30,14 +30,10 @@ repl.commands?
   * Run [godoc](http://localhost:6060/) and then review where the help text is lacking.
   * ~~Move to a storage layer.~~
   * this.Folder should be a storage.Folder.
-  * Implement batch jobs
-     * ~~Have install populate the database with some test data.~~
-     * ~~reboot~~
-     * ~~expire~~
   * Implement each command.
     * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~
-    * Messages: ~~ADD~~, CURRENT, ~~DIRECTORY~~, BACK, CHANGE,
-                FIRST, REMOVE, ~~NEXT~~, ~~READ~~
+    * Messages: ~~ADD~~, ~~CURRENT~~, ~~DIRECTORY~~, ~~BACK~~, CHANGE,
+                ~~FIRST~~, ~~NEXT~~, ~~READ~~, DELETE
     * Messages edit: CHANGE, REPLY, FORWARD
     * Moving messages: COPY, MOVE
     * Compound commands: SET and SHOW - make HELP work for them.
@@ -53,15 +49,12 @@ repl.commands?
     * trigger to limit values for 'visibility'?
   * Add some of the early announcements from the sources - see the
     conversion branch - to the GENERAL folder.
-  * Add a pager
   * Add commands:
     * A way to add / delete ssh keys.
     * A way to manage files?
     * Commands for a local mail system?
     * Commands to connect to Mattermost or mastodon?
     * Commands to manage users.
-  * ~~SHOW VERSION~~
-  * Check db version; notify user if it changes; refuse to write to db if it has.
 
 Done:
 
@@ -72,6 +65,13 @@ Done:
     * Using giu, a [text-editor](https://serge-hulne.medium.com/coding-a-simple-text-editor-in-go-using-giu-quick-and-dirty-b9b97ab41e4a) (needs cgo, no)
     * [bubbletea](https://github.com/charmbracelet/bubbletea) seems to be the tui that's winning
     * Another option is tview - [simpler](https://github.com/rivo/tview).
+  * ~~Implement batch jobs~~
+     * ~~Have install populate the database with some test data.~~
+     * ~~reboot~~
+     * ~~expire~~
+  * ~~Add a pager~~
+  * ~~SHOW VERSION~~
+  * Check db version; notify user if it changes; refuse to write to db if it has.
 
 ## Module links
 
diff --git a/folders/messages.go b/folders/messages.go
index e59c2a9216734925f83697b47063f8f4f3e1bae9..6eeb8eb16203f99b8b86aeca779411122aeec7b8 100644
--- a/folders/messages.go
+++ b/folders/messages.go
@@ -60,6 +60,34 @@ func ReadMessage(login, folder string, msgid int64) (*storage.Message, error) {
 	return &msg, nil
 }
 
+// MarkSeen marks a list of messages as seen.
+func MarkSeen(msgids []int64) error {
+	ctx := storage.Context()
+
+	for _, msgid := range msgids {
+		this.Q.SetMessageSeen(ctx, storage.SetMessageSeenParams{
+			Login:  this.User.Login,
+			Folder: this.Folder,
+			Msgid:  msgid,
+		})
+	}
+	return nil
+}
+
+// MarkUnseen marks a list of messages as unseen.
+func MarkUnseen(msgids []int64) error {
+	ctx := storage.Context()
+
+	for _, msgid := range msgids {
+		this.Q.UnsetMessageSeen(ctx, storage.UnsetMessageSeenParams{
+			Login:  this.User.Login,
+			Folder: this.Folder,
+			Msgid:  msgid,
+		})
+	}
+	return nil
+}
+
 // NextMsgid gets the next message id.
 func NextMsgid(login, folder string, msgid int64) int64 {
 	ctx := storage.Context()
@@ -74,6 +102,34 @@ func NextMsgid(login, folder string, msgid int64) int64 {
 	return newid
 }
 
+// PrevMsgid gets the next message id.
+func PrevMsgid(login, folder string, msgid int64) int64 {
+	ctx := storage.Context()
+	newid, err := this.Q.PrevMsgid(ctx, storage.PrevMsgidParams{
+		Folder: folder,
+		Login:  login,
+		ID:     msgid,
+	})
+	if err != nil {
+		return 0
+	}
+	return newid
+}
+
+// FirstMessage gets the first message in a folder.
+func FirstMessage(folder string) int64 {
+	ctx := storage.Context()
+	first, err := this.Q.NextMsgidIgnoringSeen(ctx,
+		storage.NextMsgidIgnoringSeenParams{
+			Folder: folder,
+			ID:     0,
+		})
+	if err != nil {
+		return 0
+	}
+	return first
+}
+
 // ListMessages lists messages.
 func ListMessages(folder string) ([]storage.Message, error) {
 	ctx := storage.Context()
diff --git a/repl/args.go b/repl/args.go
new file mode 100644
index 0000000000000000000000000000000000000000..dd8845509ba00d43c1ccee4cdc150ab41452146d
--- /dev/null
+++ b/repl/args.go
@@ -0,0 +1,43 @@
+package repl
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+// ParseNumberList takes a string with a number list
+// like "1,8,3-6,10,20-30" and returns a number list like
+// []int64{1,8,3,4,5,6,10,20,21,...,30}.  Well, also an error since
+// somethings can go wrong here.
+func ParseNumberList(input string) ([]int64, error) {
+	var result []int64
+	segments := strings.Split(input, ",")
+
+	for _, segment := range segments {
+		segment = strings.TrimSpace(segment)
+		if strings.Contains(segment, "-") {
+			// Handle range
+			parts := strings.SplitN(segment, "-", 2)
+			if len(parts) != 2 {
+				return nil, fmt.Errorf("invalid range format: %s", segment)
+			}
+			start, err1 := strconv.ParseInt(parts[0], 10, 64)
+			end, err2 := strconv.ParseInt(parts[1], 10, 64)
+			if err1 != nil || err2 != nil || start > end {
+				return nil, fmt.Errorf("invalid range: %s", segment)
+			}
+			for i := start; i <= end; i++ {
+				result = append(result, i)
+			}
+		} else {
+			// Handle single number
+			num, err := strconv.ParseInt(segment, 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("invalid number: %s", segment)
+			}
+			result = append(result, num)
+		}
+	}
+	return result, nil
+}
diff --git a/repl/command.go b/repl/command.go
index 30534049749ebf1897937d3e667b39c9fce941d6..83167dfd28d992e161d53081db3df8027c01ce21 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -1139,16 +1139,16 @@ automatically  be set  as  being SEEN  when they  are  read.
 If you  have used  the SEEN  command and wish  to disable  the automatic
 marking of messages in regular folders  as SEEN when they are read, type
 the command SEEN/NOREAD. To reenable, simply use the SEEN command again.`,
-		MinArgs: 1,
 		MaxArgs: 1,
+		Action:  ActionSeen,
 	},
 	"UNSEEN": {
 		Description: `Sets the current or message-id message as unseen.
 
   Format:
     UNSEEN [message-number or numbers]`,
-		MinArgs: 1,
 		MaxArgs: 1,
+		Action:  ActionUnseen,
 	},
 	"SELECT": {
 		Description: `Selects a folder of messages.  See HELP Folders for a description  of  a
diff --git a/repl/messages.go b/repl/messages.go
index 4778a6d1fa1d8f9d7eed42d6148360d8ad587484..9e5f223be6e9f6a89b5c55ac865503129bb8deeb 100644
--- a/repl/messages.go
+++ b/repl/messages.go
@@ -148,14 +148,36 @@ func ActionAdd(cmd *dclish.Command) error {
 }
 
 // ActionCurrent handles the `CURRENT` command.
-func ActionCurrent(cmd *dclish.Command) error {
-	fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description)
+func ActionCurrent(_ *dclish.Command) error {
+	// TODO: handle flags.
+	msg, err := folders.ReadMessage(this.User.Login, this.Folder, this.MsgID)
+	if err != nil {
+		return err
+	}
+	lines := strings.Split(msg.String(), "\n")
+	if len(lines) > 10 {
+		lines = lines[:10]
+	}
+	fmt.Printf("%s\n", strings.Join(lines, "\n"))
 	return nil
 }
 
 // ActionBack handles the `BACK` command.
-func ActionBack(cmd *dclish.Command) error {
-	fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description)
+func ActionBack(_ *dclish.Command) error {
+	// TODO: handle flags.
+	msgid := folders.PrevMsgid(this.User.Login, this.Folder, this.MsgID)
+	if msgid == 0 {
+		fmt.Println("No previous messages")
+		return nil
+	}
+	msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid)
+	if err != nil {
+		return err
+	}
+	// TODO: pager needs to report if the whole message was read
+	// and only increment if not.
+	pager.Pager(msg.String())
+	this.MsgID = msgid
 	return nil
 }
 
@@ -166,15 +188,24 @@ func ActionChange(cmd *dclish.Command) error {
 }
 
 // ActionFirst handles the `FIRST` command.
-func ActionFirst(cmd *dclish.Command) error {
-	fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description)
+func ActionFirst(_ *dclish.Command) error {
+	msgid := folders.FirstMessage(this.Folder)
+	if msgid == 0 {
+		fmt.Println("No messages in folder")
+		return nil
+	}
+	this.MsgID = msgid
 	return nil
 }
 
 // ActionNext handles the `NEXT` command.
-func ActionNext(cmd *dclish.Command) error {
+func ActionNext(_ *dclish.Command) error {
 	// TODO: handle flags.
-	msgid := max(folders.NextMsgid(this.User.Login, this.Folder, this.MsgID), this.MsgID)
+	msgid := folders.NextMsgid(this.User.Login, this.Folder, this.MsgID)
+	if msgid == 0 {
+		fmt.Println("No next messages")
+		return nil
+	}
 	msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid)
 	if err != nil {
 		return err
@@ -182,8 +213,7 @@ func ActionNext(cmd *dclish.Command) error {
 	// TODO: pager needs to report if the whole message was read
 	// and only increment if not.
 	pager.Pager(msg.String())
-	msgid = folders.NextMsgid(this.User.Login, this.Folder, msgid)
-	this.MsgID = max(this.MsgID, msgid)
+	this.MsgID = msgid
 	return nil
 }
 
@@ -192,11 +222,11 @@ func ActionRead(cmd *dclish.Command) error {
 	// TODO: handle flags.
 	msgid := this.MsgID
 	if len(cmd.Args) == 1 {
-		id, err := strconv.Atoi(cmd.Args[0])
+		var err error
+		msgid, err = strconv.ParseInt(cmd.Args[0], 10, 64)
 		if err != nil {
 			return err
 		}
-		msgid = int64(id)
 	}
 	msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid)
 	if err != nil {
@@ -221,3 +251,37 @@ func ActionForward(cmd *dclish.Command) error {
 	fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description)
 	return nil
 }
+
+// ActionSeen handles the `SEEN` command.
+func ActionSeen(cmd *dclish.Command) error {
+	var err error
+	msgids := []int64{this.MsgID}
+	if len(cmd.Args) == 1 {
+		msgids, err = ParseNumberList(cmd.Args[0])
+		if err != nil {
+			return err
+		}
+	}
+	err = folders.MarkSeen(msgids)
+	if err != nil {
+		fmt.Printf("ERROR: %s.\n", err)
+	}
+	return nil
+}
+
+// ActionUnseen handles the `UNSEEN` command.
+func ActionUnseen(cmd *dclish.Command) error {
+	var err error
+	msgids := []int64{this.MsgID}
+	if len(cmd.Args) == 1 {
+		msgids, err = ParseNumberList(cmd.Args[0])
+		if err != nil {
+			return err
+		}
+	}
+	err = folders.MarkUnseen(msgids)
+	if err != nil {
+		fmt.Printf("ERROR: %s.\n", err)
+	}
+	return nil
+}
diff --git a/repl/show.go b/repl/show.go
index b3c4df28b28a612b3dbe963c484573ad45b1342b..6b23e8fce6ba017576caacaf9e9d80561f8d95c8 100644
--- a/repl/show.go
+++ b/repl/show.go
@@ -11,7 +11,7 @@ import (
 
 // ActionShowFlags handles the `SHOW FLAGS` command.
 func ActionShowFlags(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionShowFlags.")
+	// ctx := storage.Context()
 	return nil
 }
 
diff --git a/storage/connection.go b/storage/connection.go
index 616afd288fc680db3668b3bbe02b4b96adfa0893..d4c9eb5669f2a51cc5fdaffb246a32ed9c1fb0bb 100644
--- a/storage/connection.go
+++ b/storage/connection.go
@@ -51,6 +51,10 @@ func Open() (*sqlx.DB, error) {
 	if err != nil {
 		return nil, errors.New("bulletin database problem")
 	}
+
+	// Prepare to watch for schema skew - see version.go.
+	InitialiseSkewChecker(db, m)
+
 	return db, nil
 }
 
diff --git a/storage/display.go b/storage/display.go
index 75155344ae056ec99677f9ff5bf43fc312de4fc7..d01d7f28ed8a7ca698aa87fcdb133faf76e739f5 100644
--- a/storage/display.go
+++ b/storage/display.go
@@ -33,3 +33,30 @@ func (m *Message) OneLine(expire bool) string {
 	ts := t.Format("2006-05-04 15:02:01")
 	return fmt.Sprintf("%4d %-43s %-12s %-10s\n", m.ID, m.Subject, m.Author, ts)
 }
+
+// String displays a user (mainly used for debugging).
+func (u User) String() string {
+	return fmt.Sprintf("User %s (%s) [a%d, m%d, !%d, d%d] [%s]",
+		u.Login,
+		u.Name,
+		u.Admin,
+		u.Moderator,
+		u.Alert,
+		u.Disabled,
+		u.LastLogin.Format("06-05-04 15:02:01"))
+}
+
+// String displays a folder (mainly used for debugging).
+func (f Folder) String() string {
+	return fmt.Sprintf("Folder %s (%s) [a%d, b%d, n%d, rn%d, sn%d, sys%d, exp%d, v%d]",
+		f.Name,
+		f.Description,
+		f.Always,
+		f.Brief,
+		f.Notify,
+		f.Readnew,
+		f.Shownew,
+		f.System,
+		f.Expire,
+		f.Visibility)
+}
diff --git a/storage/messages.sql.go b/storage/messages.sql.go
index b922e096c630a132966c425741c4b56b250e9241..ac9ea85e3778ffb187304e15b0c4ec0c984730ac 100644
--- a/storage/messages.sql.go
+++ b/storage/messages.sql.go
@@ -41,17 +41,6 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) er
 	return err
 }
 
-const getFirstMessageID = `-- name: GetFirstMessageID :one
-SELECT id FROM messages WHERE folder = ? and id = MIN(id) GROUP BY folder
-`
-
-func (q *Queries) GetFirstMessageID(ctx context.Context, folder string) (int64, error) {
-	row := q.db.QueryRowContext(ctx, getFirstMessageID, folder)
-	var id int64
-	err := row.Scan(&id)
-	return id, err
-}
-
 const nextMsgid = `-- name: NextMsgid :one
 SELECT CAST(COALESCE(MIN(id), 0) AS INT) FROM messages AS m
   WHERE m.folder = ?1 AND m.id > ?2
@@ -71,6 +60,59 @@ func (q *Queries) NextMsgid(ctx context.Context, arg NextMsgidParams) (int64, er
 	return column_1, err
 }
 
+const nextMsgidIgnoringSeen = `-- name: NextMsgidIgnoringSeen :one
+SELECT CAST(MIN(id) AS INT) FROM messages AS m
+  WHERE m.folder = ?1 AND m.id > ?2
+`
+
+type NextMsgidIgnoringSeenParams struct {
+	Folder string
+	ID     int64
+}
+
+func (q *Queries) NextMsgidIgnoringSeen(ctx context.Context, arg NextMsgidIgnoringSeenParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, nextMsgidIgnoringSeen, arg.Folder, arg.ID)
+	var column_1 int64
+	err := row.Scan(&column_1)
+	return column_1, err
+}
+
+const prevMsgid = `-- name: PrevMsgid :one
+SELECT CAST(COALESCE(MAX(id), 0) AS INT) FROM messages AS m
+  WHERE m.folder = ?1 AND m.id < ?2
+  AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ?1 AND s.login = ?3)
+`
+
+type PrevMsgidParams struct {
+	Folder string
+	ID     int64
+	Login  string
+}
+
+func (q *Queries) PrevMsgid(ctx context.Context, arg PrevMsgidParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, prevMsgid, arg.Folder, arg.ID, arg.Login)
+	var column_1 int64
+	err := row.Scan(&column_1)
+	return column_1, err
+}
+
+const prevMsgidIgnoringSeen = `-- name: PrevMsgidIgnoringSeen :one
+SELECT CAST(MAX(id) AS INT) FROM messages AS m
+  WHERE m.folder = ?1 AND m.id < ?2
+`
+
+type PrevMsgidIgnoringSeenParams struct {
+	Folder string
+	ID     int64
+}
+
+func (q *Queries) PrevMsgidIgnoringSeen(ctx context.Context, arg PrevMsgidIgnoringSeenParams) (int64, error) {
+	row := q.db.QueryRowContext(ctx, prevMsgidIgnoringSeen, arg.Folder, arg.ID)
+	var column_1 int64
+	err := row.Scan(&column_1)
+	return column_1, err
+}
+
 const readMessage = `-- name: ReadMessage :one
 SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages WHERE folder = ? AND id = ?
 `
@@ -112,3 +154,18 @@ func (q *Queries) SetMessageSeen(ctx context.Context, arg SetMessageSeenParams)
 	_, err := q.db.ExecContext(ctx, setMessageSeen, arg.Login, arg.Folder, arg.Msgid)
 	return err
 }
+
+const unsetMessageSeen = `-- name: UnsetMessageSeen :exec
+DELETE FROM seen WHERE login = ? AND folder = ? AND msgid = ?
+`
+
+type UnsetMessageSeenParams struct {
+	Login  string
+	Folder string
+	Msgid  int64
+}
+
+func (q *Queries) UnsetMessageSeen(ctx context.Context, arg UnsetMessageSeenParams) error {
+	_, err := q.db.ExecContext(ctx, unsetMessageSeen, arg.Login, arg.Folder, arg.Msgid)
+	return err
+}
diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql
index 0866f466296f44b7b71e3b9453cc22396f4d8657..52eed05f97927196f0da8bba8fd09ca9bbf6c999 100644
--- a/storage/queries/messages.sql
+++ b/storage/queries/messages.sql
@@ -8,8 +8,8 @@ INSERT INTO messages (
 -- name: SetMessageSeen :exec
 INSERT INTO seen (login, folder, msgid) VALUES (?, ?, ?);
 
--- name: GetFirstMessageID :one
-SELECT id FROM messages WHERE folder = ? and id = MIN(id) GROUP BY folder;
+-- name: UnsetMessageSeen :exec
+DELETE FROM seen WHERE login = ? AND folder = ? AND msgid = ?;
 
 -- name: ReadMessage :one
 SELECT * FROM messages WHERE folder = ? AND id = ?;
@@ -18,3 +18,16 @@ SELECT * FROM messages WHERE folder = ? AND id = ?;
 SELECT CAST(COALESCE(MIN(id), 0) AS INT) FROM messages AS m
   WHERE m.folder = ?1 AND m.id > ?2
   AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ?1 AND s.login = ?3);
+
+-- name: NextMsgidIgnoringSeen :one
+SELECT CAST(MIN(id) AS INT) FROM messages AS m
+  WHERE m.folder = ?1 AND m.id > ?2;
+
+-- name: PrevMsgid :one
+SELECT CAST(COALESCE(MAX(id), 0) AS INT) FROM messages AS m
+  WHERE m.folder = ?1 AND m.id < ?2
+  AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ?1 AND s.login = ?3);
+
+-- name: PrevMsgidIgnoringSeen :one
+SELECT CAST(MAX(id) AS INT) FROM messages AS m
+  WHERE m.folder = ?1 AND m.id < ?2;
diff --git a/storage/version.go b/storage/version.go
new file mode 100644
index 0000000000000000000000000000000000000000..565065b93fc35ff8c03753a84c2499ea9f898e5e
--- /dev/null
+++ b/storage/version.go
@@ -0,0 +1,53 @@
+package storage
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/golang-migrate/migrate/v4"
+	"github.com/jmoiron/sqlx"
+)
+
+// Skew helps us track database skew.
+var Skew SkewChecker
+
+// SkewChecker helps us track database skew.
+type SkewChecker struct {
+	db     *sqlx.DB
+	migver dbVersion
+}
+
+// InitialiseSkewChecker creates an object to track skew.
+func InitialiseSkewChecker(db *sqlx.DB, m *migrate.Migrate) {
+	version, dirty, err := m.Version()
+	if err != nil {
+		fmt.Printf("ERROR: Schema issue: %s\n", err)
+		os.Exit(1)
+	}
+	Skew = SkewChecker{
+		db: db,
+		migver: dbVersion{
+			Version: version,
+			Dirty:   dirty,
+		},
+	}
+}
+
+type dbVersion struct {
+	Version uint `db:"version"`
+	Dirty   bool `db:"dirty"`
+}
+
+// Safe returns true if the DB hasn't skewed.
+func (skew *SkewChecker) Safe() bool {
+	row := skew.db.QueryRowx("SELECT * FROM schema_migrations")
+	v := &dbVersion{
+		Dirty: true,
+	}
+	row.StructScan(v)
+
+	if skew.migver.Version != v.Version {
+		return false
+	}
+	return !v.Dirty
+}
diff --git a/this/this.go b/this/this.go
index 9ea7e69b1c7776eb2bcb8248c6cfaceee3feb2e4..c644852865b2d057d045fe6fba902c298b39e291 100644
--- a/this/this.go
+++ b/this/this.go
@@ -74,7 +74,18 @@ func StartThis(login, name string) error {
 		return errors.New("User is disabled")
 	}
 	Folder = "GENERAL"
-	MsgID, err = Q.GetFirstMessageID(ctx, Folder)
+	MsgID, err = Q.NextMsgid(ctx, storage.NextMsgidParams{
+		Folder: Folder,
+		ID:     0,
+		Login:  User.Login,
+	})
+	if MsgID == 0 {
+		MsgID, err = Q.NextMsgidIgnoringSeen(ctx, storage.NextMsgidIgnoringSeenParams{
+			Folder: Folder,
+			ID:     0,
+		})
+	}
 
+	fmt.Printf("User: %s\nFolder: %s\nMsgID: %d\n", User, Folder, MsgID)
 	return nil
 }