diff --git a/CHANGELOG.md b/CHANGELOG.md
index 306d161f0ad91a8a7df2be341b2d79231626bbbf..ff8555cd4305c721599a381ad2cae872dfde0938 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added ###
 
+* Versioning support (experimental).
+
 ### Changed ###
 
 ### Deprecated ###
diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py
index be6dfdcfd8ce5c90929785a3649da9b745868861..7ef5df660af1578cd34838adf14815dd9f776771 100644
--- a/src/caosdb/common/models.py
+++ b/src/caosdb/common/models.py
@@ -42,6 +42,7 @@ from warnings import warn
 
 from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
                                     LIST, REFERENCE, TEXT, is_reference)
+from caosdb.common.versioning import Version
 from caosdb.common.utils import uuid, xml2str
 from caosdb.configuration import get_config
 from caosdb.connection.connection import get_connection
@@ -88,6 +89,7 @@ class Entity(object):
         self._size = None
         self._upload = None
         self._wrapped_entity = None
+        self._version = None
         self._cuid = None
         self._flags = dict()
         self.__value = None
@@ -108,6 +110,17 @@ class Entity(object):
         self.description = description
         self.id = id
 
+    @property
+    def version(self):
+        if self._version is not None or self._wrapped_entity is None:
+            return self._version
+
+        return self._wrapped_entity.version
+
+    @version.setter
+    def version(self, version):
+        self._version = version
+
     @property
     def role(self):
         return self.__role
@@ -810,6 +823,9 @@ class Entity(object):
         if self.description is not None:
             xml.set("description", str(self.description))
 
+        if self.version is not None:
+            xml.append(self.version.to_xml())
+
         if self.value is not None:
             if isinstance(self.value, Entity):
                 if self.value.id is not None:
@@ -933,6 +949,8 @@ class Entity(object):
                 entity.permissions = child
             elif isinstance(child, Message):
                 entity.add_message(child)
+            elif isinstance(child, Version):
+                entity.version = child
             elif child is None or hasattr(child, "encode"):
                 vals.append(child)
             elif isinstance(child, Entity):
@@ -1220,6 +1238,7 @@ class QueryTemplate():
         self.permissions = None
         self.is_valid = lambda: False
         self.is_deleted = lambda: False
+        self.version = None
 
     def retrieve(self, strict=True, raise_exception_on_error=True,
                  unique=True, sync=True, flags=None):
@@ -1270,6 +1289,9 @@ class QueryTemplate():
         if self.description is not None:
             xml.set("description", self.description)
 
+        if self.version is not None:
+            xml.append(self.version.to_xml())
+
         if self.query is not None:
             queryElem = etree.Element("Query")
             queryElem.text = self.query
@@ -1299,6 +1321,8 @@ class QueryTemplate():
                         q.messages.append(child)
                     elif isinstance(child, ACL):
                         q.acl = child
+                    elif isinstance(child, Version):
+                        q.version = child
                     elif isinstance(child, Permissions):
                         q.permissions = child
             q.id = int(xml.get("id"))
@@ -2184,6 +2208,7 @@ def _basic_sync(e_local, e_remote):
     e_local.permissions = e_remote.permissions
     e_local.is_valid = e_remote.is_valid
     e_local.is_deleted = e_remote.is_deleted
+    e_local.version = e_remote.version
 
     if hasattr(e_remote, "query"):
         e_local.query = e_remote.query
@@ -3817,6 +3842,8 @@ def _parse_single_xml_element(elem):
         Entity._from_xml(entity, elem)
 
         return entity
+    elif elem.tag.lower() == "version":
+        return Version.from_xml(elem)
     elif elem.tag.lower() == "emptystring":
         return ""
     elif elem.tag.lower() == "value":
