diff --git a/docs/api/wutta_continuum.testing.rst b/docs/api/wutta_continuum.testing.rst
new file mode 100644
index 0000000..4afca16
--- /dev/null
+++ b/docs/api/wutta_continuum.testing.rst
@@ -0,0 +1,6 @@
+
+``wutta_continuum.testing``
+===========================
+
+.. automodule:: wutta_continuum.testing
+   :members:
diff --git a/docs/api/wutta_continuum.util.rst b/docs/api/wutta_continuum.util.rst
new file mode 100644
index 0000000..c337ddb
--- /dev/null
+++ b/docs/api/wutta_continuum.util.rst
@@ -0,0 +1,6 @@
+
+``wutta_continuum.util``
+========================
+
+.. automodule:: wutta_continuum.util
+   :members:
diff --git a/docs/conf.py b/docs/conf.py
index cf8790a..330e71f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,6 +27,7 @@ templates_path = ["_templates"]
 exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 intersphinx_mapping = {
+    "sqlalchemy": ("http://docs.sqlalchemy.org/en/latest/", None),
     "sqlalchemy-continuum": (
         "https://sqlalchemy-continuum.readthedocs.io/en/latest/",
         None,
diff --git a/docs/index.rst b/docs/index.rst
index b4add5a..a8f656f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -29,3 +29,5 @@ This package adds data versioning/history for `WuttJamaican`_, using
    api/wutta_continuum
    api/wutta_continuum.app
    api/wutta_continuum.conf
+   api/wutta_continuum.testing
+   api/wutta_continuum.util
diff --git a/src/wutta_continuum/conf.py b/src/wutta_continuum/conf.py
index dc3f476..1cb3bd7 100644
--- a/src/wutta_continuum/conf.py
+++ b/src/wutta_continuum/conf.py
@@ -45,7 +45,28 @@ class WuttaContinuumConfigExtension(WuttaConfigExtension):
     key = "wutta_continuum"
 
     def startup(self, config):  # pylint: disable=empty-docstring
-        """ """
+        """
+        Perform final configuration setup for app startup.
+
+        This will do nothing at all, unless config enables the
+        versioning feature.  This must be done in config file and not
+        in DB settings table:
+
+        .. code-block:: ini
+
+           [wutta_continuum]
+           enable_versioning = true
+
+        Once enabled, this method will configure the integration, via
+        these steps:
+
+        1. call :func:`sqlalchemy-continuum:sqlalchemy_continuum.make_versioned()`
+        2. call :meth:`wuttjamaican:wuttjamaican.app.AppHandler.get_model()`
+        3. call :func:`sqlalchemy:sqlalchemy.orm.configure_mappers()`
+
+        For more about SQLAlchemy-Continuum see
+        :doc:`sqlalchemy-continuum:intro`.
+        """
         # only do this if config enables it
         if not config.get_bool(
             "wutta_continuum.enable_versioning", usedb=False, default=False
@@ -60,14 +81,17 @@ class WuttaContinuumConfigExtension(WuttaConfigExtension):
         )
         plugin = load_object(spec)
 
-        # tell sqlalchemy-continuum to do its thing
+        app = config.get_app()
+        if "model" in app.__dict__:
+            raise RuntimeError("something not right, app already has model")
+
+        # let sqlalchemy-continuum do its thing
         make_versioned(plugins=[plugin()])
 
-        # nb. must load the model before configuring mappers
-        app = config.get_app()
-        model = app.model  # pylint: disable=unused-variable
+        # must load model *between* prev and next calls
+        app.get_model()
 
-        # tell sqlalchemy to do its thing
+        # let sqlalchemy do its thing
         configure_mappers()
 
 
diff --git a/src/wutta_continuum/testing.py b/src/wutta_continuum/testing.py
new file mode 100644
index 0000000..c9229d3
--- /dev/null
+++ b/src/wutta_continuum/testing.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+#  Wutta-Continuum -- SQLAlchemy Versioning for Wutta Framework
+#  Copyright © 2024-2025 Lance Edgar
+#
+#  This file is part of Wutta Framework.
+#
+#  Wutta Framework is free software: you can redistribute it and/or modify it
+#  under the terms of the GNU General Public License as published by the Free
+#  Software Foundation, either version 3 of the License, or (at your option) any
+#  later version.
+#
+#  Wutta Framework is distributed in the hope that it will be useful, but
+#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+#  more details.
+#
+#  You should have received a copy of the GNU General Public License along with
+#  Wutta Framework.  If not, see .
+#
+################################################################################
+"""
+Testing utilities
+"""
+
+import sys
+
+import sqlalchemy_continuum as continuum
+
+from wuttjamaican.testing import DataTestCase
+
+from wutta_continuum.conf import WuttaContinuumConfigExtension
+
+
+class VersionTestCase(DataTestCase):
+    """
+    Base class for test suites requiring the SQLAlchemy-Continuum
+    versioning feature.
+
+    This inherits from
+    :class:`~wuttjamaican:wuttjamaican.testing.DataTestCase`.
+    """
+
+    def setUp(self):
+        self.setup_versioning()
+
+    def setup_versioning(self):
+        """
+        Do setup tasks relating to this class, as well as its parent(s):
+
+        * call :meth:`wuttjamaican:wuttjamaican.testing.DataTestCase.setup_db()`
+
+          * this will in turn call :meth:`make_config()`
+        """
+        self.setup_db()
+
+    def tearDown(self):
+        self.teardown_versioning()
+
+    def teardown_versioning(self):
+        """
+        Do teardown tasks relating to this class, as well as its parent(s):
+
+        * call :func:`sqlalchemy-continuum:sqlalchemy_continuum.remove_versioning()`
+        * call :meth:`wuttjamaican:wuttjamaican.testing.DataTestCase.teardown_db()`
+        """
+        continuum.remove_versioning()
+        continuum.versioning_manager.transaction_cls = continuum.TransactionFactory()
+        self.teardown_db()
+
+    def make_config(self, **kwargs):
+        """
+        Make and customize the config object.
+
+        We override this to explicitly enable the versioning feature.
+        """
+        config = super().make_config(**kwargs)
+        config.setdefault("wutta_continuum.enable_versioning", "true")
+
+        # nb. must purge model classes from sys.modules, so they will
+        # be reloaded and sqlalchemy-continuum can reconfigure
+        if "wuttjamaican.db.model" in sys.modules:
+            del sys.modules["wuttjamaican.db.model.batch"]
+            del sys.modules["wuttjamaican.db.model.upgrades"]
+            del sys.modules["wuttjamaican.db.model.auth"]
+            del sys.modules["wuttjamaican.db.model.base"]
+            del sys.modules["wuttjamaican.db.model"]
+
+        ext = WuttaContinuumConfigExtension()
+        ext.startup(config)
+        return config
diff --git a/src/wutta_continuum/util.py b/src/wutta_continuum/util.py
new file mode 100644
index 0000000..4ca64ec
--- /dev/null
+++ b/src/wutta_continuum/util.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+#  Wutta-Continuum -- SQLAlchemy Versioning for Wutta Framework
+#  Copyright © 2024-2025 Lance Edgar
+#
+#  This file is part of Wutta Framework.
+#
+#  Wutta Framework is free software: you can redistribute it and/or modify it
+#  under the terms of the GNU General Public License as published by the Free
+#  Software Foundation, either version 3 of the License, or (at your option) any
+#  later version.
+#
+#  Wutta Framework is distributed in the hope that it will be useful, but
+#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+#  more details.
+#
+#  You should have received a copy of the GNU General Public License along with
+#  Wutta Framework.  If not, see .
+#
+################################################################################
+"""
+SQLAlchemy-Continuum utilities
+"""
+
+import sqlalchemy as sa
+from sqlalchemy import orm
+import sqlalchemy_continuum as continuum
+
+
+OPERATION_TYPES = {
+    continuum.Operation.INSERT: "INSERT",
+    continuum.Operation.UPDATE: "UPDATE",
+    continuum.Operation.DELETE: "DELETE",
+}
+
+
+def render_operation_type(operation_type):
+    """
+    Render a SQLAlchemy-Continuum ``operation_type`` from a version
+    record, for display to user.
+
+    :param operation_type: Value of same name from a version record.
+       Must be one of:
+
+       * :attr:`sqlalchemy_continuum:sqlalchemy_continuum.operation.Operation.INSERT`
+       * :attr:`sqlalchemy_continuum:sqlalchemy_continuum.operation.Operation.UPDATE`
+       * :attr:`sqlalchemy_continuum:sqlalchemy_continuum.operation.Operation.DELETE`
+
+    :returns: Display name for the operation type, as string.
+    """
+    return OPERATION_TYPES[operation_type]
+
+
+def model_transaction_query(instance, session=None, model_class=None):
+    """
+    Make a query capable of finding all SQLAlchemy-Continuum
+    ``transaction`` records associated with the given model instance.
+
+    :param instance: Instance of a versioned :term:`data model`.
+
+    :param session: Optional :term:`db session` to use for the query.
+       If not specified, will be obtained from the ``instance``.
+
+    :param model_class: Optional :term:`data model` class to query.
+       If not specified, will be obtained from the ``instance``.
+
+    :returns: SQLAlchemy query object.  Note that it will *not* have an
+       ``ORDER BY`` clause yet.
+    """
+    if not session:
+        session = orm.object_session(instance)
+    if not model_class:
+        model_class = type(instance)
+
+    txncls = continuum.transaction_class(model_class)
+    vercls = continuum.version_class(model_class)
+
+    query = session.query(txncls).join(
+        vercls,
+        sa.and_(vercls.uuid == instance.uuid, vercls.transaction_id == txncls.id),
+    )
+
+    return query
diff --git a/tests/test_conf.py b/tests/test_conf.py
index 22c2873..1ea5d44 100644
--- a/tests/test_conf.py
+++ b/tests/test_conf.py
@@ -4,33 +4,45 @@ import socket
 
 from unittest.mock import patch
 
-from wuttjamaican.testing import DataTestCase
+from wuttjamaican.testing import ConfigTestCase, DataTestCase
 
 from wutta_continuum import conf as mod
 
 
-class TestWuttaContinuumConfigExtension(DataTestCase):
+class TestWuttaContinuumConfigExtension(ConfigTestCase):
 
     def make_extension(self):
         return mod.WuttaContinuumConfigExtension()
 
-    def test_startup(self):
+    def test_startup_without_versioning(self):
         ext = self.make_extension()
-
         with patch.object(mod, "make_versioned") as make_versioned:
             with patch.object(mod, "configure_mappers") as configure_mappers:
-
-                # nothing happens by default
                 ext.startup(self.config)
                 make_versioned.assert_not_called()
                 configure_mappers.assert_not_called()
 
-                # but will if we enable it in config
+    def test_startup_with_versioning(self):
+        ext = self.make_extension()
+        with patch.object(mod, "make_versioned") as make_versioned:
+            with patch.object(mod, "configure_mappers") as configure_mappers:
                 self.config.setdefault("wutta_continuum.enable_versioning", "true")
                 ext.startup(self.config)
                 make_versioned.assert_called_once()
                 configure_mappers.assert_called_once_with()
 
+    def test_startup_with_error(self):
+        ext = self.make_extension()
+        with patch.object(mod, "make_versioned") as make_versioned:
+            with patch.object(mod, "configure_mappers") as configure_mappers:
+                self.config.setdefault("wutta_continuum.enable_versioning", "true")
+                # nb. it is an error for the model to be loaded prior to
+                # calling make_versioned() for sqlalchemy-continuum
+                self.app.get_model()
+                self.assertRaises(RuntimeError, ext.startup, self.config)
+                make_versioned.assert_not_called()
+                configure_mappers.assert_not_called()
+
 
 class TestWuttaContinuumPlugin(DataTestCase):
 
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..944c861
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8; -*-
+
+from unittest import TestCase
+
+import sqlalchemy_continuum as continuum
+
+from wutta_continuum import util as mod
+from wutta_continuum.testing import VersionTestCase
+
+
+class TestRenderOperationType(TestCase):
+
+    def test_basic(self):
+        self.assertEqual(
+            mod.render_operation_type(continuum.Operation.INSERT), "INSERT"
+        )
+        self.assertEqual(
+            mod.render_operation_type(continuum.Operation.UPDATE), "UPDATE"
+        )
+        self.assertEqual(
+            mod.render_operation_type(continuum.Operation.DELETE), "DELETE"
+        )
+
+
+class TestModelTransactionQuery(VersionTestCase):
+
+    def test_basic(self):
+        model = self.app.model
+
+        user = model.User(username="fred")
+        self.session.add(user)
+        self.session.commit()
+
+        query = mod.model_transaction_query(user)
+        self.assertEqual(query.count(), 1)
+        txn = query.one()
+
+        UserVersion = continuum.version_class(model.User)
+        version = self.session.query(UserVersion).one()
+        self.assertIs(version.transaction, txn)