Cursor pagination survives an insert; offset does not

Offset pagination on a list ordered by recency shows an item twice when a new row arrives between page one and page two, and skips one when a row is deleted.

// GET /orders?page=2&per_page=20      — shifts under you
// GET /orders?after=eyJpZCI6OTEyMDR9   — anchored to a row

$query->orderBy('placed_at', 'desc')->orderBy('id', 'desc');

if ($cursor) {
    $query->where(function ($q) use ($cursor) {
        $q->where('placed_at', '<', $cursor['placed_at'])
          ->orWhere(function ($q) use ($cursor) {
              $q->where('placed_at', '=', $cursor['placed_at'])
                ->where('id', '<', $cursor['id']);
          });
    });
}

The tiebreaker on the primary key is what makes it correct rather than nearly correct, because two rows sharing a timestamp are common and a cursor on a non-unique column skips or repeats at exactly that boundary. Opaque cursors stop a client reconstructing one and coupling itself to the sort order. The cost is that jumping to page seven becomes impossible, which matters for an admin table and not for an infinite scroll — so the choice is per endpoint.