diff --git a/src/caosdb/common/versioning.py b/src/caosdb/common/versioning.py
new file mode 100644
index 0000000000000000000000000000000000000000..bac99b73d5803b512ec6e6a9558acfd13a8881a5
--- /dev/null
+++ b/src/caosdb/common/versioning.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+#
+""" Versioning module for anything related to entity versions.
+
+Currently this module defines nothing but a single class, `Version`.
+"""
+
+from __future__ import absolute_import
+from caosdb.common.utils import xml2str
+from lxml import etree
+
+
+class Version():
+    """The version of an entity.
+
+    An entity version has a version id (string), a date (UTC timestamp), a
+    list of predecessors and a list of successors.
+
+    Parameters
+    ----------
+    id : str, optional
+        See attribute `id`. Default: None
+    date : str, optional
+        See attribute `data`. Default: None
+    predecessors : list of Version, optional
+        See attribute `predecessors`. Default: empty list.
+    successors : list of Version, optional
+        See attribute `successors`. Default: empty list.
+
+    Attributes
+    ----------
+    id : str
+        Version ID (not the entity's id).
+    date : str
+        UTC Timestamp of the version, i.e. the date and time when the entity of
+        this version has been inserted or modified.
+    predecessors : list of Version
+        Predecessors are the older entity versions which have been modified
+        into this version. Usually, there is only one predecessor. However,
+        this API allows for entities to be merged into one entity, which would
+        result in more than one predecessor.
+    successors : list of Version
+        Successors are newer versions of this entity. If there are successors,
+        this version is not the latest version of this entity. Usually, there
+        is only one successor. However, this API allows that a single entity
+        may co-exist in several versions (e.g. several proposals for the next
+        entity status). That would result in more than one successor.
+    """
+
+    # pylint: disable=redefined-builtin
+    def __init__(self, id=None, date=None, predecessors=None, successors=None):
+        self.id = id
+        self.date = date
+        self.predecessors = predecessors if predecessors is not None else []
+        self.successors = successors if successors is not None else []
+
+    def to_xml(self, tag="Version"):
+        """Serialize this version to xml.
+
+        The tag name is 'Version' per default. But since this method is called
+        recursively for the predecessors and successors as well, the tag name
+        can be configured.
+
+        The resulting xml element contains attributes 'id' and 'date' and
+        'Predecessor' and 'Successor' child elements.
+
+        Parameters
+        ----------
+        tag : str, optional
+            The name of the returned xml element. Defaults to 'Version'.
+
+        Returns
+        -------
+        xml : etree.Element
+        """
+        xml = etree.Element(tag)
+        if self.id is not None:
+            xml.set("id", self.id)
+        if self.date is not None:
+            xml.set("date", self.date)
+        if self.predecessors is not None:
+            for p in self.predecessors:
+                xml.append(p.to_xml(tag="Predecessor"))
+        if self.successors is not None:
+            for s in self.successors:
+                xml.append(s.to_xml(tag="Successor"))
+        return xml
+
+    def __str__(self):
+        """Return a stringified xml representation."""
+        return self.__repr__()
+
+    def __repr__(self):
+        """Return a stringified xml representation."""
+        return xml2str(self.to_xml())
+
+    @staticmethod
+    def from_xml(xml):
+        """Parse a version object from a 'Version' xml element.
+
+        Parameters
+        ----------
+        xml : etree.Element
+            A 'Version' xml element, with 'id' and 'date' attributes and
+            'Predecessor' and 'Successor' child elements.
+
+        Returns
+        -------
+        version : Version
+            a new version instance
+        """
+        predecessors = [Version.from_xml(p) for p in xml if p.tag.lower() == "predecessor"]
+        successors = [Version.from_xml(s) for s in xml if s.tag.lower() == "successor"]
+        return Version(id=xml.get("id"), date=xml.get("date"),
+                       predecessors=predecessors, successors=successors)
+
+    def __hash__(self):
+        """Hash of the version instance.
+
+        Also hashes the predecessors and successors.
+        """
+        return (hash(self.id)
+                + hash(self.date)
+                + (Version._hash_list(self.predecessors)
+                   if self.predecessors else 26335)
+                + (Version._hash_list(self.successors)
+                   if self.successors else -23432))
+
+    @staticmethod
+    def _hash_list(_list):
+        """Hash a list by hashing each element and its index."""
+        result = 12352
+        for idx, val in enumerate(_list):
+            result += hash(val) + idx
+        return result
+
+    @staticmethod
+    def _eq_list(this, that):
+        """List equality.
+
+        List equality is defined as equality of each element, the order
+        and length.
+        """
+        if len(this) != len(that):
+            return False
+        for v1, v2 in zip(this, that):
+            if v1 != v2:
+                return False
+        return True
+
+    def __eq__(self, other):
+        """Equality of versions is defined by equality of id, date, and list
+        equality of the predecessors and successors."""
+        return (self.id == other.id
+                and self.date == other.date
+                and Version._eq_list(self.predecessors, other.predecessors)
+                and Version._eq_list(self.successors, other.successors))
diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py
index df6fe52d5b6f7ceddea791167a2355d7b3bf2fc8..c560b5e3c7c424b762bc8381c7cc9f42617288d0 100644
--- a/unittests/test_apiutils.py
+++ b/unittests/test_apiutils.py
@@ -27,9 +27,9 @@
 # A. Schlemmer, 02/2018
 
 import caosdb as db
-from caosdb.apiutils import apply_to_ids
 import pickle
 import tempfile
+from caosdb.apiutils import apply_to_ids
 from .test_property import testrecord
 
 
diff --git a/unittests/test_record.xml b/unittests/test_record.xml
index 5567e59050fdbcd07ce9b13cdc640c7bccf6c165..e961bdc6b88eb8e62b92696b52d7ad6a2dbf8089 100644
--- a/unittests/test_record.xml
+++ b/unittests/test_record.xml
@@ -1,4 +1,5 @@
 <Record id="171179">
+  <Version id="version-str" date="2019-04-02T12:22:34.837UTC"/>
   <TransactionBenchmark></TransactionBenchmark>
   <Parent description="Experiment for the LEAP project." id="163454" name="LeapExperiment"/>
   <Property datatype="TEXT" description="A unique identifier for experiments" flag="inheritance:FIX" id="163414" importance="FIX" name="experimentId">KCdSYWJiaXQnLCAnZXhfdml2bycsICdzeW5jaHJvbml6YXRpb25fbWFwJywgTm9uZSwgJzIwMTctMDgtMTEnLCBOb25lLCBOb25lKQ==</Property>
