Build Stable Keyset Cursors for List APIs

Encode keyset cursors as opaque base64 JSON and page records by creation time and ID without offset drift.

GoData Fetching

Code

snippet
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"sort"
	"time"
)

type Cursor struct {
	CreatedAt time.Time
	ID        int
}

type Item struct {
	ID        int
	CreatedAt time.Time
	Name      string
}

func encodeCursor(cursor Cursor) string {
	payload, err := json.Marshal(cursor)
	if err != nil {
		panic(err)
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

func decodeCursor(raw string) (Cursor, error) {
	var cursor Cursor
	payload, err := base64.RawURLEncoding.DecodeString(raw)
	if err != nil {
		return cursor, fmt.Errorf("decode cursor: %w", err)
	}
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return cursor, fmt.Errorf("parse cursor: %w", err)
	}
	if cursor.ID <= 0 || cursor.CreatedAt.IsZero() {
		return cursor, fmt.Errorf("cursor is missing fields")
	}
	return cursor, nil
}

func afterCursor(item Item, cursor Cursor) bool {
	if item.CreatedAt.After(cursor.CreatedAt) {
		return true
	}
	return item.CreatedAt.Equal(cursor.CreatedAt) && item.ID > cursor.ID
}

func pageAfter(items []Item, cursor *Cursor, limit int) []Item {
	if limit <= 0 {
		return nil
	}
	ordered := append([]Item(nil), items...)
	sort.Slice(ordered, func(left, right int) bool {
		if ordered[left].CreatedAt.Equal(ordered[right].CreatedAt) {
			return ordered[left].ID < ordered[right].ID
		}
		return ordered[left].CreatedAt.Before(ordered[right].CreatedAt)
	})
	page := make([]Item, 0, limit)
	for _, item := range ordered {
		if cursor != nil && !afterCursor(item, *cursor) {
			continue
		}
		page = append(page, item)
		if len(page) == limit {
			break
		}
	}
	return page
}

func main() {
	base := time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)
	items := []Item{
		{ID: 1, CreatedAt: base.Add(1 * time.Minute), Name: "alpha"},
		{ID: 2, CreatedAt: base.Add(2 * time.Minute), Name: "beta"},
		{ID: 3, CreatedAt: base.Add(3 * time.Minute), Name: "gamma"},
		{ID: 4, CreatedAt: base.Add(4 * time.Minute), Name: "delta"},
	}
	first := pageAfter(items, nil, 2)
	encoded := encodeCursor(Cursor{CreatedAt: first[1].CreatedAt, ID: first[1].ID})
	cursor, err := decodeCursor(encoded)
	if err != nil {
		panic(err)
	}
	second := pageAfter(items, &cursor, 2)
	fmt.Println(encoded)
	fmt.Println(first[0].ID, first[1].ID, second[0].ID, second[1].ID)
	if len(first) != 2 || len(second) != 2 || first[0].ID != 1 || second[0].ID != 3 {
		panic("unexpected page contents")
	}
	if _, err := decodeCursor("not-base64!"); err == nil {
		panic("invalid cursor should fail")
	}
}