diff --git a/lib/philomena_query/cursor.ex b/lib/philomena_query/cursor.ex
new file mode 100644
index 00000000..6f721a41
--- /dev/null
+++ b/lib/philomena_query/cursor.ex
@@ -0,0 +1,146 @@
+defmodule PhilomenaQuery.Cursor do
+  alias PhilomenaQuery.Search
+  alias Philomena.Repo
+  import Ecto.Query
+
+  @typedoc """
+  The underlying cursor type, which contains the ordered sort field values
+  of a document.
+  """
+  @type cursor :: [integer() | binary() | boolean()]
+
+  @typedoc """
+  A mapping of document IDs to cursors.
+  """
+  @type cursor_map :: %{integer() => cursor()}
+
+  @doc """
+  Execute search with optional input cursor, and return results as tuple of
+  `{results, cursors}`.
+
+  ## Example
+
+      iex> search_records(
+      ...>   %{query: ..., sort: [%{created_at: :desc}, %{id: :desc}]}
+      ...>   Image
+      ...> )
+      {%Scrivener.Page{entries: [%Image{id: 1}, ...]},
+       %{1 => [1325394000000, 1], ...}}
+
+  """
+  @spec search_records(Search.search_definition(), Search.queryable(), search_after :: term()) ::
+          {Scrivener.Page.t(), cursor_map()}
+  def search_records(search_definition, queryable, search_after) do
+    search_definition = search_after_definition(search_definition, search_after)
+    page = Search.search_records_with_hits(search_definition, queryable)
+
+    {records, cursors} =
+      Enum.map_reduce(page, %{}, fn {record, hit}, cursors ->
+        sort = Map.fetch!(hit, "sort")
+
+        {record, Map.put(cursors, record.id, sort)}
+      end)
+
+    {Map.put(page, :entries, records), cursors}
+  end
+
+  @doc """
+  Return page of records and cursors map based on sort.
+
+  ## Example
+
+      iex> paginate(Forum, [page_size: 25], ["dis", 3], asc: :name, asc: :id)
+      %{4 => ["Generals", 4]}
+
+  """
+  @spec paginate(
+          Ecto.Query.t(),
+          scrivener_opts :: any(),
+          search_after :: term(),
+          sorts :: Keyword.t()
+        ) :: {Scrivener.Page.t(), cursor_map()}
+  def paginate(query, pagination, search_after, sorts) do
+    total_entries = Repo.aggregate(query, :count)
+    pagination = Keyword.merge(pagination, options: [total_entries: total_entries])
+
+    records =
+      query
+      |> order_by(^sorts)
+      |> search_after_query(search_after, sorts)
+      |> Repo.paginate(pagination)
+
+    fields = Keyword.values(sorts)
+
+    cursors =
+      Enum.reduce(records, %{}, fn record, cursors ->
+        field_values = Enum.map(fields, &Map.fetch!(record, &1))
+        Map.put(cursors, record.id, field_values)
+      end)
+
+    {records, cursors}
+  end
+
+  @spec search_after_definition(Search.search_definition(), term()) :: Search.search_definition()
+  defp search_after_definition(search_definition, search_after) do
+    search_after
+    |> permit_search_after()
+    |> case do
+      [] ->
+        search_definition
+
+      search_after ->
+        update_in(search_definition.body, &Map.put(&1, :search_after, search_after))
+    end
+  end
+
+  @spec search_after_query(Ecto.Query.t(), term(), Keyword.t()) :: Ecto.Query.t()
+  defp search_after_query(query, search_after, sorts) do
+    search_after = permit_search_after(search_after)
+    combined = Enum.zip(sorts, search_after)
+
+    case combined do
+      [_some | _rest] = values ->
+        or_clauses = dynamic([], false)
+
+        {or_clauses, _} =
+          Enum.reduce(values, {or_clauses, []}, fn {{sd, col}, value}, {next, equal_parts} ->
+            # more specific column has next value
+            and_clauses =
+              if sd == :asc do
+                dynamic([s], field(s, ^col) > ^value)
+              else
+                dynamic([s], field(s, ^col) < ^value)
+              end
+
+            # and
+            and_clauses =
+              Enum.reduce(equal_parts, and_clauses, fn {col, value}, rest ->
+                # less specific columns are equal
+                dynamic([s], field(s, ^col) == ^value and ^rest)
+              end)
+
+            {dynamic(^next or ^and_clauses), equal_parts ++ [{col, value}]}
+          end)
+
+        where(query, ^or_clauses)
+
+      _ ->
+        query
+    end
+  end
+
+  # Validate that search_after values are only strings, numbers, and bools
+  defp permit_search_after(search_after) do
+    search_after
+    |> permit_list()
+    |> Enum.flat_map(&permit_value/1)
+  end
+
+  defp permit_list(value) when is_list(value), do: value
+  defp permit_list(_value), do: []
+
+  defp permit_value(value) when is_binary(value) or is_number(value) or is_boolean(value),
+    do: [value]
+
+  defp permit_value(_value), do: []
+end
diff --git a/lib/philomena_web/controllers/api/json/filter/system_filter_controller.ex b/lib/philomena_web/controllers/api/json/filter/system_filter_controller.ex
index 6fbbfb30..0eed1992 100755
--- a/lib/philomena_web/controllers/api/json/filter/system_filter_controller.ex
+++ b/lib/philomena_web/controllers/api/json/filter/system_filter_controller.ex
@@ -2,18 +2,21 @@ defmodule PhilomenaWeb.Api.Json.Filter.SystemFilterController do
   use PhilomenaWeb, :controller
 
   alias Philomena.Filters.Filter
