diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php
index 834764f7e2945b8b750ed4652f75b5b0ce738661..b22ffb79bf89e2ae45e147f2b9e65cba12346c60 100755
--- a/app/controllers/course/courseware.php
+++ b/app/controllers/course/courseware.php
@@ -170,7 +170,7 @@ class Course_CoursewareController extends AuthenticatedController
 
     private function getProgress(Course $course, StructuralElement $element, bool $course_progress = false, array $cw_user_progresses, array $course_member_ids): array
     {
-        $descendants = $element->findDescendants();
+        $descendants = $element->findDescendants(\User::findCurrent());
         $count = count($descendants);
         $progress = 0;
         $own_progress = 0;
diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php
index 63b32b496a9f6401b12e3607241985c42c8554e5..cfc15ce4dc223598fffff8fb3e381accb74c25bf 100755
--- a/lib/classes/JsonApi/Routes/Courseware/Authority.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php
@@ -34,25 +34,9 @@ class Authority
      */
     public static function canShowBlock(User $user, Block $resource)
     {
-        if ($GLOBALS['perm']->have_perm('root')) {
-            return true;
-        }
-
         $struct = $resource->container->structural_element;
 
-        if ('user' == $struct->range_type) {
-            if ($user->id == $struct->range_id) {
-                return true;
-            } else {
-                return false;
-            }
-        } elseif ($struct->range_type == 'course') {
-            return $GLOBALS['perm']->have_studip_perm('user', $struct->course->id, $user->id) ||
-                self::canUpdateStructuralElement($user, $struct) ||
-                $struct->canRead($user);
-        } else {
-            return false; // should we throw an exeption here?
-        }
+        return $struct->canRead($user);
     }
 
     public static function canIndexBlocks(User $user, Container $resource)
@@ -69,9 +53,9 @@ class Authority
     {
         if ($resource->isBlocked()) {
             return $resource->getBlockerUserId() == $user->id;
-        } else {
-            return self::canUpdateContainer($user, $resource->container);
         }
+
+        return self::canUpdateContainer($user, $resource->container);
     }
 
     public static function canDeleteBlock(User $user, Block $resource)
@@ -119,54 +103,14 @@ class Authority
         return self::canUpdateStructuralElement($user, $resource);
     }
 
-    /**
-     * @SuppressWarnings(PHPMD.Superglobals)
-     */
     public static function canShowStructuralElement(User $user, StructuralElement $resource)
     {
-        if ($GLOBALS['perm']->have_perm('root')) {
-            return true;
-        }
-        if ($resource->range_type == 'user') {
-            if ($user->id == $resource->range_id) {
-                return true;
-            } else {
-                return false;
-            }
-        } elseif ($resource->range_type == 'course') {
-            return $GLOBALS['perm']->have_studip_perm('user', $resource->course->id, $user->id) ||
-                self::canUpdateStructuralElement($user, $resource) ||
-                $resource->canRead($user);
-        } else {
-            return false; // should we throw an exeption here?
-        }
+        return $resource->canRead($user);
     }
 
-    /**
-     * @SuppressWarnings(PHPMD.Superglobals)
-     */
     public static function canUpdateStructuralElement(User $user, StructuralElement $resource)
     {
-        if ($GLOBALS['perm']->have_perm('root')) {
-            return true;
-        }
-
-        $perm = false;
-
-        if ($resource->user) {
-            // check if user is owner of the courseware for this element
-            $perm = $resource->user->id == $user->id;
-
-            return $perm || $resource->canEdit($user);
-        } elseif ($resource->course) {
-            $perm = $GLOBALS['perm']->have_studip_perm(
-                $resource->course->config->COURSEWARE_EDITING_PERMISSION,
-                $resource->course->id,
-                $user->id
-            );
-
-            return $perm || $resource->canEdit($user);
-        }
+        return $resource->canEdit($user);
     }
 
     public static function canCreateStructuralElement(User $user, StructuralElement $resource)
@@ -204,7 +148,7 @@ class Authority
 
     public static function canShowUserDataField(User $user, UserDataField $resource)
     {
-        return  $user->id == $resource->user_id;;
+        return $user->id === $resource->user_id;
     }
 
     public static function canUpdateUserDataField(User $user, UserDataField $resource)
@@ -260,7 +204,7 @@ class Authority
 
     public static function canShowBlockFeedback(User $user, BlockFeedback $resource)
     {
-        return $resource->user_id === $user->id || self::canUpdateBlock($resource->block);
+        return $resource->user_id === $user->id || self::canUpdateBlock($user, $resource->block);
     }
 
     public static function canUploadStructuralElementsImage(User $user, StructuralElement $resource)
diff --git a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php
index 3ea220ae5dc0014b5f55eb7195d395582f151bb6..c715d395d36d714d38e1e17e59cd22feaca3b9b2 100755
--- a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php
@@ -25,9 +25,6 @@ class BookmarkedStructuralElementsIndex extends JsonApiController
         'containers.blocks.user-data-field',
         'containers.blocks.user-progress',
         'course',
-        'descendants',
-        'descendants.containers',
-        'descendants.containers.blocks',
         'editor',
         'owner',
         'parent',
diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesShow.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesShow.php
index ab961264a806aadda20f6b2b4b2ffd4e240c2842..9516ac7ddfd06bdb54e010b0e119dbf18ebf78ca 100755
--- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesShow.php
+++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesShow.php
@@ -15,7 +15,7 @@ class CoursewareInstancesShow extends JsonApiController
 {
     use CoursewareInstancesHelper;
 
-    protected $allowedIncludePaths = ['bookmarks', 'root', 'root.descendants'];
+    protected $allowedIncludePaths = ['bookmarks', 'root'];
 
     /**
      * @SuppressWarnings(PHPMD.UnusedFormalParameter)
diff --git a/lib/classes/JsonApi/Routes/Courseware/DescendantsOfStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/DescendantsOfStructuralElementsIndex.php
index 4f50af793f751efb9e99ef22e595d17d0987c4b2..8cf12d334e1d6181518d575a47f474d84b131ca0 100755
--- a/lib/classes/JsonApi/Routes/Courseware/DescendantsOfStructuralElementsIndex.php
+++ b/lib/classes/JsonApi/Routes/Courseware/DescendantsOfStructuralElementsIndex.php
@@ -6,6 +6,7 @@ use Courseware\StructuralElement;
 use JsonApi\Errors\AuthorizationFailedException;
 use JsonApi\Errors\RecordNotFoundException;
 use JsonApi\JsonApiController;
+use Neomerx\JsonApi\Contracts\Http\ResponsesInterface;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
 
@@ -16,34 +17,58 @@ class DescendantsOfStructuralElementsIndex extends JsonApiController
 {
     protected $allowedPagingParameters = ['offset', 'limit'];
 
-    protected $allowedIncludePaths = [
-        'containers',
-        'course',
-        'descendants',
-        'editor',
-        'owner',
-        'parent',
-    ];
+    protected $allowedIncludePaths = ['containers', 'course', 'editor', 'owner', 'parent'];
 
     /**
      * @SuppressWarnings(PHPMD.UnusedFormalParameter)
      */
     public function __invoke(Request $request, Response $response, $args)
     {
-        if (!$resource = StructuralElement::find($args['id'])) {
+        /** @var ?StructuralElement $resource */
+        $resource = StructuralElement::find($args['id']);
+        if (!$resource) {
             throw new RecordNotFoundException();
         }
 
-        if (!Authority::canShowStructuralElement($this->getUser($request), $resource)) {
+        if (!Authority::canShowStructuralElement($user = $this->getUser($request), $resource)) {
             throw new AuthorizationFailedException();
         }
 
+        $descendants = $resource->findDescendants($user);
+
         list($offset, $limit) = $this->getOffsetAndLimit();
-        $descendants = $resource->findDescendants();
+        $page = array_slice($descendants, $offset, $limit);
+        $total = count($descendants);
+
+        // compute ETag, compare it and short-cut this route if they match
+        $etag = $this->getETag($user, $resource, $descendants);
+        if ($request->hasHeader('If-None-Match')) {
+            $sentETag = $request->getHeaderLine('If-None-Match');
+            if ($etag === $sentETag) {
+                return $response->withStatus(304)->withHeader('Cache-Control', 'private, must-revalidate');
+            }
+        }
 
         return $this->getPaginatedContentResponse(
-            array_slice($descendants, $offset, $limit),
-            count($descendants)
+            $page,
+            $total,
+            ResponsesInterface::HTTP_OK,
+            [],
+            [],
+            [
+                'Cache-Control' => 'private, must-revalidate',
+                'ETag' => $etag,
+            ]
         );
     }
+
+    private function getEtag(\User $user, StructuralElement $resource, array $elements): string
+    {
+        $ids = join(',', array_column($elements, 'id'));
+        $maxChdate = count($elements) ? max(array_column($elements, 'chdate')) : '';
+
+        $payload = [$user->id, $ids, $maxChdate];
+
+        return 'W/"' . md5(join(',', $payload)) . '"';
+    }
 }
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsIndex.php
index 987d58906721cdb36852f9bbc44c0208c6aa7162..73795fe96a0d39c6ad8c0acc62d784c5057528ed 100755
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsIndex.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsIndex.php
@@ -25,9 +25,6 @@ class StructuralElementsIndex extends JsonApiController
         'containers.blocks.user-data-field',
         'containers.blocks.user-progress',
         'course',
-        'descendants',
-        'descendants.containers',
-        'descendants.containers.blocks',
         'editor',
         'owner',
         'parent',
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php
index 961f5cab131c173f7c4bc2fe1960df4a7392e156..9589dd386185ae1dc634e7c52b0626e411291940 100755
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php
@@ -6,6 +6,7 @@ use Courseware\StructuralElement;
 use JsonApi\Errors\AuthorizationFailedException;
 use JsonApi\Errors\RecordNotFoundException;
 use JsonApi\JsonApiController;
+use Neomerx\JsonApi\Contracts\Http\ResponsesInterface;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
 
@@ -24,9 +25,6 @@ class StructuralElementsShow extends JsonApiController
         'containers.blocks.user-data-field',
         'containers.blocks.user-progress',
         'course',
-        'descendants',
-        'descendants.containers',
-        'descendants.containers.blocks',
         'editor',
         'owner',
         'parent',
@@ -37,15 +35,18 @@ class StructuralElementsShow extends JsonApiController
      */
     public function __invoke(Request $request, Response $response, $args)
     {
-        if (!$resource = StructuralElement::find($args['id'])) {
+        /** @var ?StructuralElement $resource*/
+        $resource = StructuralElement::find($args['id']);
+        if (!$resource) {
             throw new RecordNotFoundException();
         }
 
-        if (!Authority::canShowStructuralElement($this->getUser($request), $resource)) {
+        $user = $this->getUser($request);
+        if (!Authority::canShowStructuralElement($user, $resource)) {
             throw new AuthorizationFailedException();
         }
 
-        $last = \UserConfig::get($GLOBALS['user']->id)->getValue('COURSEWARE_LAST_ELEMENT');
+        $last = \UserConfig::get($user->id)->getValue('COURSEWARE_LAST_ELEMENT');
 
         if ($resource->user) {
             $last['global'] = $args['id'];
@@ -55,8 +56,10 @@ class StructuralElementsShow extends JsonApiController
             throw new RecordNotFoundException();
         }
 
-        \UserConfig::get($GLOBALS['user']->id)->store('COURSEWARE_LAST_ELEMENT', $last);
+        \UserConfig::get($user->id)->store('COURSEWARE_LAST_ELEMENT', $last);
 
-        return $this->getContentResponse($resource);
+        $meta = [ 'can-visit' => $resource->canVisit($user) ];
+
+        return $this->getContentResponse($resource, ResponsesInterface::HTTP_OK, [], $meta);
     }
 }
diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php
index df8052b25fb5be0035e8eb27c9d42a27426a3677..af09f009439b43f47b6a369e6294ba84d9c2d477 100755
--- a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php
@@ -4,6 +4,7 @@ namespace JsonApi\Schemas\Courseware;
 
 use JsonApi\Schemas\SchemaProvider;
 use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Identifier;
 use Neomerx\JsonApi\Schema\Link;
 
 class StructuralElement extends SchemaProvider
@@ -49,7 +50,6 @@ class StructuralElement extends SchemaProvider
             'write-approval' => $resource['write_approval']->getIterator(),
             'copy-approval' => $resource['copy_approval']->getIterator(),
             'can-edit' => $resource->canEdit($user),
-            'can-read' => $resource->canRead($user),
 
             'external-relations' => $resource['external_relations']->getIterator(),
             'mkdate' => date('c', $resource['mkdate']),
@@ -67,75 +67,47 @@ class StructuralElement extends SchemaProvider
     {
         $relationships = [];
 
-        $relationships[self::REL_CHILDREN] = [
-            self::RELATIONSHIP_LINKS => [
-                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_CHILDREN),
-            ],
-            self::RELATIONSHIP_DATA => $resource->children,
-        ];
-
-        $relationships[self::REL_CONTAINERS] = [
-            self::RELATIONSHIP_LINKS => [
-                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_CONTAINERS),
-            ],
-            self::RELATIONSHIP_DATA => $resource->containers,
-        ];
-
-        if ($resource->course) {
-            $relationships[self::REL_COURSE] = [
-                self::RELATIONSHIP_LINKS => [
-                    Link::RELATED => $this->createLinkToResource($resource->course),
-                ],
-                self::RELATIONSHIP_DATA => $resource->course,
-            ];
-        }
+        $relationships = $this->addChildrenRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_CHILDREN)
+        );
 
-        if ($resource->user) {
-            $relationships[self::REL_USER] = [
-                self::RELATIONSHIP_LINKS => [
-                    Link::RELATED => $this->createLinkToResource($resource->user),
-                ],
-                self::RELATIONSHIP_DATA => $resource->user,
-            ];
-        }
+        $relationships = $this->addContainersRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_CONTAINERS)
+        );
 
-        $relationships[self::REL_OWNER] = $resource['owner_id']
-            ? [
-                self::RELATIONSHIP_LINKS => [
-                    Link::RELATED => $this->createLinkToResource($resource->owner),
-                ],
-                self::RELATIONSHIP_DATA => $resource->owner,
-            ]
-            : [self::RELATIONSHIP_DATA => null];
+        $relationships = $this->addRangeRelationship(
+            $relationships,
+            $resource,
+            $context
+        );
 
-        $relationships[self::REL_EDITOR] = $resource['editor_id']
-            ? [
-                self::RELATIONSHIP_LINKS => [
-                    Link::RELATED => $this->createLinkToResource($resource->editor),
-                ],
-                self::RELATIONSHIP_DATA => $resource->editor,
-            ]
-            : [self::RELATIONSHIP_DATA => $resource->editor];
+        $relationships = $this->addOwnerRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_OWNER)
+        );
 