diff --git a/unittests/test_versioning.py b/unittests/test_versioning.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6009e79949c4b96ab0a6645eb6ec3cf28461441
--- /dev/null
+++ b/unittests/test_versioning.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+#
+
+from __future__ import absolute_import
+from caosdb import Record
+from caosdb.common.utils import xml2str
+from caosdb.common.versioning import Version
+from .test_property import testrecord
+
+
+def test_constructor():
+    v = Version(id="1234abcd", date="2020-01-01T20:15:00.000UTC",
+                predecessors=[Version(id="2345abdc",
+                                      date="2020-01-01T20:00:00.000UTC")],
+                successors=[Version(id="3465abdc",
+                                    date="2020-01-01T20:30:00.000UTC")])
+    assert v.id == "1234abcd"
+    assert v.date == "2020-01-01T20:15:00.000UTC"
+    assert isinstance(v.predecessors, list)
+    assert isinstance(v.predecessors[0], Version)
+    assert isinstance(v.successors, list)
+    assert isinstance(v.successors[0], Version)
+
+    return v
+
+
+def test_to_xml():
+    v = test_constructor()
+    xmlstr = xml2str(v.to_xml())
+    assert xmlstr == ('<Version id="{i}" date="{d}">\n'
+                      '  <Predecessor id="{pi}" date="{pd}"/>\n'
+                      '  <Successor id="{si}" date="{sd}"/>\n'
+                      '</Version>\n').format(i=v.id, d=v.date,
+                                             pi=v.predecessors[0].id,
+                                             pd=v.predecessors[0].date,
+                                             si=v.successors[0].id,
+                                             sd=v.successors[0].date)
+
+    xmlstr2 = xml2str(v.to_xml(tag="OtherVersionTag"))
+    assert xmlstr2 == ('<OtherVersionTag id="{i}" date="{d}">\n'
+                       '  <Predecessor id="{pi}" date="{pd}"/>\n'
+                       '  <Successor id="{si}" date="{sd}"/>\n'
+                       '</OtherVersionTag>\n'
+                       ).format(i=v.id, d=v.date, pi=v.predecessors[0].id,
+                                pd=v.predecessors[0].date,
+                                si=v.successors[0].id, sd=v.successors[0].date)
+
+
+def test_equality():
+    v = test_constructor()
+    assert hash(v) == hash(v)
+    v2 = test_constructor()
+    assert hash(v) == hash(v2)
+    assert v == v2
+
+    v = Version()
+    v2 = Version()
+    assert hash(v) == hash(v2)
+    assert v == v2
+
+    v = Version(id="123")
+    v2 = Version(id="123")
+    v3 = Version(id="2345")
+    assert hash(v) == hash(v2)
+    assert v == v2
+    assert hash(v) != hash(v3)
+    assert v != v3
+
+    v = Version(id="123", date="2345", predecessors=None)
+    v2 = Version(id="123", date="2345", predecessors=[])
+    v3 = Version(id="123", date="Another date", predecessors=[])
+    assert hash(v) == hash(v2)
+    assert v == v2
+    assert hash(v) != hash(v3)
+    assert v != v3
+
+    v = Version(id="123", date="2345", predecessors=[Version(id="bla")])
+    v2 = Version(id="123", date="2345", predecessors=[Version(id="bla")])
+    v3 = Version(id="123", date="2345", predecessors=[Version(id="blub")])
+    v4 = Version(id="123", date="2345", predecessors=[Version(id="bla"),
+                                                      Version(id="bla")])
+    assert hash(v) == hash(v2)
+    assert v == v2
+    assert hash(v) != hash(v3)
+    assert v != v3
+    assert hash(v) != hash(v4)
+    assert v != v4
+
+    v = Version(id="123", date="2345", predecessors=[Version(id="bla")],
+                successors=[Version(id="234")])
+    v2 = Version(id="123", date="2345", predecessors=[Version(id="bla")],
+                 successors=[Version(id="234")])
+    v3 = Version(id="123", date="2345", predecessors=[Version(id="bla")],
+                 successors=[Version(id="bluup")])
+    assert hash(v) == hash(v2)
+    assert v == v2
+    assert hash(v) != hash(v3)
+    assert v != v3
+
+
+def test_from_xml():
+    v = test_constructor()
+    xml = v.to_xml()
+
+    v2 = Version.from_xml(xml)
+
+    assert hash(v) == hash(v2)
+    assert v == v2
+    assert str(v) == str(v2)
+
+
+def test_version_deserialization():
+    assert testrecord.version == Version(id="version-str",
+                                         date="2019-04-02T12:22:34.837UTC")
+
+
+def test_version_serialization():
+    r = Record()
+    r.version = Version(id="test-version", date="asdfsadf")
+
+    # <Record><Version id="test-version" date="asdfsadf"/></Record>
+    assert "test-version" == r.to_xml().xpath("/Record/Version/@id")[0]
+    assert "asdfsadf" == r.to_xml().xpath("/Record/Version/@date")[0]