-  alias Philomena.Repo
+  alias PhilomenaQuery.Cursor
   import Ecto.Query
 
-  def index(conn, _params) do
-    system_filters =
+  def index(conn, params) do
+    {system_filters, cursors} =
       Filter
       |> where(system: true)
-      |> order_by(asc: :id)
-      |> Repo.paginate(conn.assigns.scrivener)
+      |> Cursor.paginate(conn.assigns.scrivener, params["search_after"], asc: :id)
 
     conn
     |> put_view(PhilomenaWeb.Api.Json.FilterView)
-    |> render("index.json", filters: system_filters, total: system_filters.total_entries)
+    |> render("index.json",
+      cursors: cursors,
+      filters: system_filters,
+      total: system_filters.total_entries
+    )
   end
 end
diff --git a/lib/philomena_web/controllers/api/json/filter/user_filter_controller.ex b/lib/philomena_web/controllers/api/json/filter/user_filter_controller.ex
index 5d7eee7b..77b0d600 100755
--- a/lib/philomena_web/controllers/api/json/filter/user_filter_controller.ex
+++ b/lib/philomena_web/controllers/api/json/filter/user_filter_controller.ex
@@ -2,10 +2,10 @@ defmodule PhilomenaWeb.Api.Json.Filter.UserFilterController do
   use PhilomenaWeb, :controller
 
   alias Philomena.Filters.Filter
-  alias Philomena.Repo
+  alias PhilomenaQuery.Cursor
   import Ecto.Query
 
-  def index(conn, _params) do
+  def index(conn, params) do
     user = conn.assigns.current_user
 
     case user do
@@ -15,15 +15,18 @@ defmodule PhilomenaWeb.Api.Json.Filter.UserFilterController do
         |> text("")
 
       _ ->
-        user_filters =
+        {user_filters, cursors} =
           Filter
           |> where(user_id: ^user.id)
-          |> order_by(asc: :id)
-          |> Repo.paginate(conn.assigns.scrivener)
+          |> Cursor.paginate(conn.assigns.scrivener, params["search_after"], asc: :id)
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.FilterView)
-        |> render("index.json", filters: user_filters, total: user_filters.total_entries)
+        |> render("index.json",
+          cursors: cursors,
+          filters: user_filters,
+          total: user_filters.total_entries
+        )
     end
   end
 end
diff --git a/lib/philomena_web/controllers/api/json/forum/topic/post_controller.ex b/lib/philomena_web/controllers/api/json/forum/topic/post_controller.ex
index e33f08d8..cbae2f31 100644
--- a/lib/philomena_web/controllers/api/json/forum/topic/post_controller.ex
+++ b/lib/philomena_web/controllers/api/json/forum/topic/post_controller.ex
@@ -1,39 +1,46 @@
 defmodule PhilomenaWeb.Api.Json.Forum.Topic.PostController do
   use PhilomenaWeb, :controller
 
+  alias Philomena.Topics.Topic
   alias Philomena.Posts.Post