-        $relationships[self::REL_EDITBLOCKER] = $resource['edit_blocker_id']
-            ? [
-                self::RELATIONSHIP_LINKS_SELF => true,
-                self::RELATIONSHIP_LINKS => [
-                    Link::RELATED => $this->createLinkToResource($resource->edit_blocker),
-                ],
-                self::RELATIONSHIP_DATA => $resource->edit_blocker,
-            ]
-            : [self::RELATIONSHIP_LINKS_SELF => true, self::RELATIONSHIP_DATA => null];
+        $relationships = $this->addEditorRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_EDITOR)
+        );
 
-        $relationships[self::REL_PARENT] = $resource->parent_id
-            ? [
-                self::RELATIONSHIP_LINKS => [
-                    Link::RELATED => $this->createLinkToResource($resource->parent),
-                ],
+        $relationships = $this->addEditBlockerRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_EDITBLOCKER)
+        );
 
-                self::RELATIONSHIP_DATA => $resource->parent,
-            ]
-            : [self::RELATIONSHIP_DATA => null];
+        $relationships = $this->addParentRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_PARENT)
+        );
 
         $relationships = $this->addAncestorsRelationship(
             $relationships,
@@ -176,6 +148,54 @@ class StructuralElement extends SchemaProvider
         return $relationships;
     }
 
+    private function addChildrenRelationship(array $relationships, $resource, bool $includeData): array
+    {
+        $relation = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_CHILDREN),
+            ],
+        ];
+
+        if ($includeData) {
+            $user = $this->currentUser;
+            $relation[self::RELATIONSHIP_DATA] = array_filter(
+                $resource->children,
+                function ($child) use ($user) {
+                    return $child->canRead($user);
+                }
+            );
+        }
+
+        $relationships[self::REL_CHILDREN] = $relation;
+
+        return $relationships;
+    }
+
+    private function addContainersRelationship(array $relationships, $resource, bool $includeData): array
+    {
+        $relation = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_CONTAINERS),
+            ],
+        ];
+
+        if ($includeData) {
+            $relation[self::RELATIONSHIP_DATA] = $resource->containers;
+        } else {
+            $relation[self::RELATIONSHIP_DATA] = function () use ($resource) {
+                $sql = 'SELECT id FROM cw_containers WHERE structural_element_id = ?';
+                $containers = \DBManager::get()->fetchAll($sql, [$resource->id], function ($container) {
+                    return new Identifier($container['id'], \JsonApi\Schemas\Courseware\Container::TYPE);
+                });
+
+                return $containers;
+            };
+        }
+        $relationships[self::REL_CONTAINERS] = $relation;
+
+        return $relationships;
+    }
+
     private function addDescendantsRelationship(array $relationships, $resource, $includeData)
     {
         $relation = [
@@ -185,7 +205,8 @@ class StructuralElement extends SchemaProvider
         ];
 
         if ($includeData) {
-            $related = $resource->findDescendants();
+            $user = $this->currentUser;
+            $related = $resource->findDescendants($user);
             $relation[self::RELATIONSHIP_DATA] = $related;
         }
 
@@ -196,13 +217,14 @@ class StructuralElement extends SchemaProvider
 
     private function addImageRelationship(array $relationships, $resource, $includeData)
     {
+        $image = $resource->image;
         $relation = [
-            self::RELATIONSHIP_DATA => $resource->image ?: null,
+            self::RELATIONSHIP_DATA => $image ?: null,
         ];
 
-        if ($resource->image) {
+        if ($image) {
             $relation[self::RELATIONSHIP_META] = [
-                'download-url' => $resource->image->getFileType()->getDownloadURL(),
+                'download-url' => $resource->getImageUrl(),
             ];
         }
 
@@ -210,4 +232,135 @@ class StructuralElement extends SchemaProvider
 
         return $relationships;
     }
+
+    private function addEditBlockerRelationship(array $relationships, $resource, bool $includeData): array
+    {
+        $relation = [
+            self::RELATIONSHIP_LINKS_SELF => true,
+        ];
+        if ($resource['edit_blocker_id']) {
+            $relation[self::RELATIONSHIP_LINKS] = [
+                Link::RELATED => $this->createLinkToUser($resource['edit_blocker_id']),
+            ];
+            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->edit_blocker : new Identifier($resource['edit_blocker_id'], \JsonApi\Schemas\User::TYPE);
+        } else {
+            $relation[self::RELATIONSHIP_DATA] = null;
+        }
+        $relationships[self::REL_EDITBLOCKER] = $relation;
+
+        return $relationships;
+    }
+
+    private function addEditorRelationship(array $relationships, $resource, bool $includeData): array
+    {
+        $relation = [];
+        if ($resource['editor_id']) {
+            $relation[self::RELATIONSHIP_LINKS] = [
+                Link::RELATED => $this->createLinkToUser($resource['editor_id']),
+            ];
+            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->editor : new Identifier($resource['editor_id'], \JsonApi\Schemas\User::TYPE);
+        } else {
+            $relation[self::RELATIONSHIP_DATA] = null;
+        }
+        $relationships[self::REL_EDITOR] = $relation;
+
+        return $relationships;
+    }
+
+    private function addOwnerRelationship(array $relationships, $resource, bool $includeData): array
+    {
+        $relation = [];
+        if ($resource['owner_id']) {
+            $relation[self::RELATIONSHIP_LINKS] = [
+                Link::RELATED => $this->createLinkToUser($resource['owner_id']),
+            ];
+            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->owner : new Identifier($resource['owner_id'], \JsonApi\Schemas\User::TYPE);
+        } else {
+            $relation[self::RELATIONSHIP_DATA] = null;
+        }
+        $relationships[self::REL_OWNER] = $relation;
+
+        return $relationships;
+    }
+
+    private function addParentRelationship(array $relationships, $resource, bool $includeData): array
+    {
+        $relation = [];
+
+        if ($resource['parent_id']) {
+            $relation[self::RELATIONSHIP_LINKS] = [
+                Link::RELATED => $this->createLinkToStructuralElement($resource['parent_id']),
+            ];
+            $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->parent : new Identifier($resource['parent_id'], self::TYPE);
+        } else {
+            $relation[self::RELATIONSHIP_DATA] = null;
+        }
+        $relationships[self::REL_PARENT] = $relation;
+
+        return $relationships;
+    }
+
+    private function addRangeRelationship(array $relationships, $resource, $context): array
+    {
+        if ($resource['range_type'] === 'course') {
+            $includeData = $this->shouldInclude($context, self::REL_COURSE);
+            $relationships[self::REL_COURSE] = [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToCourse($resource['range_id']),
+                ],
+                self::RELATIONSHIP_DATA => $includeData ? $resource->course : new Identifier($resource['range_id'], \JsonApi\Schemas\Course::TYPE),
+            ];
+        } elseif ($resource['range_type'] === 'user') {
+            $includeData = $this->shouldInclude($context, self::REL_USER);
+            $relationships[self::REL_USER] = [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToUser($resource['range_id']),
+                ],
+                self::RELATIONSHIP_DATA => $includeData ? $resource->user : new Identifier($resource['range_id'], \JsonApi\Schemas\User::TYPE),
+            ];
+        }
+
+        return $relationships;
+    }
+
+    private static $memo = [];
+
+    private function createLinkToCourse($rangeId)
+    {
+        if (isset(self::$memo['course' . $rangeId])) {
+            return self::$memo['course' . $rangeId];
+        }
+
+        $course = \Course::build(['id' => $rangeId], false);
+        $link = $this->createLinkToResource($course);
+        self::$memo['course' . $rangeId] = $link;
+
+        return $link;
+    }
+
+    private function createLinkToStructuralElement($structuralElementId)
+    {
+        if (isset(self::$memo['structuralelement' . $structuralElementId])) {
+            return self::$memo['structuralelement' . $structuralElementId];
+        }
+
+        $structuralElement = \Courseware\StructuralElement::build(['id' => $structuralElementId], false);
+        $link = $this->createLinkToResource($structuralElement);
+        self::$memo['structuralelement' . $structuralElementId] = $link;
+
+        return $link;
+    }
+
+    private function createLinkToUser($rangeId)
+    {
+        if (isset(self::$memo['user' . $rangeId])) {
+            return self::$memo['user' . $rangeId];
+        }
+
+        $course = \User::build(['id' => $rangeId], false);
+        $link = $this->createLinkToResource($course);
+        self::$memo['user' . $rangeId] = $link;
+
+        return $link;
+    }
 }
diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php
index 0637bd7e8a1c1a186e231188a4f5a4e06b3ea169..44451504a56aecefaaf312abfc0eab8a6d5d18f6 100755
--- a/lib/models/Courseware/StructuralElement.php
+++ b/lib/models/Courseware/StructuralElement.php
@@ -174,18 +174,21 @@ class StructuralElement extends \SimpleORMap
      *
      * @return bool true if the user may edit this instance
      *
-     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
      * @SuppressWarnings(PHPMD.Superglobals)
      */
     public function canEdit($user): bool
     {
+        if ($GLOBALS['perm']->have_perm('root', $user->id)) {
+            return true;
+        }
+
         switch ($this->range_type) {
             case 'user':
-                return $this->range_id == $user->id;
+                return $this->range_id === $user->id;
 
             case 'course':
                 $haveStudipPerm = $GLOBALS['perm']->have_studip_perm(
-                    $this->course->config->COURSEWARE_EDITING_PERMISSION,
+                    \CourseConfig::get($this->range_id)->COURSEWARE_EDITING_PERMISSION,
                     $this->range_id,
                     $user->id
                 );
@@ -193,32 +196,7 @@ class StructuralElement extends \SimpleORMap
                     return true;
                 }
 
-                if (!count($this->write_approval)) {
-                    return false;
-                }
-
-                if ($this->write_approval['all']) {
-                    return true;
-                }
-                $users = $this->write_approval['users'];
-                $groups = $this->write_approval['groups'];
-
-                // find user in users
-                foreach ($users as $approvedUserId) {
-                    if ($approvedUserId == $user->id) {
-                        return true;
-                    }
-                }
-                // find user in groups
-                foreach ($groups as $groupId) {
-                    /** @var ?\Statusgruppen $group */
-                    $group = \Statusgruppen::find($groupId);
-                    if ($group && $group->isMember($user->id)) {
-                        return true;
-                    }
-                }
-
-                return false;
+                return $this->hasWriteApproval($user);
 
             default:
                 throw new \InvalidArgumentException('Unknown range type.');
@@ -234,36 +212,119 @@ class StructuralElement extends \SimpleORMap
      */
     public function canRead($user): bool
     {
-        if ($this->canEdit($user)) {
+        // root darf immer
+        if ($GLOBALS['perm']->have_perm('root', $user->id)) {
             return true;
         }
 
-        if (!$this->releasedForReaders($this)) {
-            return false;
+        switch ($this->range_type) {
+            case 'user':
+                // Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen.
+                return $this->range_id === $user->id;
+
+            case 'course':
+                if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) {
+                    return false;
+                }
+
+                if ($this->canEdit($user)) {
+                    return true;
+                }
+
+                if (!$this->releasedForReaders($this)) {
+                    return false;
+                }
+
+                return $this->hasReadApproval($user);
+
+            default:
+                throw new \InvalidArgumentException('Unknown range type.');
+        }
+    }
+
+    public function canVisit($user): bool
+    {
+        // root darf immer
+        if ($GLOBALS['perm']->have_perm('root', $user->id)) {
+            return true;
         }
 
+        switch ($this->range_type) {
+            case 'user':
+                // Kontext "user": Nutzende können nur ihre eigenen Strukturknoten sehen.
+                return $this->range_id === $user->id;
+
+            case 'course':
+                if (!$GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user->id)) {
+                    return false;
+                }
+
+                if ($this->canEdit($user)) {
+                    return true;
+                }
+
+                if (!$this->releasedForReaders($this)) {
+                    return false;
+                }
+
+                return $this->hasReadApproval($user) && $this->canReadSequential($user);
+
+            default:
+                throw new \InvalidArgumentException('Unknown range type.');
+        }
+    }
+
+    private function hasReadApproval($user): bool
+    {
         if (!count($this->read_approval)) {
-            return $this->canReadSequential($user);
+            return true;
         }
 
         if ($this->read_approval['all']) {
-            return $this->canReadSequential($user);
+            return true;
         }
-        $users = $this->read_approval['users'];
-        $groups = $this->read_approval['groups'];
 
         // find user in users
-        foreach ($users as $user) {
-            if ($user == $user->id) {
-                return $this->canReadSequential($user);
+        $users = $this->read_approval['users'];
+        foreach ($users as $approvedUserId) {
+            if ($approvedUserId == $user->id) {
+                return true;
             }
         }
+
         // find user in groups
+        $groups = $this->read_approval['groups'];
         foreach ($groups as $groupId) {
             /** @var ?\Statusgruppen $group */
             $group = \Statusgruppen::find($groupId);
             if ($group && $group->isMember($user->id)) {
-                return $this->canReadSequential($user);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private function hasWriteApproval($user): bool
+    {
+        if (!count($this->write_approval)) {
+            return false;
+        }
+
+        if ($this->write_approval['all']) {
+            return true;
+        }
+
+        // find user in users
+        $users = $this->write_approval['users']->getArrayCopy();
+        if (in_array($user->id, $users)) {
+            return true;
+        }
+
+        // find user in groups
+        foreach (\Statusgruppen::findMany($this->write_approval['groups']->getArrayCopy()) as $group) {
+            if ($group->isMember($user->id)) {
+                return true;
             }
         }
 
@@ -279,7 +340,7 @@ class StructuralElement extends \SimpleORMap
      */
     private function canReadSequential($user): bool
     {
-        if (!$this->course->config->COURSEWARE_SEQUENTIAL_PROGRESSION) {
+        if (!\CourseConfig::get($this->range_id)->COURSEWARE_SEQUENTIAL_PROGRESSION) {
             return true;
         }
 
@@ -319,28 +380,48 @@ class StructuralElement extends \SimpleORMap
      */
     private function previousProgressAchieved($user): bool
     {
-        $achieved = true;
-        $root = $this->getCourseware($this->range_id, $this->range_type);
-        $courseware = array_merge([$root], $root->findDescendants());
-        foreach ($courseware as $element) {
+        $elements = $this->findCoursewareElements($user);
+
+        foreach ($elements as $element) {
+            // found me in depth-first order
+            // so everything before me was fine and we're done
             if ($element->id == $this->id) {
                 break;
             }
-            foreach ($element->containers as $container) {
-                foreach ($container->blocks as $block) {
-                    /** @var ?UserProgress $progress */
-                    $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [
-                        $user->id,
-                        $block->id,
-                    ]);
-                    if (1 != $progress->grade) {
-                        $achieved = false;
-                    }
+
+            if (!$element->hasBeenAchieved($user)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private function findCoursewareElements($user): array
+    {
+        $root = $this->getCourseware($this->range_id, $this->range_type);
+        $elements = array_merge([$root], $root->findDescendants($user));
+
+        return $elements;
+    }
+
+    private function hasBeenAchieved($user): bool
+    {
+        foreach ($this->containers as $container) {
+            foreach ($container->blocks as $block) {
+                /** @var ?UserProgress $progress */
+                $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [
+                    $user->id,
+                    $block->id,
+                ]);
+
+                if (!$progress || $progress->grade != 1) {
+                    return false;
                 }
             }
         }
 
-        return $achieved;
+        return true;
     }
 
     /**
@@ -389,16 +470,20 @@ class StructuralElement extends \SimpleORMap
     }
 
     /**
-     * Returns the list of all descendants of this instance.
+     * Returns the list of all descendants of this instance in depth-first search order.
+     *
+     * @param ?User  $user  the user whose bookmarked structural elements will be returned
      *
-     * @return array a list of all descendants
+     * @return StructuralElement[] a list of all descendants
      */
-    public function findDescendants(): array
+    public function findDescendants(User $user = null)
     {
         $descendants = [];
         foreach ($this->children as $child) {
-            $descendants[] = $child;
-            $descendants = array_merge($descendants, $child->findDescendants());
+            if ($user === null || $child->canRead($user)) {
+                $descendants[] = $child;
+                $descendants = array_merge($descendants, $child->findDescendants($user));
+            }
         }
 
         return $descendants;
diff --git a/resources/vue/components/courseware/CoursewareManagerElement.vue b/resources/vue/components/courseware/CoursewareManagerElement.vue
index 2ab5f17b72c71a50392737c74ce4895d28cf1ff2..14a22822bf8c5830521c068ea324e89c58900195 100755
--- a/resources/vue/components/courseware/CoursewareManagerElement.vue
+++ b/resources/vue/components/courseware/CoursewareManagerElement.vue
@@ -21,9 +21,8 @@
                 </header>
             </div>
             <courseware-collapsible-box
-                v-if="canRead"
-                :open="true" 
-                :title="$gettext('Abschnitt')" 
+                :open="true"
+                :title="$gettext('Abschnitt')"
                 class="cw-manager-element-containers"
             >
                 <div v-if="canSortContainers">
@@ -161,8 +160,9 @@ export default {
     },
     computed: {
         ...mapGetters({
-            structuralElementById: 'courseware-structural-elements/byId',
+            childrenById: 'courseware-structure/children',
             containerById: 'courseware-containers/byId',
+            structuralElementById: 'courseware-structural-elements/byId',
         }),
         isCurrent() {
             return this.type === 'current';
@@ -189,27 +189,33 @@ export default {
                 return false;
             }
         },
-        canRead() {
-            if (this.currentElement.attributes) {
-                return this.currentElement.attributes['can-read'];
-            } else {
-                return false;
-            }
-        },
         breadcrumb() {
-            if(this.currentElement.relationships) {
-                let view = this;
-                let ancestors = this.currentElement.relationships.ancestors.data;
-                let ancestorElements = [];
-                if(ancestors) {
-                    ancestors.forEach((element) => {
-                        ancestorElements.push(view.structuralElementById({ id: element.id }));
-                    });
-                }
-                return ancestorElements;
-            } else {
+            if (!this.currentElement) {
                 return [];
             }
+
+            const finder = (parent) => {
+                const parentId = parent.relationships?.parent?.data?.id;
+                if (!parentId) {
+                    return null;
+                }
+                const element = this.structuralElementById({id: parentId});
+                if (!element) {
+                    console.error("CoursewareManagerElement#breadcrumb: Could not find parent by ID.");
+                }
+
+                return element;
+            };
+
+            const visitAncestors = function* (node) {
+                const parent = finder(node);
+                if (parent) {
+                    yield parent;
+                    yield *visitAncestors(parent);
+                }
+            };
+
+            return [...visitAncestors(this.currentElement)].reverse()
         },
         elementTitle() {
             if (this.currentElement.attributes) {
@@ -265,19 +271,9 @@ export default {
                 return [];
             }
 
-            if(this.currentElement.relationships) {
-                let view = this;
-                let children = this.currentElement.relationships.children.data;
-                let childElements = [];
-                children.forEach((element) => {
-                    childElements.push(view.structuralElementById({ id: element.id }));
-                });
-
-                return childElements;
-            } else {
-                return [];
-            }
-
+            return this.childrenById(this.currentElement.id)
+                .map((id) => this.structuralElementById({ id }))
+                .filter(Boolean);
         },
         filingData() {
             return this.$store.getters.filingData;
@@ -313,7 +309,7 @@ export default {
                 let element = data.element;
                 if (source === 'self') {
                     element.relationships.parent.data.id = this.filingData.parentItem.id;
-                    element.attributes.position = this.filingData.parentItem.relationships.children.data.length;
+                    element.attributes.position = this.childrenById(this.filingData.parentItem.id).length;
                     await this.lockObject({ id: element.id, type: 'courseware-structural-elements' });
                     await this.updateStructuralElement({
                         element: element,
diff --git a/resources/vue/components/courseware/CoursewareRibbon.vue b/resources/vue/components/courseware/CoursewareRibbon.vue
index 2ae9e188ce1c660bbe53b0054cf66401fe484303..ce4092e29c0f462296f9d2c831b57ed0c8b79777 100755
--- a/resources/vue/components/courseware/CoursewareRibbon.vue
+++ b/resources/vue/components/courseware/CoursewareRibbon.vue
@@ -25,7 +25,7 @@
             </div>
             <div v-if="consumeMode" class="cw-ribbon-consume-bottom"></div>
             <courseware-ribbon-toolbar
-                v-show="showTools"
+                v-if="showTools"
                 :toolsActive="unfold"
                 :class="{ 'cw-ribbon-tools-sticky': stickyRibbon }"
                 :canEdit="canEdit"
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
index 9baecd4695e596eb894cc2d0edd12bce4421acb3..3c0f7ec5149ff138a46f009a60ec1441c57ccf30 100755
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -62,7 +62,7 @@
                 </courseware-ribbon>
 
                 <div
-                    v-if="canRead"
+                    v-if="canVisit"
                     class="cw-container-wrapper"
                     :class="{ 'cw-container-wrapper-consume': consumeMode }"
                 >
@@ -488,7 +488,7 @@ export default {
         IsoDate,
         StudipDialog,
     },
-    props: ['orderedStructuralElements', 'structuralElement'],
+    props: ['canVisit', 'orderedStructuralElements', 'structuralElement'],
 
     mixins: [CoursewareExport],
 
@@ -627,7 +627,28 @@ export default {
                 return [];
             }
 
-            return this.relatedStructuralElements({ parent: this.structuralElement, relationship: 'ancestors' });
+            const finder = (parent) => {
+                const parentId = parent.relationships?.parent?.data?.id;
+                if (!parentId) {
+                    return null;
+                }
+                const element = this.structuralElementById({id: parentId});
+                if (!element) {
+                    console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
+                }
+
+                return element;
+            };
+
+            const visitAncestors = function* (node) {
+                const parent = finder(node);
+                if (parent) {
+                    yield parent;
+                    yield *visitAncestors(parent);
+                }
+            };
+
+            return [...visitAncestors(this.structuralElement)].reverse()
         },
         prevElement() {
             const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id);
@@ -683,12 +704,7 @@ export default {
             }
             return this.structuralElement.attributes['can-edit'];
         },
-        canRead() {
-            if (!this.structuralElement) {
-                return false;
-            }
-            return this.structuralElement.attributes['can-read'];
-        },
+
         isTeacher() {
             return this.userIsTeacher;
         },
diff --git a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
index f6b45265e0b134cc9fcf23c2b2f1004eeef08788..ce860c4c14cdd7c31e0775c4a9c43d3156bb2828 100755
--- a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
+++ b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
@@ -45,9 +45,9 @@
                                     <p>{{ child.attributes.payload.description }}</p>
                                 </div>
                                 <footer>
-                                    {{ child.relationships.children.data.length }}
-                                    <translate 
-                                        :translate-n="child.relationships.children.data.length" 
+                                    {{ countChildren }}
+                                    <translate
+                                        :translate-n="countChildren"
                                         translate-plural="Seiten"
                                     >
                                        Seite
@@ -101,23 +101,17 @@ export default {
     },
     computed: {
         ...mapGetters({
+            childrenById: 'courseware-structure/children',
             structuralElementById: 'courseware-structural-elements/byId',
         }),
         structuralElement() {
             return this.structuralElementById({ id: this.$route.params.id });
         },
         childElements() {
-            let view = this;
-            let children = this.structuralElement.relationships.children.data;
-            let childElements = [];
-            children.forEach((element) => {
-                let childElement = view.structuralElementById({ id: element.id });
-                if (childElement.attributes['can-read']) {
-                    childElements.push(childElement);
-                }
-            });
-
-            return childElements;
+            return this.childrenById(this.structuralElement.id).map((id) => this.structuralElementById({ id }));
+        },
+        countChildren() {
+            return this.childrenById(this.structuralElement.id).length;
         },
         title() {
             return this.block?.attributes?.payload?.title;
diff --git a/resources/vue/components/courseware/CoursewareToolsContents.vue b/resources/vue/components/courseware/CoursewareToolsContents.vue
index c75c13c59ca03d0bba06da952a90a5edb8056b10..406045eb4d54281edb16d2d9bba6a0efc0d7350e 100755
--- a/resources/vue/components/courseware/CoursewareToolsContents.vue
+++ b/resources/vue/components/courseware/CoursewareToolsContents.vue
@@ -1,6 +1,6 @@
 <template>
     <div class="cw-tools cw-tools-contents">
-        <courseware-tree :treeData="treeData" v-if="courseware.length" />
+        <courseware-tree v-if="structuralElements.length" />
     </div>
 </template>
 
@@ -16,59 +16,8 @@ export default {
 
     computed: {
         ...mapGetters({
-            courseware: 'courseware-structural-elements/all',
+            structuralElements: 'courseware-structural-elements/all',
         }),
-
-        currentElementId() {
-            return this.$route.params.id;
-        },
-
-        treeData() {
-            let treeData = {
-                name: 'Courseware',
-            };
-            if (this.courseware !== []) {
-                let children = this.loadChildren(null, this.courseware, 0);
-
-                if (children.length) {
-                    treeData.children = children;
-                }
-            }
-
-            if (treeData.children !== undefined && treeData.children.length) {
-                return treeData.children[0];
-            }
-
-            return treeData;
-        },
-    },
-
-    methods: {
-        loadChildren(parentId, data, depth) {
-            let children = [];
-
-            for (var i = 0; i < data.length; i++) {
-                if (data[i].relationships.parent.data?.id == parentId) {
-                    let new_childs = this.loadChildren(data[i].id, data, depth + 1);
-                    if (data[i].attributes['can-read']) {
-                        children.push({
-                            name: data[i].attributes.title,
-                            position: data[i].attributes.position,
-                            element_id: data[i].id,
-                            children: new_childs,
-                            depth: depth,
-                            current: this.currentElementId === data[i].id
-                        });
-                    }
-                }
-            }
-
-            children.sort((a, b) => {
-                return a.position > b.position ? 1 : b.position > a.position ? -1 : 0;
-            });
-
-            return children;
-        },
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareTree.vue b/resources/vue/components/courseware/CoursewareTree.vue
index 992f3eb7a2a73a4b283765635c134c47b00dfbd7..881c1a48df724642f61e3ce25bed91e20c3bdc89 100755
--- a/resources/vue/components/courseware/CoursewareTree.vue
+++ b/resources/vue/components/courseware/CoursewareTree.vue
@@ -1,18 +1,45 @@
 <template>
     <div class="cw-tree">
         <ul class="cw-tree-root-list">
-            <courseware-tree-item class="cw-tree-item" :item="treeData"></courseware-tree-item>
+            <courseware-tree-item
+                class="cw-tree-item"
+                :element="rootElement"
+                :currentElement="currentElement"
+            ></courseware-tree-item>
         </ul>
     </div>
 </template>
 
 <script>
 import CoursewareTreeItem from './CoursewareTreeItem.vue';
+import { mapGetters } from 'vuex';
+
 export default {
     components: { CoursewareTreeItem },
     name: 'courseware-tree',
-    props: {
-        treeData: Object,
+    computed: {
+        ...mapGetters({
+            courseware: 'courseware',
+            relatedStructuralElement: 'courseware-structural-elements/related',
+            structuralElementById: 'courseware-structural-elements/byId',
+        }),
+        currentElement() {
+            const id = this.$route?.params?.id;
+            if (!id) {
+                return null;
+            }
+
+            return this.structuralElementById({ id }) ?? null;
+        },
+
+        rootElement() {
+            const root = this.relatedStructuralElement({
+                parent: { id: this.courseware.id, type: this.courseware.type },
+                relationship: 'root',
+            });
+
+            return root;
+        },
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareTreeItem.vue b/resources/vue/components/courseware/CoursewareTreeItem.vue
index d980d7f50386ec267b81f88955feaf6a6826edfc..bf7f522d5b6a32ddaa8a8d7f2b447ac28d5716f1 100755
--- a/resources/vue/components/courseware/CoursewareTreeItem.vue
+++ b/resources/vue/components/courseware/CoursewareTreeItem.vue
@@ -1,19 +1,21 @@
 <template>
     <li>
-        <div :class="{'cw-tree-item-is-root': isRoot, 'cw-tree-item-first-level': isFirstLevel}">
+        <div :class="{ 'cw-tree-item-is-root': isRoot, 'cw-tree-item-first-level': isFirstLevel }">
             <router-link
-                :to="'/structural_element/' + item.element_id"
+                :to="'/structural_element/' + element.id"
                 class="cw-tree-item-link"
-                :class="{'cw-tree-item-link-current': item.current}"
+                :class="{ 'cw-tree-item-link-current': isCurrent }"
             >
-                {{ item.name }}
+                {{ element.attributes.title }}
             </router-link>
         </div>
-        <ul v-if="hasChildren" :class="{'cw-tree-chapter-list': isRoot}">
+        <ul v-if="hasChildren" :class="{ 'cw-tree-chapter-list': isRoot }">
             <courseware-tree-item
-                v-for="(child, index) in item.children"
-                :key="index"
-                :item="child"
+                v-for="child in children"
+                :key="child.id"
+                :element="child"
+                :currentElement="currentElement"
+                :depth="depth + 1"
                 class="cw-tree-item"
             />
         </ul>
@@ -21,20 +23,48 @@
 </template>
 
 <script>
+import { mapGetters } from 'vuex';
+
 export default {
     name: 'courseware-tree-item',
     props: {
-        item: Object,
+        element: {
+            type: Object,
+            required: true,
+        },
+        currentElement: {
+            type: Object,
+        },
+        depth: {
+            type: Number,
+            default: 0,
+        },
     },
     computed: {
+        ...mapGetters({
+            childrenById: 'courseware-structure/children',
+            structuralElementById: 'courseware-structural-elements/byId',
+        }),
+        children() {
+            if (!this.element) {
+                return [];
+            }
+
+            return this.childrenById(this.element.id)
+                .map((id) => this.structuralElementById({ id }))
+                .filter(Boolean);
+        },
         hasChildren() {
-            return this.item.children && this.item.children.length;
+            return this.childrenById(this.element.id).length;
         },
         isRoot() {
-            return this.item.depth === 0;
+            return this.depth === 0;
         },
         isFirstLevel() {
-            return this.item.depth === 1;
+            return this.depth === 1;
+        },
+        isCurrent() {
+            return this.element.id === this.currentElement?.id;
         },
     },
 };
diff --git a/resources/vue/components/courseware/IndexApp.vue b/resources/vue/components/courseware/IndexApp.vue
index e4b16cdb35c35637a869a8108391eaaf41276dcb..76fbc4b7150206ebbdaabcf7f437ac17830513de 100755
--- a/resources/vue/components/courseware/IndexApp.vue
+++ b/resources/vue/components/courseware/IndexApp.vue
@@ -1,6 +1,7 @@
 <template>
-    <div v-if="courseware">
+    <div v-if="structureLoadingState === 'done'">
         <courseware-structural-element
+            :canVisit="canVisit"
             :structural-element="selected"
             :ordered-structural-elements="orderedStructuralElements"
             @select="selectStructuralElement"
@@ -28,37 +29,45 @@ export default {
         CoursewareActionWidget,
     },
     data: () => ({
+        canVisit: null,
         selected: null,
-        orderedStructuralElements: [],
+        structureLoadingState: 'idle',
     }),
     computed: {
         ...mapGetters({
             courseware: 'courseware',
+            orderedStructuralElements: 'courseware-structure/ordered',
             relatedStructuralElement: 'courseware-structural-elements/related',
+            structuralElementLastMeta: 'courseware-structural-elements/lastMeta',
             structuralElements: 'courseware-structural-elements/all',
             structuralElementById: 'courseware-structural-elements/byId',
             userId: 'userId',
         }),
     },
     methods: {
-        ...mapActions([
-            'coursewareBlockAdder',
-            'loadCoursewareStructure',
-            'loadStructuralElement',
-            'loadTeacherStatus',
-        ]),
+        ...mapActions({
+            buildStructure: 'courseware-structure/build',
+            coursewareBlockAdder: 'coursewareBlockAdder',
+            invalidateStructureCache: 'courseware-structure/invalidateCache',
+            loadCoursewareStructure: 'courseware-structure/load',
+            loadStructuralElement: 'loadStructuralElement',
+            loadTeacherStatus: 'loadTeacherStatus',
+        }),
         async selectStructuralElement(id) {
             if (!id) {
                 return;
             }
 
             await this.loadStructuralElement(id);
+            this.canVisit = this.structuralElementLastMeta['can-visit'];
             this.selected = this.structuralElementById({ id });
         },
     },
     async mounted() {
+        this.structureLoadingState = 'loading';
         await this.loadCoursewareStructure();
         await this.loadTeacherStatus(this.userId);
+        this.structureLoadingState = 'done';
         const selectedId = this.$route.params?.id;
         await this.selectStructuralElement(selectedId);
     },
@@ -70,51 +79,13 @@ export default {
             const selectedId = to.params?.id;
             this.selectStructuralElement(selectedId);
         },
-        structuralElements(newElements, oldElements) {
-            const nodes = buildNodes(this.structuralElements, this.relatedStructuralElement.bind(this));
-            this.orderedStructuralElements = [...visitTree(nodes, findRoot(nodes))];
+        async structuralElements(newElements, oldElements) {
+            // compute order of structural elements once more
+            await this.buildStructure();
+
+            // throw away stale cache
+            this.invalidateStructureCache();
         },
     },
 };
-
-function buildNodes(structuralElements, relatedStructuralElement) {
-    return structuralElements.reduce((memo, element) => {
-        if (element.attributes['can-read']) {
-            memo.push({
-                id: element.id,
-                parent:
-                    relatedStructuralElement({
-                        parent: element,
-                        relationship: 'parent',
-                    })?.id ?? null,
-
-                children:
-                    relatedStructuralElement({
-                        parent: element,
-                        relationship: 'children',
-                    })?.map((child) => child.id) ?? [],
-            });
-        }
-
-        return memo;
-    }, []);
-}
-
-function findRoot(nodes) {
-    return nodes.find((node) => node.parent === null);
-}
-
-function findNode(nodes, id) {
-    return nodes.find((node) => node.id === id);
-}
-
-function* visitTree(nodes, current) {
-    if (current) {
-        yield current.id;
-
-        for (let index = 0; index < current.children.length; index++) {
-            yield* visitTree(nodes, findNode(nodes, current.children[index]));
-        }
-    }
-}
 </script>
diff --git a/resources/vue/components/courseware/ManagerApp.vue b/resources/vue/components/courseware/ManagerApp.vue
index 0369522cab6faeedff76d1adc176b253fc52cf71..57d71e587b95ecd9fbdf10bba32949dacfdc8ed5 100755
--- a/resources/vue/components/courseware/ManagerApp.vue
+++ b/resources/vue/components/courseware/ManagerApp.vue
@@ -9,13 +9,29 @@ import { mapActions, mapGetters } from 'vuex';
 export default {
     components: { CoursewareCourseManager },
     computed: {
-        ...mapGetters(['courseware']),
+        ...mapGetters({
+            courseware: 'courseware',
+            structuralElements: 'courseware-structural-elements/all',
+        }),
     },
     methods: {
-        ...mapActions(['loadCoursewareStructure']),
+        ...mapActions({
+            buildStructure: 'courseware-structure/build',
+            invalidateStructureCache: 'courseware-structure/invalidateCache',
+            loadCoursewareStructure: 'courseware-structure/load',
+        }),
     },
     async mounted() {
         await this.loadCoursewareStructure();
     },
+    watch: {
+        async structuralElements(newElements, oldElements) {
+            // compute order of structural elements once more
+            await this.buildStructure();
+
+            // throw away stale cache
+            this.invalidateStructureCache();
+        },
+    },
 };
 </script>
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index 2594e66ec2338f285b94d3fe5f1339a52fce7e14..e4552a1f0bb0614755f79cef6ae1e69545f479c8 100755
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -1,4 +1,5 @@
 import CoursewareModule from './store/courseware/courseware.module';
+import CoursewareStructureModule from './store/courseware/structure.module';
 import CoursewareStructuralElement from './components/courseware/CoursewareStructuralElement.vue';
 import IndexApp from './components/courseware/IndexApp.vue';
 import PluginManager from './components/courseware/plugin-manager.js';
@@ -81,6 +82,7 @@ const mountApp = (STUDIP, createApp, element) => {
     const store = new Vuex.Store({
         modules: {
             courseware: CoursewareModule,
+            'courseware-structure': CoursewareStructureModule,
             ...mapResourceModules({
                 names: [
                     'courses',
diff --git a/resources/vue/courseware-manager-app.js b/resources/vue/courseware-manager-app.js
index fc6f98afadd8256302678f529131971fbe50ef7e..fc700a2264455c023e486519755582bc35f42f8a 100755
--- a/resources/vue/courseware-manager-app.js
+++ b/resources/vue/courseware-manager-app.js
@@ -1,4 +1,5 @@
 import CoursewareModule from './store/courseware/courseware.module';
+import CoursewareStructureModule from './store/courseware/structure.module';
 import ManagerApp from './components/courseware/ManagerApp.vue';
 import Vuex from 'vuex';
 import axios from 'axios';
@@ -19,6 +20,7 @@ const mountApp = (STUDIP, createApp, element) => {
     const store = new Vuex.Store({
         modules: {
             courseware: CoursewareModule,
+            'courseware-structure': CoursewareStructureModule,
             ...mapResourceModules({
                 names: [
                     'courses',
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 3061066b639e1b51a822931254546cb786a0c7d5..2107113d16a689a20eae26fde4f3021c90bd9824 100755
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -169,18 +169,6 @@ const getters = {
 export const state = { ...initialState };
 
 export const actions = {
-    async loadCoursewareStructure({ commit, dispatch, state, rootGetters }) {
-        const parent = state.context;
-        const relationship = 'courseware';
-        const options = {
-            include: 'bookmarks,root,root.descendants',
-        };
-
-        await dispatch(`courseware-instances/loadRelated`, { parent, relationship, options }, { root: true });
-
-        return commit('coursewareSet', rootGetters['courseware-instances/all'][0]);
-    },
-
     loadContainer({ dispatch }, containerId) {
         const options = {
             include: 'blocks',
@@ -192,7 +180,7 @@ export const actions = {
     loadStructuralElement({ dispatch }, structuralElementId) {
         const options = {
             include:
-                'ancestors,containers,containers.blocks,containers.blocks.editor,containers.blocks.owner,containers.blocks.user-data-field,containers.blocks.user-progress,descendants,editor,owner',
+                'containers,containers.blocks,containers.blocks.editor,containers.blocks.owner,containers.blocks.user-data-field,containers.blocks.user-progress,editor,owner',
             'fields[users]': 'formatted-name',
         };
 
diff --git a/resources/vue/store/courseware/structure.module.js b/resources/vue/store/courseware/structure.module.js
new file mode 100644
index 0000000000000000000000000000000000000000..fdfe762758541c276f2b71844a36ca8fd19992bf
--- /dev/null
+++ b/resources/vue/store/courseware/structure.module.js
@@ -0,0 +1,196 @@
+const getDefaultState = () => {
+    return {
+        children: [],
+        ordered: [],
+    };
+};
+
+const initialState = getDefaultState();
+const state = { ...initialState };
+
+const getters = {
+    children(state) {
+        return (id) => state.children[id] ?? [];
+    },
+    ordered(state) {
+        return state.ordered;
+    },
+};
+
+export const mutations = {
+    reset(state) {
+        state = getDefaultState();
+    },
+    setChildren(state, children) {
+        state.children = children;
+    },
+    setOrdered(state, ordered) {
+        state.ordered = ordered;
+    },
+};
+
+const actions = {
+    build({ commit, rootGetters }) {
+        const structuralElements = rootGetters['courseware-structural-elements/all'];
+        const root = findRoot(structuralElements);
+        if (!root) {
+            commit('reset');
+
+            return;
+        }
+
+        const children = structuralElements.reduce((memo, element) => {
+            const parent = element.relationships.parent?.data?.id ?? null;
+            if (parent) {
+                if (!memo[parent]) {
+                    memo[parent] = [];
+                }
+                memo[parent].push([element.id, element.attributes.position]);
+            }
+
+            return memo;
+        }, {});
+        for (const key of Object.keys(children)) {
+            children[key].sort((childA, childB) => childA[1] - childB[1]);
+            children[key] = children[key].map(([id]) => id);
+        }
+        commit('setChildren', children);
+
+        const ordered = [...visitTree(children, root.id)];
+        commit('setOrdered', ordered);
+    },
+
+    invalidateCache({ rootGetters }) {
+        const courseware = rootGetters['courseware'];
+        if (!courseware) {
+            return;
+        }
+        const element = rootGetters['courseware-structural-elements/related']({
+            parent: { id: courseware.id, type: courseware.type },
+            relationship: 'root',
+        });
+        if (!element) {
+            return;
+        }
+        const cache = window.STUDIP.Cache.getInstance('courseware');
+        const cacheKey = `descendants/${element.id}/${rootGetters['userId']}`;
+        try {
+            cache.remove(cacheKey);
+        } catch (e) {
+            // nothing we can do
+        }
+    },
+
+    async load({ commit, dispatch, rootGetters }) {
+        const parent = rootGetters['context'];
+        const relationship = 'courseware';
+        const options = {
+            include: 'bookmarks,root',
+        };
+
+        // get courseware instance
+        await dispatch(`courseware-instances/loadRelated`, { parent, relationship, options }, { root: true });
+        const courseware = rootGetters['courseware-instances/all'][0];
+        commit('coursewareSet', courseware, { root: true });
+
+        // load descendants
+        dispatch('fetchDescendants');
+    },
+
+    async fetchDescendants({ dispatch, rootGetters, commit }) {
+        // get root of that instance
+        const courseware = rootGetters['courseware'];
+        if (!courseware) {
+            return;
+        }
+        const rootElement = rootGetters['courseware-structural-elements/related']({
+            parent: { id: courseware.id, type: courseware.type },
+            relationship: 'root',
+        });
+        if (!rootElement) {
+            return;
+        }
+
+        const cache = window.STUDIP.Cache.getInstance('courseware');
+        const cacheKey = `descendants/${rootElement.id}/${rootGetters['userId']}`;
+
+        await unpickleDescendants();
+        revalidateDescendants();
+
+        function unpickleDescendants() {
+            try {
+                const descendants = cache.get(cacheKey);
+                const cacheHit = descendants !== undefined;
+                if (cacheHit) {
+                    commit('courseware-structural-elements/REPLACE_ALL_RECORDS', descendants, { root: true });
+                }
+            } catch (e) {
+                return;
+            }
+        }
+
+        function revalidateDescendants() {
+            return loadDescendants().then(removeStaleElements).then(pickleDescendants);
+        }
+
+        function loadDescendants() {
+            const parent = { id: rootElement.id, type: rootElement.type };
+            const relationship = 'descendants';
+            const options = {
+                'page[offset]': 0,
+                'page[limit]': 10000,
+            };
+
+            return dispatch(
+                'courseware-structural-elements/loadRelated',
+                { parent, relationship, options },
+                { root: true }
+            );
+        }
+
+        function pickleDescendants() {
+            try {
+                cache.set(cacheKey, rootGetters['courseware-structural-elements/all']);
+            } catch (e) {}
+        }
+
+        function removeStaleElements() {
+            const idsToKeep = [
+                rootElement.id,
+                ...rootGetters['courseware-structural-elements/related']({
+                    parent: rootElement,
+                    relationship: 'descendants',
+                }).map(({ id }) => id),
+            ];
+            rootGetters['courseware-structural-elements/all']
+                .map(({ id }) => id)
+                .filter((id) => !idsToKeep.includes(id))
+                .forEach((id) => commit('courseware-structural-elements/REMOVE_RECORD', { id }, { root: true }));
+        }
+    },
+};
+
+function findRoot(nodes) {
+    return nodes.find((node) => !node.relationships.parent?.data);
+}
+
+function* visitTree(tree, current) {
+    if (current) {
+        yield current;
+
+        const children = tree[current];
+        if (children) {
+            for (let index = 0; index < children.length; index++) {
+                yield* visitTree(tree, children[index]);
+            }
+        }
+    }
+}
+
+export default {
+    namespaced: true,
+    actions,
+    getters,
+    mutations,
+    state,
+};