diff --git a/pyproject.toml b/pyproject.toml
index e07c98b..9570746 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ classifiers = [
 requires-python = ">= 3.8"
 dependencies = [
         "ColanderAlchemy",
+        "paginate",
         "pyramid>=2",
         "pyramid_beaker",
         "pyramid_deform",
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index 740607c..5f5b34e 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -61,6 +61,11 @@ class Grid:
        Presumably unique key for the grid; used to track per-grid
        sort/filter settings etc.
 
+    .. attribute:: vue_tagname
+
+       String name for Vue component tag.  By default this is
+       ``'wutta-grid'``.  See also :meth:`render_vue_tag()`.
+
     .. attribute:: model_class
 
        Model class for the grid, if applicable.  When set, this is
@@ -106,15 +111,43 @@ class Grid:
 
        See also :meth:`set_link()` and :meth:`is_linked()`.
 
-    .. attribute:: vue_tagname
+    .. attribute:: paginated
 
-       String name for Vue component tag.  By default this is
-       ``'wutta-grid'``.  See also :meth:`render_vue_tag()`.
+       Boolean indicating whether the grid data should be paginated
+       vs. all data is shown at once.  Default is ``False``.
+
+       See also :attr:`pagesize` and :attr:`page`.
+
+    .. attribute:: pagesize_options
+
+       List of "page size" options for the grid.  See also
+       :attr:`pagesize`.
+
+       Only relevant if :attr:`paginated` is true.  If not specified,
+       constructor will call :meth:`get_pagesize_options()` to get the
+       value.
+
+    .. attribute:: pagesize
+
+       Number of records to show in a data page.  See also
+       :attr:`pagesize_options` and :attr:`page`.
+
+       Only relevant if :attr:`paginated` is true.  If not specified,
+       constructor will call :meth:`get_pagesize()` to get the value.
+
+    .. attribute:: page
+
+       The current page number (of data) to display in the grid.  See
+       also :attr:`pagesize`.
+
+       Only relevant if :attr:`paginated` is true.  If not specified,
+       constructor will assume ``1`` (first page).
     """
 
     def __init__(
             self,
             request,
+            vue_tagname='wutta-grid',
             model_class=None,
             key=None,
             columns=None,
@@ -123,9 +156,13 @@ class Grid:
             renderers={},
             actions=[],
             linked_columns=[],
-            vue_tagname='wutta-grid',
+            paginated=False,
+            pagesize_options=None,
+            pagesize=None,
+            page=1,
     ):
         self.request = request
+        self.vue_tagname = vue_tagname
         self.model_class = model_class
         self.key = key
         self.data = data
@@ -133,13 +170,17 @@ class Grid:
         self.renderers = renderers or {}
         self.actions = actions or []
         self.linked_columns = linked_columns or []
-        self.vue_tagname = vue_tagname
 
         self.config = self.request.wutta_config
         self.app = self.config.get_app()
 
         self.set_columns(columns or self.get_columns())
 
+        self.paginated = paginated
+        self.pagesize_options = pagesize_options or self.get_pagesize_options()
+        self.pagesize = pagesize or self.get_pagesize()
+        self.page = page
+
     def get_columns(self):
         """
         Returns the official list of column names for the grid, or
@@ -340,6 +381,64 @@ class Grid:
                 return True
         return False
 
+    ##############################
+    # paging methods
+    ##############################
+
+    def get_pagesize_options(self, default=None):
+        """
+        Returns a list of default page size options for the grid.
+
+        It will check config but if no setting exists, will fall
+        back to::
+
+           [5, 10, 20, 50, 100, 200]
+
+        :param default: Alternate default value to return if none is
+           configured.
+
+        This method is intended for use in the constructor.  Code can
+        instead access :attr:`pagesize_options` directly.
+        """
+        options = self.config.get_list('wuttaweb.grids.default_pagesize_options')
+        if options:
+            options = [int(size) for size in options
+                       if size.isdigit()]
+            if options:
+                return options
+
+        return default or [5, 10, 20, 50, 100, 200]
+
+    def get_pagesize(self, default=None):
+        """
+        Returns the default page size for the grid.
+
+        It will check config but if no setting exists, will fall back
+        to a value from :attr:`pagesize_options` (will return ``20`` if
+        that is listed; otherwise the "first" option).
+
+        :param default: Alternate default value to return if none is
+           configured.
+
+        This method is intended for use in the constructor.  Code can
+        instead access :attr:`pagesize` directly.
+        """
+        size = self.config.get_int('wuttaweb.grids.default_pagesize')
+        if size:
+            return size
+
+        if default:
+            return default
+
+        if 20 in self.pagesize_options:
+            return 20
+
+        return self.pagesize_options[0]
+
+    ##############################
+    # rendering methods
+    ##############################
+
     def render_vue_tag(self, **kwargs):
         """
         Render the Vue component tag for the grid.
diff --git a/src/wuttaweb/templates/grids/vue_template.mako b/src/wuttaweb/templates/grids/vue_template.mako
index 5e60bab..0505c33 100644
--- a/src/wuttaweb/templates/grids/vue_template.mako
+++ b/src/wuttaweb/templates/grids/vue_template.mako
@@ -2,8 +2,20 @@
 
 
 
 
diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py
index 1c7518d..0fb050d 100644
--- a/src/wuttaweb/views/master.py
+++ b/src/wuttaweb/views/master.py
@@ -181,6 +181,14 @@ class MasterView(View):
 
        This is optional; see also :meth:`get_grid_columns()`.
 
+    .. attribute:: paginated
+
+       Boolean indicating whether the grid data for the
+       :meth:`index()` view should be paginated.  Default is ``True``.
+
+       This is used by :meth:`make_model_grid()` to set the grid's
+       :attr:`~wuttaweb.grids.base.Grid.paginated` flag.
+
     .. attribute:: creatable
 
        Boolean indicating whether the view model supports "creating" -
@@ -229,6 +237,7 @@ class MasterView(View):
     # features
     listable = True
     has_grid = True
+    paginated = True
     creatable = True
     viewable = True
     editable = True
@@ -1061,6 +1070,8 @@ class MasterView(View):
 
             kwargs['actions'] = actions
 
+        kwargs.setdefault('paginated', self.paginated)
+
         grid = self.make_grid(**kwargs)
         self.configure_grid(grid)
         return grid
diff --git a/tests/grids/test_base.py b/tests/grids/test_base.py
index 2d15689..9e23c02 100644
--- a/tests/grids/test_base.py
+++ b/tests/grids/test_base.py
@@ -8,24 +8,10 @@ from pyramid import testing
 from wuttjamaican.conf import WuttaConfig
 from wuttaweb.grids import base
 from wuttaweb.forms import FieldList
+from tests.util import WebTestCase
 
 
-class TestGrid(TestCase):
-
-    def setUp(self):
-        self.config = WuttaConfig(defaults={
-            'wutta.web.menus.handler_spec': 'tests.util:NullMenuHandler',
-        })
-        self.app = self.config.get_app()
-
-        self.request = testing.DummyRequest(wutta_config=self.config, use_oruga=False)
-
-        self.pyramid_config = testing.setUp(request=self.request, settings={
-            'mako.directories': ['wuttaweb:templates'],
-        })
-
-    def tearDown(self):
-        testing.tearDown()
+class TestGrid(WebTestCase):
 
     def make_grid(self, request=None, **kwargs):
         return base.Grid(request or self.request, **kwargs)
@@ -144,6 +130,44 @@ class TestGrid(TestCase):
         self.assertFalse(grid.is_linked('foo'))
         self.assertTrue(grid.is_linked('bar'))
 
+    def test_get_pagesize_options(self):
+        grid = self.make_grid()
+
+        # default
+        options = grid.get_pagesize_options()
+        self.assertEqual(options, [5, 10, 20, 50, 100, 200])
+
+        # override default
+        options = grid.get_pagesize_options(default=[42])
+        self.assertEqual(options, [42])
+
+        # from config
+        self.config.setdefault('wuttaweb.grids.default_pagesize_options', '1 2 3')
+        options = grid.get_pagesize_options()
+        self.assertEqual(options, [1, 2, 3])
+
+    def test_get_pagesize(self):
+        grid = self.make_grid()
+
+        # default
+        size = grid.get_pagesize()
+        self.assertEqual(size, 20)
+
+        # override default
+        size = grid.get_pagesize(default=42)
+        self.assertEqual(size, 42)
+
+        # override default options
+        self.config.setdefault('wuttaweb.grids.default_pagesize_options', '10 15 30')
+        grid = self.make_grid()
+        size = grid.get_pagesize()
+        self.assertEqual(size, 10)
+
+        # from config
+        self.config.setdefault('wuttaweb.grids.default_pagesize', '15')
+        size = grid.get_pagesize()
+        self.assertEqual(size, 15)
+
     def test_render_vue_tag(self):
         grid = self.make_grid(columns=['foo', 'bar'])
         html = grid.render_vue_tag()