diff --git a/aleksis/apps/resint/menus.py b/aleksis/apps/resint/menus.py
index 983f95d1346704e1184ffdfbb1419a2e72aa1008..ba45ab2760529763fa873180732a5d5ebfc85103 100644
--- a/aleksis/apps/resint/menus.py
+++ b/aleksis/apps/resint/menus.py
@@ -34,7 +34,7 @@ get_menu_entries_lazy = lazy(_get_menu_entries, list)
 MENUS = {
     "NAV_MENU_CORE": [
         {
-            "name": _("Poster"),
+            "name": _("Documents"),
             "url": "#",
             "icon": "open_in_browser",
             "root": True,
@@ -64,6 +64,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("Live documents"),
+                    "url": "live_documents",
+                    "icon": "update",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "resint.view_livedocuments_rule",
+                        ),
+                    ],
+                },
             ],
         }
     ]
diff --git a/aleksis/apps/resint/rules.py b/aleksis/apps/resint/rules.py
index 326dff271ef4136a92c0debe03552d94fddecc6d..f1d9f72db0f2e38dfc1b9f20f4c74876a2e10432 100644
--- a/aleksis/apps/resint/rules.py
+++ b/aleksis/apps/resint/rules.py
@@ -109,3 +109,32 @@ add_perm("resint.view_poster_pdf_menu", view_poster_pdf_menu_predicate)
 # Show the poster manage menu
 view_poster_menu_predicate = view_posters_predicate | view_poster_groups_predicate
 add_perm("resint.view_poster_menu", view_poster_menu_predicate)