+  alias PhilomenaQuery.Cursor
   alias Philomena.Repo
   import Ecto.Query
 
+  def index(conn, %{
+        "forum_id" => forum_id,
+        "topic_id" => topic_id,
+        "search_after" => search_after
+      }) do
+    topic = Repo.one!(topic_query(topic_id, forum_id))
+
+    {posts, cursors} =
+      post_query(topic_id, forum_id)
+      |> Cursor.paginate(conn.assigns.scrivener, search_after, asc: :topic_position)
+
+    render(conn, "index.json", cursors: cursors, posts: posts, total: topic.post_count)
+  end
+
   def index(conn, %{"forum_id" => forum_id, "topic_id" => topic_id}) do
     page = conn.assigns.pagination.page_number
 
-    posts =
-      Post
-      |> join(:inner, [p], _ in assoc(p, :topic))
-      |> join(:inner, [_p, t], _ in assoc(t, :forum))
-      |> where(destroyed_content: false)
-      |> where([_p, t], t.hidden_from_users == false and t.slug == ^topic_id)
-      |> where([_p, _t, f], f.access_level == "normal" and f.short_name == ^forum_id)
-      |> where([p], p.topic_position >= ^(25 * (page - 1)) and p.topic_position < ^(25 * page))
-      |> order_by(asc: :topic_position)
-      |> preload([:user, :topic])
-      |> preload([_p, t, _f], topic: t)
-      |> Repo.all()
+    topic = Repo.one!(topic_query(topic_id, forum_id))
 
-    render(conn, "index.json", posts: posts, total: hd(posts).topic.post_count)
+    {posts, cursors} =
+      post_query(topic_id, forum_id)
+      |> where(
+        [posts: p],
+        p.topic_position >= ^(25 * (page - 1)) and p.topic_position < ^(25 * page)
+      )
+      |> Cursor.paginate([page_size: 25], [], asc: :topic_position)
+
+    render(conn, "index.json", cursors: cursors, posts: posts, total: topic.post_count)
   end
 
   def show(conn, %{"forum_id" => forum_id, "topic_id" => topic_id, "id" => post_id}) do
     post =
-      Post
-      |> join(:inner, [p], _ in assoc(p, :topic))
-      |> join(:inner, [_p, t], _ in assoc(t, :forum))
+      post_query(forum_id, topic_id)
       |> where(id: ^post_id)
-      |> where(destroyed_content: false)
-      |> where([_p, t], t.hidden_from_users == false and t.slug == ^topic_id)
-      |> where([_p, _t, f], f.access_level == "normal" and f.short_name == ^forum_id)
-      |> preload([:user, :topic])
       |> Repo.one()
 
     cond do
@@ -46,4 +53,27 @@ defmodule PhilomenaWeb.Api.Json.Forum.Topic.PostController do
         render(conn, "show.json", post: post)
     end
   end
+
+  defp topic_query(topic_id, forum_id) do
+    Topic
+    |> from(as: :topic)
+    |> join(:inner, [topic: t], _ in assoc(t, :forum), as: :forum)
+    |> topic_conditions(topic_id, forum_id)
+  end
+
+  defp post_query(topic_id, forum_id) do
+    Post
+    |> from(as: :posts)
+    |> join(:inner, [posts: p], _ in assoc(p, :topic), as: :topic)
+    |> join(:inner, [topic: t], _ in assoc(t, :forum), as: :forum)
+    |> topic_conditions(topic_id, forum_id)
+    |> where([posts: p], p.destroyed_content == false)
+    |> preload([:user])
+  end
+
+  defp topic_conditions(queryable, topic_id, forum_id) do
+    queryable
+    |> where([topic: t], t.hidden_from_users == false and t.slug == ^topic_id)
+    |> where([forum: f], f.access_level == "normal" and f.short_name == ^forum_id)
+  end
 end
diff --git a/lib/philomena_web/controllers/api/json/forum/topic_controller.ex b/lib/philomena_web/controllers/api/json/forum/topic_controller.ex
index 56730f65..fb7fde49 100644
--- a/lib/philomena_web/controllers/api/json/forum/topic_controller.ex
+++ b/lib/philomena_web/controllers/api/json/forum/topic_controller.ex
@@ -2,20 +2,24 @@ defmodule PhilomenaWeb.Api.Json.Forum.TopicController do
   use PhilomenaWeb, :controller
 
   alias Philomena.Topics.Topic
+  alias PhilomenaQuery.Cursor
   alias Philomena.Repo
   import Ecto.Query
 
-  def index(conn, %{"forum_id" => id}) do
-    topics =
+  def index(conn, %{"forum_id" => id} = params) do
+    {topics, cursors} =
       Topic
       |> join(:inner, [t], _ in assoc(t, :forum))
       |> where(hidden_from_users: false)
       |> where([_t, f], f.access_level == "normal" and f.short_name == ^id)
-      |> order_by(desc: :sticky, desc: :last_replied_to_at)
       |> preload([:user])
-      |> Repo.paginate(conn.assigns.scrivener)
+      |> Cursor.paginate(conn.assigns.scrivener, params["search_after"],
+        desc: :sticky,
+        desc: :last_replied_to_at,
+        desc: :slug
+      )
 
-    render(conn, "index.json", topics: topics, total: topics.total_entries)
+    render(conn, "index.json", cursors: cursors, topics: topics, total: topics.total_entries)
   end
 
   def show(conn, %{"forum_id" => forum_id, "id" => id}) do
@@ -25,7 +29,6 @@ defmodule PhilomenaWeb.Api.Json.Forum.TopicController do
       |> where(slug: ^id)
       |> where(hidden_from_users: false)
       |> where([_t, f], f.access_level == "normal" and f.short_name == ^forum_id)
-      |> order_by(desc: :sticky, desc: :last_replied_to_at)
       |> preload([:user])
       |> Repo.one()
 
diff --git a/lib/philomena_web/controllers/api/json/forum_controller.ex b/lib/philomena_web/controllers/api/json/forum_controller.ex
index 4ef39af5..e6cf7543 100644
--- a/lib/philomena_web/controllers/api/json/forum_controller.ex
+++ b/lib/philomena_web/controllers/api/json/forum_controller.ex
@@ -1,18 +1,21 @@
 defmodule PhilomenaWeb.Api.Json.ForumController do
   use PhilomenaWeb, :controller
 
+  alias PhilomenaQuery.Cursor
   alias Philomena.Forums.Forum
   alias Philomena.Repo
   import Ecto.Query
 
-  def index(conn, _params) do
-    forums =
+  def index(conn, params) do
+    {forums, cursors} =
       Forum
       |> where(access_level: "normal")
-      |> order_by(asc: :name)
-      |> Repo.paginate(conn.assigns.scrivener)
+      |> Cursor.paginate(conn.assigns.scrivener, params["search_after"],
+        asc: :name,
+        asc: :short_name
+      )
 
-    render(conn, forums: forums, total: forums.total_entries)
+    render(conn, cursors: cursors, forums: forums, total: forums.total_entries)
   end
 
   def show(conn, %{"id" => id}) do
diff --git a/lib/philomena_web/controllers/api/json/search/comment_controller.ex b/lib/philomena_web/controllers/api/json/search/comment_controller.ex
index 6942a4ff..b974fc48 100644
--- a/lib/philomena_web/controllers/api/json/search/comment_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/comment_controller.ex
@@ -1,6 +1,7 @@
 defmodule PhilomenaWeb.Api.Json.Search.CommentController do
   use PhilomenaWeb, :controller
 
+  alias PhilomenaQuery.Cursor
   alias PhilomenaQuery.Search
   alias Philomena.Comments.Comment
   alias Philomena.Comments.Query
@@ -12,7 +13,7 @@ defmodule PhilomenaWeb.Api.Json.Search.CommentController do
 
     case Query.compile(params["q"], user: user) do
       {:ok, query} ->
-        comments =
+        {comments, cursors} =
           Comment
           |> Search.search_definition(
             %{
@@ -27,15 +28,19 @@ defmodule PhilomenaWeb.Api.Json.Search.CommentController do
                   }
                 }
               },
-              sort: %{posted_at: :desc}
+              sort: [%{posted_at: :desc}, %{id: :desc}]
             },
             conn.assigns.pagination
           )
-          |> Search.search_records(preload(Comment, [:image, :user]))
+          |> Cursor.search_records(preload(Comment, [:image, :user]), params["search_after"])
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.CommentView)
-        |> render("index.json", comments: comments, total: comments.total_entries)
+        |> render("index.json",
+          comments: comments,
+          cursors: cursors,
+          total: comments.total_entries
+        )
 
       {:error, msg} ->
         conn