+
+
+# View live document list
+view_live_documents_predicate = has_person & has_global_perm("resint.view_livedocument")
+add_perm("resint.view_livedocuments_rule", view_live_documents_predicate)
+
+# View live document
+view_live_document_predicate = has_person & (
+    has_global_perm("resint.view_livedocument") | has_object_perm("resint.view_livedocument")
+)
+add_perm("resint.view_livedocument_rule", view_live_document_predicate)
+
+# Add live document
+add_live_document_predicate = view_live_documents_predicate & has_global_perm(
+    "resint.add_livedocument"
+)
+add_perm("resint.add_livedocument_rule", add_live_document_predicate)
+
+# Edit live document
+edit_live_document_predicate = view_live_documents_predicate & has_global_perm(
+    "resint.change_livedocument"
+)
+add_perm("resint.edit_livedocument_rule", edit_live_document_predicate)
+
+# Delete live document
+delete_live_document_predicate = view_live_documents_predicate & has_global_perm(
+    "resint.delete_livedocument"
+)
+add_perm("resint.delete_livedocument_rule", delete_live_document_predicate)
diff --git a/aleksis/apps/resint/tables.py b/aleksis/apps/resint/tables.py
new file mode 100644
index 0000000000000000000000000000000000000000..a769e50729e28893481369929477ae2755f059ab
--- /dev/null
+++ b/aleksis/apps/resint/tables.py
@@ -0,0 +1,30 @@
+from django.utils.translation import gettext as _
+
+from django_tables2 import A, Column, LinkColumn, Table
+
+
+class LiveDocumentTable(Table):
+    """Table to list live documents."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    document_name = Column(accessor="pk")
+    name = LinkColumn("edit_live_document", args=[A("id")])
+    edit = LinkColumn(
+        "edit_live_document",
+        args=[A("id")],
+        text=_("Edit"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}},
+        verbose_name=_("Actions"),
+    )
+    delete = LinkColumn(
+        "delete_live_document",
+        args=[A("id")],
+        text=_("Delete"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
+        verbose_name=_("Actions"),
+    )
+
+    def render_document_name(self, value, record):
+        return record._meta.verbose_name
diff --git a/aleksis/apps/resint/templates/resint/live_document/create.html b/aleksis/apps/resint/templates/resint/live_document/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..9e30f4e329aa1f6447e32ea74c51a3dbfe8b025b
--- /dev/null
+++ b/aleksis/apps/resint/templates/resint/live_document/create.html
@@ -0,0 +1,21 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n data_helpers %}
+
+{% block browser_title %}
+  {% verbose_name_object model as document_title %}
+  {% blocktrans with document=document_title %}Create {{ document }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% verbose_name_object model as document_title %}
+  {% blocktrans with document=document_title %}Create {{ document }}{% endblocktrans %}
+{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
diff --git a/aleksis/apps/resint/templates/resint/live_document/edit.html b/aleksis/apps/resint/templates/resint/live_document/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..c47d5382977026a4c8a6f0f1f827d6007e4d0f93
--- /dev/null
+++ b/aleksis/apps/resint/templates/resint/live_document/edit.html
@@ -0,0 +1,21 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n data_helpers %}
+
+{% block browser_title %}
+  {% verbose_name_object object as document_title %}
+  {% blocktrans with document=document_title %}Edit {{ document }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% verbose_name_object object as document_title %}
+  {% blocktrans with document=document_title %}Edit {{ document }}{% endblocktrans %}
+{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
diff --git a/aleksis/apps/resint/templates/resint/live_document/list.html b/aleksis/apps/resint/templates/resint/live_document/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..fcebf71fc88d2c0e39538ebce725f7c60cd3e70f
--- /dev/null
+++ b/aleksis/apps/resint/templates/resint/live_document/list.html
@@ -0,0 +1,34 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n data_helpers rules %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Live documents{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Live documents{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light dropdown-trigger" href="#" data-target="widget-dropdown">
+    <i class="material-icons left">add</i>
+    {% trans "Create live document" %}
+  </a>
+
+  <ul id="widget-dropdown" class="dropdown-content">
+    <li>
+      <a href="{% url 'create_live_document' "resint" "livedocument" %}">
+        {% trans "Live document" %}
+      </a>
+    </li>
+    {% for ct, model in widget_types %}
+      <li>
+        <a href="{% url 'create_live_document' ct.app_label ct.model %}">
+          {% verbose_name_object model as widget_name %}
+          {% blocktrans with name=widget_name %}Create {{ name }}{% endblocktrans %}
+        </a>
+      </li>
+    {% endfor %}
+  </ul>
+
+  {% render_table table %}
+{% endblock %}
diff --git a/aleksis/apps/resint/urls.py b/aleksis/apps/resint/urls.py
index aea1b58abb91513c0f8a8b06d94c81fa22f67b71..f72aee866a7583501be114253243807f8e742ce6 100644
--- a/aleksis/apps/resint/urls.py
+++ b/aleksis/apps/resint/urls.py
@@ -1,6 +1,11 @@
 from django.urls import path
 
 from .views import (
+    LiveDocumentCreateView,
+    LiveDocumentDeleteView,
+    LiveDocumentEditView,
+    LiveDocumentListView,
+    LiveDocumentShowView,
     PosterCurrentView,
     PosterDeleteView,
     PosterEditView,
@@ -22,4 +27,19 @@ urlpatterns = [
     path("groups/create/", PosterGroupCreateView.as_view(), name="create_poster_group"),
     path("groups/<int:pk>/edit/", PosterGroupEditView.as_view(), name="edit_poster_group"),
     path("groups/<int:pk>/delete/", PosterGroupDeleteView.as_view(), name="delete_poster_group"),
+    path("live/", LiveDocumentListView.as_view(), name="live_documents"),
+    path(
+        "live/<str:app>/<str:model>/create/",
+        LiveDocumentCreateView.as_view(),
+        name="create_live_document",
+    ),
+    path("live/<int:pk>/edit/", LiveDocumentEditView.as_view(), name="edit_live_document",),
+    path(
+        "live_documents/<int:pk>/delete/",
+        LiveDocumentDeleteView.as_view(),
+        name="delete_live_document",
+    ),
+    path(
+        "live_documents/<str:slug>.pdf", LiveDocumentShowView.as_view(), name="show_live_document",
+    ),
 ]
diff --git a/aleksis/apps/resint/views.py b/aleksis/apps/resint/views.py
index d2e16ee48c0e3f7f2dc7985e7d8fc9a9dbf71538..f987f381cc211fd6d11802dfe788a107c2d63bdc 100644
--- a/aleksis/apps/resint/views.py
+++ b/aleksis/apps/resint/views.py
@@ -1,20 +1,28 @@
-from typing import Any, Dict
+from typing import Any, Dict, Type
 
+from django.contrib.contenttypes.models import ContentType
 from django.db.models import QuerySet
+from django.forms import BaseModelForm, modelform_factory
 from django.http import FileResponse, HttpRequest
+from django.shortcuts import get_object_or_404
 from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
 from django.utils.translation import gettext as _
 from django.views import View
+from django.views.decorators.cache import never_cache
 from django.views.generic.detail import SingleObjectMixin
 from django.views.generic.list import ListView
 
+from django_tables2 import SingleTableView
 from guardian.shortcuts import get_objects_for_user
+from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin
 
 from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
 
 from .forms import PosterGroupForm, PosterUploadForm
-from .models import Poster, PosterGroup
+from .models import LiveDocument, Poster, PosterGroup
+from .tables import LiveDocumentTable
 
 
 class PosterGroupListView(PermissionRequiredMixin, ListView):
@@ -138,3 +146,87 @@ class PosterCurrentView(PermissionRequiredMixin, SingleObjectMixin, View):
         current_poster = group.current_poster
         file = current_poster.pdf if current_poster else group.default_pdf
         return FileResponse(file, content_type="application/pdf")
+
+
+class LiveDocumentListView(PermissionRequiredMixin, SingleTableView):
+    """Table of all live documents."""
+
+    model = LiveDocument
+    table_class = LiveDocumentTable
+    permission_required = "resint.view_livedocuments_rule"
+    template_name = "resint/live_document/list.html"
+
+    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["widget_types"] = [
+            (ContentType.objects.get_for_model(m, False), m) for m in LiveDocument.__subclasses__()
+        ]
+        return context
+
+
+@method_decorator(never_cache, name="dispatch")
+class LiveDocumentCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    """Create view for live documents."""
+
+    def get_model(self, request, *args, **kwargs):
+        app_label = kwargs.get("app")
+        model = kwargs.get("model")
+        ct = get_object_or_404(ContentType, app_label=app_label, model=model)
+        return ct.model_class()
+
+    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["model"] = self.model
+        return context
+
+    def get(self, request, *args, **kwargs):
+        self.model = self.get_model(request, *args, **kwargs)
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        self.model = self.get_model(request, *args, **kwargs)
+        return super().post(request, *args, **kwargs)
+
+    fields = "__all__"
+    model = LiveDocument
+    permission_required = "resint.add_livedocument_rule"
+    template_name = "resint/live_document/create.html"
+    success_url = reverse_lazy("live_documents")
+    success_message = _("The live document has been created.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class LiveDocumentEditView(PermissionRequiredMixin, AdvancedEditView):
+    """Edit view for live documents."""
+
+    def get_form_class(self) -> Type[BaseModelForm]:
+        return modelform_factory(self.object.__class__, fields=self.fields)
+
+    model = LiveDocument
+    fields = "__all__"
+    permission_required = "resint.edit_livedocument_rule"
+    template_name = "resint/live_document/edit.html"
+    success_url = reverse_lazy("live_documents")
+    success_message = _("The live document has been saved.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class LiveDocumentDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+    """Delete view for live documents."""
+
+    model = LiveDocument
+    permission_required = "resint.delete_livedocument_rule"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("live_documents")
+    success_message = _("The live document has been deleted.")
+
+
+class LiveDocumentShowView(PermissionRequiredMixin, SingleObjectMixin, View):
+    """Show the current version of the live document."""
+
+    model = LiveDocument
+    permission_required = "resint.view_livedocument_rule"
+
+    def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> FileResponse:
+        live_document = self.get_object()
+        return FileResponse(live_document.get_current_file(), content_type="application/pdf")