diff --git a/lib/philomena_web/controllers/api/json/search/filter_controller.ex b/lib/philomena_web/controllers/api/json/search/filter_controller.ex
index 7c4f81b5..f6757f62 100644
--- a/lib/philomena_web/controllers/api/json/search/filter_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/filter_controller.ex
@@ -1,6 +1,7 @@
 defmodule PhilomenaWeb.Api.Json.Search.FilterController do
   use PhilomenaWeb, :controller
 
+  alias PhilomenaQuery.Cursor
   alias PhilomenaQuery.Search
   alias Philomena.Filters.Filter
   alias Philomena.Filters.Query
@@ -11,7 +12,7 @@ defmodule PhilomenaWeb.Api.Json.Search.FilterController do
 
     case Query.compile(params["q"], user: user) do
       {:ok, query} ->
-        filters =
+        {filters, cursors} =
           Filter
           |> Search.search_definition(
             %{
@@ -36,11 +37,11 @@ defmodule PhilomenaWeb.Api.Json.Search.FilterController do
             },
             conn.assigns.pagination
           )
-          |> Search.search_records(preload(Filter, [:user]))
+          |> Cursor.search_records(preload(Filter, [:user]), params["search_after"])
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.FilterView)
-        |> render("index.json", filters: filters, total: filters.total_entries)
+        |> render("index.json", cursors: cursors, filters: filters, total: filters.total_entries)
 
       {:error, msg} ->
         conn
diff --git a/lib/philomena_web/controllers/api/json/search/gallery_controller.ex b/lib/philomena_web/controllers/api/json/search/gallery_controller.ex
index e1b999bb..b63691a9 100644
--- a/lib/philomena_web/controllers/api/json/search/gallery_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/gallery_controller.ex
@@ -1,6 +1,7 @@
 defmodule PhilomenaWeb.Api.Json.Search.GalleryController do
   use PhilomenaWeb, :controller
 
+  alias PhilomenaQuery.Cursor
   alias PhilomenaQuery.Search
   alias Philomena.Galleries.Gallery
   alias Philomena.Galleries.Query
@@ -9,20 +10,24 @@ defmodule PhilomenaWeb.Api.Json.Search.GalleryController do
   def index(conn, params) do
     case Query.compile(params["q"]) do
       {:ok, query} ->
-        galleries =
+        {galleries, cursors} =
           Gallery
           |> Search.search_definition(
             %{
               query: query,
-              sort: %{created_at: :desc}
+              sort: [%{created_at: :desc}, %{id: :desc}]
             },
             conn.assigns.pagination
           )
-          |> Search.search_records(preload(Gallery, [:creator]))
+          |> Cursor.search_records(preload(Gallery, [:creator]), params["search_after"])
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.GalleryView)
-        |> render("index.json", galleries: galleries, total: galleries.total_entries)
+        |> render("index.json",
+          cursors: cursors,
+          galleries: galleries,
+          total: galleries.total_entries
+        )
 
       {:error, msg} ->
         conn
diff --git a/lib/philomena_web/controllers/api/json/search/image_controller.ex b/lib/philomena_web/controllers/api/json/search/image_controller.ex
index 109e7abe..a32ea68d 100644
--- a/lib/philomena_web/controllers/api/json/search/image_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/image_controller.ex
@@ -2,7 +2,7 @@ defmodule PhilomenaWeb.Api.Json.Search.ImageController do
   use PhilomenaWeb, :controller
 
   alias PhilomenaWeb.ImageLoader
-  alias PhilomenaQuery.Search
+  alias PhilomenaQuery.Cursor
   alias Philomena.Interactions
   alias Philomena.Images.Image
   import Ecto.Query
@@ -13,13 +13,14 @@ defmodule PhilomenaWeb.Api.Json.Search.ImageController do
 
     case ImageLoader.search_string(conn, params["q"]) do
       {:ok, {images, _tags}} ->
-        images = Search.search_records(images, queryable)
+        {images, cursors} = Cursor.search_records(images, queryable, params["search_after"])
         interactions = Interactions.user_interactions(images, user)
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.ImageView)
         |> render("index.json",
           images: images,
+          cursors: cursors,
           total: images.total_entries,
           interactions: interactions
         )
diff --git a/lib/philomena_web/controllers/api/json/search/post_controller.ex b/lib/philomena_web/controllers/api/json/search/post_controller.ex
index c305de12..6fbf4f1e 100644
--- a/lib/philomena_web/controllers/api/json/search/post_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/post_controller.ex
@@ -1,6 +1,7 @@
 defmodule PhilomenaWeb.Api.Json.Search.PostController do
   use PhilomenaWeb, :controller
 
+  alias PhilomenaQuery.Cursor
   alias PhilomenaQuery.Search
   alias Philomena.Posts.Post
   alias Philomena.Posts.Query
@@ -11,7 +12,7 @@ defmodule PhilomenaWeb.Api.Json.Search.PostController do
 
     case Query.compile(params["q"], user: user) do
       {:ok, query} ->
-        posts =
+        {posts, cursors} =
           Post
           |> Search.search_definition(
             %{
@@ -24,15 +25,15 @@ defmodule PhilomenaWeb.Api.Json.Search.PostController do
                   ]
                 }
               },
-              sort: %{created_at: :desc}
+              sort: [%{created_at: :desc}, %{id: :desc}]
             },
             conn.assigns.pagination
           )
-          |> Search.search_records(preload(Post, [:user, :topic]))
+          |> Cursor.search_records(preload(Post, [:user, :topic]), params["search_after"])
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.Forum.Topic.PostView)
-        |> render("index.json", posts: posts, total: posts.total_entries)
+        |> render("index.json", cursors: cursors, posts: posts, total: posts.total_entries)
 
       {:error, msg} ->
         conn
diff --git a/lib/philomena_web/controllers/api/json/search/reverse_controller.ex b/lib/philomena_web/controllers/api/json/search/reverse_controller.ex
index 4abe7560..10c4f207 100644
--- a/lib/philomena_web/controllers/api/json/search/reverse_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/reverse_controller.ex
@@ -29,6 +29,7 @@ defmodule PhilomenaWeb.Api.Json.Search.ReverseController do
     |> put_view(PhilomenaWeb.Api.Json.ImageView)
     |> render("index.json",
       images: images,
+      cursors: %{},
       total: total,
       interactions: interactions
     )
diff --git a/lib/philomena_web/controllers/api/json/search/tag_controller.ex b/lib/philomena_web/controllers/api/json/search/tag_controller.ex
index b860cf46..92577849 100644
--- a/lib/philomena_web/controllers/api/json/search/tag_controller.ex
+++ b/lib/philomena_web/controllers/api/json/search/tag_controller.ex
@@ -1,6 +1,7 @@
 defmodule PhilomenaWeb.Api.Json.Search.TagController do
   use PhilomenaWeb, :controller
 
+  alias PhilomenaQuery.Cursor
   alias PhilomenaQuery.Search
   alias Philomena.Tags.Tag
   alias Philomena.Tags.Query
@@ -9,19 +10,20 @@ defmodule PhilomenaWeb.Api.Json.Search.TagController do
   def index(conn, params) do
     case Query.compile(params["q"]) do
       {:ok, query} ->
-        tags =
+        {tags, cursors} =
           Tag
           |> Search.search_definition(
-            %{query: query, sort: %{images: :desc}},
+            %{query: query, sort: [%{images: :desc}, %{id: :desc}]},
             conn.assigns.pagination
           )
-          |> Search.search_records(
-            preload(Tag, [:aliased_tag, :aliases, :implied_tags, :implied_by_tags, :dnp_entries])
+          |> Cursor.search_records(
+            preload(Tag, [:aliased_tag, :aliases, :implied_tags, :implied_by_tags, :dnp_entries]),
+            params["search_after"]
           )
 
         conn
         |> put_view(PhilomenaWeb.Api.Json.TagView)
-        |> render("index.json", tags: tags, total: tags.total_entries)
+        |> render("index.json", cursors: cursors, tags: tags, total: tags.total_entries)
 
       {:error, msg} ->
         conn
diff --git a/lib/philomena_web/image_sorter.ex b/lib/philomena_web/image_sorter.ex
index ed422288..564d3781 100644
--- a/lib/philomena_web/image_sorter.ex
+++ b/lib/philomena_web/image_sorter.ex
@@ -80,7 +80,7 @@ defmodule PhilomenaWeb.ImageSorter do
   end
 
   defp parse_sf(_params, sd, query) do
-    %{query: query, sorts: [%{"first_seen_at" => sd}]}
+    %{query: query, sorts: [%{"first_seen_at" => sd}, %{"id" => sd}]}
   end
 
   defp random_query(seed, sd, query) do
diff --git a/lib/philomena_web/views/api/json/comment_view.ex b/lib/philomena_web/views/api/json/comment_view.ex
index 5d80b42b..2eb065f2 100644
--- a/lib/philomena_web/views/api/json/comment_view.ex
+++ b/lib/philomena_web/views/api/json/comment_view.ex
@@ -2,8 +2,9 @@ defmodule PhilomenaWeb.Api.Json.CommentView do
   use PhilomenaWeb, :view
   alias PhilomenaWeb.UserAttributionView
 
-  def render("index.json", %{comments: comments, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, comments: comments, total: total} = assigns) do
     %{
+      cursors: cursors,
       comments: render_many(comments, PhilomenaWeb.Api.Json.CommentView, "comment.json", assigns),
       total: total
     }
diff --git a/lib/philomena_web/views/api/json/dnp_view.ex b/lib/philomena_web/views/api/json/dnp_view.ex
index ad07db2f..9395bf06 100755
--- a/lib/philomena_web/views/api/json/dnp_view.ex
+++ b/lib/philomena_web/views/api/json/dnp_view.ex
@@ -1,17 +1,6 @@
 defmodule PhilomenaWeb.Api.Json.DnpView do
   use PhilomenaWeb, :view
 
-  def render("index.json", %{dnps: dnp, total: total} = assigns) do
-    %{
-      dnps: render_many(dnp, PhilomenaWeb.Api.Json.DnpView, "dnp.json", assigns),
-      total: total
-    }
-  end
-
-  def render("show.json", %{dnp: dnp} = assigns) do
-    %{dnp: render_one(dnp, PhilomenaWeb.Api.Json.DnpView, "dnp.json", assigns)}
-  end
-
   def render("dnp.json", %{dnp: dnp}) do
     %{
       id: dnp.id,
diff --git a/lib/philomena_web/views/api/json/filter_view.ex b/lib/philomena_web/views/api/json/filter_view.ex
index bf9294d6..a83e85fa 100644
--- a/lib/philomena_web/views/api/json/filter_view.ex
+++ b/lib/philomena_web/views/api/json/filter_view.ex
@@ -1,8 +1,9 @@
 defmodule PhilomenaWeb.Api.Json.FilterView do
   use PhilomenaWeb, :view
 
-  def render("index.json", %{filters: filters, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, filters: filters, total: total} = assigns) do
     %{
+      cursors: cursors,
       filters: render_many(filters, PhilomenaWeb.Api.Json.FilterView, "filter.json", assigns),
       total: total
     }
diff --git a/lib/philomena_web/views/api/json/forum/topic/post_view.ex b/lib/philomena_web/views/api/json/forum/topic/post_view.ex
index 4d6ecdcb..9db3665a 100644
--- a/lib/philomena_web/views/api/json/forum/topic/post_view.ex
+++ b/lib/philomena_web/views/api/json/forum/topic/post_view.ex
@@ -2,8 +2,9 @@ defmodule PhilomenaWeb.Api.Json.Forum.Topic.PostView do
   use PhilomenaWeb, :view
   alias PhilomenaWeb.UserAttributionView
 
-  def render("index.json", %{posts: posts, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, posts: posts, total: total} = assigns) do
     %{
+      cursors: cursors,
       posts: render_many(posts, PhilomenaWeb.Api.Json.Forum.Topic.PostView, "post.json", assigns),
       total: total
     }
diff --git a/lib/philomena_web/views/api/json/forum/topic_view.ex b/lib/philomena_web/views/api/json/forum/topic_view.ex
index 564eb5f2..e1428fc4 100644
--- a/lib/philomena_web/views/api/json/forum/topic_view.ex
+++ b/lib/philomena_web/views/api/json/forum/topic_view.ex
@@ -2,8 +2,9 @@ defmodule PhilomenaWeb.Api.Json.Forum.TopicView do
   use PhilomenaWeb, :view
   alias PhilomenaWeb.UserAttributionView
 
-  def render("index.json", %{topics: topics, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, topics: topics, total: total} = assigns) do
     %{
+      cursors: cursors,
       topics: render_many(topics, PhilomenaWeb.Api.Json.Forum.TopicView, "topic.json", assigns),
       total: total
     }
@@ -15,6 +16,7 @@ defmodule PhilomenaWeb.Api.Json.Forum.TopicView do
 
   def render("topic.json", %{topic: %{hidden_from_users: true}}) do
     %{
+      id: nil,
       slug: nil,
       title: nil,
       post_count: nil,
@@ -29,6 +31,7 @@ defmodule PhilomenaWeb.Api.Json.Forum.TopicView do
 
   def render("topic.json", %{topic: topic}) do
     %{
+      id: topic.id,
       slug: topic.slug,
       title: topic.title,
       post_count: topic.post_count,
diff --git a/lib/philomena_web/views/api/json/forum_view.ex b/lib/philomena_web/views/api/json/forum_view.ex
index f6995d1b..5ede57f2 100644
--- a/lib/philomena_web/views/api/json/forum_view.ex
+++ b/lib/philomena_web/views/api/json/forum_view.ex
@@ -1,8 +1,9 @@
 defmodule PhilomenaWeb.Api.Json.ForumView do
   use PhilomenaWeb, :view
 
-  def render("index.json", %{forums: forums, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, forums: forums, total: total} = assigns) do
     %{
+      cursors: cursors,
       forums: render_many(forums, PhilomenaWeb.Api.Json.ForumView, "forum.json", assigns),
       total: total
     }
@@ -14,6 +15,7 @@ defmodule PhilomenaWeb.Api.Json.ForumView do
 
   def render("forum.json", %{forum: %{access_level: "normal"} = forum}) do
     %{
+      id: forum.id,
       name: forum.name,
       short_name: forum.short_name,
       description: forum.description,
diff --git a/lib/philomena_web/views/api/json/gallery_view.ex b/lib/philomena_web/views/api/json/gallery_view.ex
index d49c1236..7ea4b3d0 100644
--- a/lib/philomena_web/views/api/json/gallery_view.ex
+++ b/lib/philomena_web/views/api/json/gallery_view.ex
@@ -1,8 +1,9 @@
 defmodule PhilomenaWeb.Api.Json.GalleryView do
   use PhilomenaWeb, :view
 
-  def render("index.json", %{galleries: galleries, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, galleries: galleries, total: total} = assigns) do
     %{
+      cursors: cursors,
       galleries:
         render_many(galleries, PhilomenaWeb.Api.Json.GalleryView, "gallery.json", assigns),
       total: total
diff --git a/lib/philomena_web/views/api/json/image_view.ex b/lib/philomena_web/views/api/json/image_view.ex
index f72a676e..79312ee4 100644
--- a/lib/philomena_web/views/api/json/image_view.ex
+++ b/lib/philomena_web/views/api/json/image_view.ex
@@ -2,8 +2,12 @@ defmodule PhilomenaWeb.Api.Json.ImageView do
   use PhilomenaWeb, :view
   alias PhilomenaWeb.ImageView
 
-  def render("index.json", %{images: images, interactions: interactions, total: total} = assigns) do
+  def render(
+        "index.json",
+        %{cursors: cursors, images: images, interactions: interactions, total: total} = assigns
+      ) do
     %{
+      cursors: cursors,
       images: render_many(images, PhilomenaWeb.Api.Json.ImageView, "image.json", assigns),
       interactions: interactions,
       total: total
diff --git a/lib/philomena_web/views/api/json/tag_view.ex b/lib/philomena_web/views/api/json/tag_view.ex
index 8aaa6248..626365f1 100644
--- a/lib/philomena_web/views/api/json/tag_view.ex
+++ b/lib/philomena_web/views/api/json/tag_view.ex
@@ -1,8 +1,9 @@
 defmodule PhilomenaWeb.Api.Json.TagView do
   use PhilomenaWeb, :view
 
-  def render("index.json", %{tags: tags, total: total} = assigns) do
+  def render("index.json", %{cursors: cursors, tags: tags, total: total} = assigns) do
     %{
+      cursors: cursors,
       tags: render_many(tags, PhilomenaWeb.Api.Json.TagView, "tag.json", assigns),
       total: total
     }