diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py
index 27e9d89bb44ed26ce2dc69161b65c03772854fe0..ec7373edc004b670bab6465405d9ade33ab3549d 100644
--- a/lms/djangoapps/ccx/tests/test_views.py
+++ b/lms/djangoapps/ccx/tests/test_views.py
@@ -1156,9 +1156,7 @@ class CCXCoachTabTestCase(CcxTestCase):
 
     def check_ccx_tab(self, course, user):
         """Helper function for verifying the ccx tab."""
-        request = RequestFactory().request()
-        request.user = user
-        all_tabs = get_course_tab_list(request, course)
+        all_tabs = get_course_tab_list(user, course)
         return any(tab.type == 'ccx_coach' for tab in all_tabs)
 
     @ddt.data(
diff --git a/lms/djangoapps/course_api/api.py b/lms/djangoapps/course_api/api.py
index 37406e2033eaf9b1b4a376b3212164cbfc3acaad..f86f6f5e0a1384049f2975ae8626637df151a451 100644
--- a/lms/djangoapps/course_api/api.py
+++ b/lms/djangoapps/course_api/api.py
@@ -65,11 +65,13 @@ def course_detail(request, username, course_key):
         `CourseOverview` object representing the requested course
     """
     user = get_effective_user(request.user, username)
-    return get_course_overview_with_access(
+    overview = get_course_overview_with_access(
         user,
         get_permission_for_course_about(),
         course_key,
     )
+    overview.effective_user = user
+    return overview
 
 
 def _filter_by_role(course_queryset, user, roles):
diff --git a/lms/djangoapps/course_api/views.py b/lms/djangoapps/course_api/views.py
index 762727dd02986b6244eb328236ea28a357512864..7320fea86e862b52caf4728e38193f92db800a74 100644
--- a/lms/djangoapps/course_api/views.py
+++ b/lms/djangoapps/course_api/views.py
@@ -5,6 +5,7 @@ Course API Views
 
 from django.core.exceptions import ValidationError
 from edx_django_utils.monitoring import set_custom_metric
+
 from edx_rest_framework_extensions.paginators import NamespacedPageNumberPagination
 from rest_framework.generics import ListAPIView, RetrieveAPIView
 from rest_framework.throttling import UserRateThrottle
diff --git a/lms/djangoapps/course_wiki/tests/test_tab.py b/lms/djangoapps/course_wiki/tests/test_tab.py
index 1ece82e07e8b2561aa37cad3437dc47c9818fcc3..4fb9b12e29f429cfa99e144c71dd7ab5552d9839 100644
--- a/lms/djangoapps/course_wiki/tests/test_tab.py
+++ b/lms/djangoapps/course_wiki/tests/test_tab.py
@@ -24,8 +24,7 @@ class WikiTabTestCase(ModuleStoreTestCase):
     def get_wiki_tab(self, user, course):
         """Returns true if the "Wiki" tab is shown."""
         request = RequestFactory().request()
-        request.user = user
-        all_tabs = get_course_tab_list(request, course)
+        all_tabs = get_course_tab_list(user, course)
         wiki_tabs = [tab for tab in all_tabs if tab.name == 'Wiki']
         return wiki_tabs[0] if len(wiki_tabs) == 1 else None
 
diff --git a/lms/djangoapps/courseware/entrance_exams.py b/lms/djangoapps/courseware/entrance_exams.py
index 381ab1708d06bdcb6a9e5d1d841079ae6b352959..0f87090517a7a0a2b6d86cf4579cd5923be7848a 100644
--- a/lms/djangoapps/courseware/entrance_exams.py
+++ b/lms/djangoapps/courseware/entrance_exams.py
@@ -17,7 +17,8 @@ def course_has_entrance_exam(course):
     """
     if not is_entrance_exams_enabled():
         return False
-    if not course.entrance_exam_enabled:
+    entrance_exam_enabled = getattr(course, 'entrance_exam_enabled', None)
+    if not entrance_exam_enabled:
         return False
     if not course.entrance_exam_id:
         return False
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index 4d8704642a9e3d7b2bab2c9747305239feeaf303..0d075e53256d10825c174c6d408fa36ed70859bd 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -307,11 +307,10 @@ class SingleTextbookTab(CourseTab):
         raise NotImplementedError('SingleTextbookTab should not be serialized.')
 
 
-def get_course_tab_list(request, course):
+def get_course_tab_list(user, course):
     """
     Retrieves the course tab list from xmodule.tabs and manipulates the set as necessary
     """
-    user = request.user
     xmodule_tab_list = CourseTabList.iterate_displayable(course, user=user)
 
     # Now that we've loaded the tabs for this course, perform the Entrance Exam work.
diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py
index 16e28c820bc58403863c1269b0ac89293cb4068e..0516ca00cc1b75030fff2a44d6e828744e4dab65 100644
--- a/lms/djangoapps/courseware/tests/test_tabs.py
+++ b/lms/djangoapps/courseware/tests/test_tabs.py
@@ -385,7 +385,6 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, Mi
             'description': 'Testing Courseware Tabs'
         }
         self.user.is_staff = False
-        request = get_mock_request(self.user)
         self.course.entrance_exam_enabled = True
         self.course.entrance_exam_id = six.text_type(entrance_exam.location)
         milestone = add_milestone(milestone)
@@ -400,7 +399,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, Mi
             self.relationship_types['FULFILLS'],
             milestone
         )
-        course_tab_list = get_course_tab_list(request, self.course)
+        course_tab_list = get_course_tab_list(self.user, self.course)
         self.assertEqual(len(course_tab_list), 1)
         self.assertEqual(course_tab_list[0]['tab_id'], 'courseware')
         self.assertEqual(course_tab_list[0]['name'], 'Entrance Exam')
@@ -425,8 +424,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, Mi
         # log in again as student
         self.client.logout()
         self.login(self.email, self.password)
-        request = get_mock_request(self.user)
-        course_tab_list = get_course_tab_list(request, self.course)
+        course_tab_list = get_course_tab_list(self.user, self.course)
         self.assertEqual(len(course_tab_list), 4)
 
     def test_course_tabs_list_for_staff_members(self):
@@ -438,8 +436,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, Mi
         self.client.logout()
         staff_user = StaffFactory(course_key=self.course.id)
         self.client.login(username=staff_user.username, password='test')
-        request = get_mock_request(staff_user)
-        course_tab_list = get_course_tab_list(request, self.course)
+        course_tab_list = get_course_tab_list(staff_user, self.course)
         self.assertEqual(len(course_tab_list), 4)
 
 
@@ -480,8 +477,7 @@ class TextBookCourseViewsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
         """
         type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}
         self.addCleanup(set_current_request, None)
-        request = get_mock_request(self.user)
-        course_tab_list = get_course_tab_list(request, self.course)
+        course_tab_list = get_course_tab_list(self.user, self.course)
         num_of_textbooks_found = 0
         for tab in course_tab_list:
             # Verify links of all textbook type tabs.
@@ -706,8 +702,7 @@ class CourseTabListTestCase(TabListTestCase):
 
         user = self.create_mock_user(is_staff=False, is_enrolled=True)
         self.addCleanup(set_current_request, None)
-        request = get_mock_request(user)
-        course_tab_list = get_course_tab_list(request, self.course)
+        course_tab_list = get_course_tab_list(user, self.course)
         name_list = [x.name for x in course_tab_list]
         self.assertIn('Static Tab Free', name_list)
         self.assertNotIn('Static Tab Instructors Only', name_list)
@@ -716,8 +711,7 @@ class CourseTabListTestCase(TabListTestCase):
         self.client.logout()
         staff_user = StaffFactory(course_key=self.course.id)
         self.client.login(username=staff_user.username, password='test')
-        request = get_mock_request(staff_user)
-        course_tab_list_staff = get_course_tab_list(request, self.course)
+        course_tab_list_staff = get_course_tab_list(staff_user, self.course)
         name_list_staff = [x.name for x in course_tab_list_staff]
         self.assertIn('Static Tab Free', name_list_staff)
         self.assertIn('Static Tab Instructors Only', name_list_staff)
@@ -775,18 +769,17 @@ class CourseInfoTabTestCase(TabTestCase):
     def setUp(self):
         self.user = self.create_mock_user()
         self.addCleanup(set_current_request, None)
-        self.request = get_mock_request(self.user)
 
     @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=False)
     def test_default_tab(self):
         # Verify that the course info tab is the first tab
-        tabs = get_course_tab_list(self.request, self.course)
+        tabs = get_course_tab_list(self.user, self.course)
         self.assertEqual(tabs[0].type, 'course_info')
 
     @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
     def test_default_tab_for_new_course_experience(self):
         # Verify that the unified course experience hides the course info tab
-        tabs = get_course_tab_list(self.request, self.course)
+        tabs = get_course_tab_list(self.user, self.course)
         self.assertEqual(tabs[0].type, 'courseware')
 
     # TODO: LEARNER-611 - remove once course_info is removed.
diff --git a/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py b/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py
index 960f7997ffe865b51a5e6c9c8cf502d5cbd1781f..762e5d070a18cbc96818aa1abf92f7872f64b5d9 100644
--- a/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py
+++ b/lms/djangoapps/discussion/django_comment_client/tests/test_utils.py
@@ -1248,8 +1248,7 @@ class DiscussionTabTestCase(ModuleStoreTestCase):
     def discussion_tab_present(self, user):
         """ Returns true if the user has access to the discussion tab. """
         request = RequestFactory().request()
-        request.user = user
-        all_tabs = get_course_tab_list(request, self.course)
+        all_tabs = get_course_tab_list(user, self.course)
         return any(tab.type == 'discussion' for tab in all_tabs)
 
     def test_tab_access(self):
diff --git a/lms/djangoapps/edxnotes/tests.py b/lms/djangoapps/edxnotes/tests.py
index de11662ddde81bf88efe489095799bdf172a0c5e..8fce8bc8d767ec084b3f47b0a20618233bf55abf 100644
--- a/lms/djangoapps/edxnotes/tests.py
+++ b/lms/djangoapps/edxnotes/tests.py
@@ -1003,9 +1003,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
         """
         def has_notes_tab(user, course):
             """Returns true if the "Notes" tab is shown."""
-            request = RequestFactory().request()
-            request.user = user
-            tabs = get_course_tab_list(request, course)
+            tabs = get_course_tab_list(user, course)
             return len([tab for tab in tabs if tab.type == 'edxnotes']) == 1
 
         self.assertFalse(has_notes_tab(self.user, self.course))
diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
index f3ee5954e599642936c9e714b3905ee46530b242..ecf76df5ee3be8dc00b1c72908908737ebe0207c 100644
--- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
+++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
@@ -107,9 +107,7 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT
         """
         def has_instructor_tab(user, course):
             """Returns true if the "Instructor" tab is shown."""
-            request = RequestFactory().request()
-            request.user = user
-            tabs = get_course_tab_list(request, course)
+            tabs = get_course_tab_list(user, course)
             return len([tab for tab in tabs if tab.name == 'Instructor']) == 1
 
         self.assertTrue(has_instructor_tab(self.instructor, self.course))
diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html
index 5946d7319b3c6c800910670e5b33a504d2de9c94..16257629c7d61636fb1859750431a25c3611b60c 100644
--- a/lms/templates/courseware/course_navigation.html
+++ b/lms/templates/courseware/course_navigation.html
@@ -35,7 +35,7 @@ if course is not None:
 
 % if disable_tabs is UNDEFINED or not disable_tabs:
     <%
-    tab_list = get_course_tab_list(request, course)
+    tab_list = get_course_tab_list(request.user, course)
     %>
     % if uses_bootstrap:
         <nav class="navbar course-tabs pb-0 navbar-expand" aria-label="${_('Course')}">
diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0019_improve_courseoverviewtab.py b/openedx/core/djangoapps/content/course_overviews/migrations/0019_improve_courseoverviewtab.py
new file mode 100644
index 0000000000000000000000000000000000000000..70b729cc15af3b4dda9ab51f9401bfaaf0ff3921
--- /dev/null
+++ b/openedx/core/djangoapps/content/course_overviews/migrations/0019_improve_courseoverviewtab.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2020-01-16 17:23
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('course_overviews', '0018_add_start_end_in_CourseOverview'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='courseoverviewtab',
+            name='course_staff_only',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='courseoverviewtab',
+            name='name',
+            field=models.TextField(null=True),
+        ),
+        migrations.AddField(
+            model_name='courseoverviewtab',
+            name='type',
+            field=models.CharField(max_length=50, null=True),
+        ),
+        migrations.AlterField(
+            model_name='courseoverviewtab',
+            name='course_overview',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tab_set', to='course_overviews.CourseOverview'),
+        ),
+    ]
diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py
index a621056a0593aedb6510d5daee0780f5468ac82c..0453dd539ccde586a5bcf420248b3d4a4854e147 100644
--- a/openedx/core/djangoapps/content/course_overviews/models.py
+++ b/openedx/core/djangoapps/content/course_overviews/models.py
@@ -16,6 +16,7 @@ from django.db.models.fields import BooleanField, DateTimeField, DecimalField, F
 from django.db.utils import IntegrityError
 from django.template import defaultfilters
 from django.utils.encoding import python_2_unicode_compatible
+from django.utils.functional import cached_property
 from model_utils.models import TimeStampedModel
 from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
 from six import text_type  # pylint: disable=ungrouped-imports
@@ -31,6 +32,8 @@ from xmodule import block_metadata_utils, course_metadata_utils
 from xmodule.course_module import DEFAULT_START_DATE, CourseDescriptor
 from xmodule.error_module import ErrorDescriptor
 from xmodule.modulestore.django import modulestore
+from xmodule.tabs import CourseTab
+
 
 log = logging.getLogger(__name__)
 
@@ -54,7 +57,7 @@ class CourseOverview(TimeStampedModel):
         app_label = 'course_overviews'
 
     # IMPORTANT: Bump this whenever you modify this model and/or add a migration.
-    VERSION = 7
+    VERSION = 8
 
     # Cache entry versioning.
     version = IntegerField()
@@ -247,7 +250,12 @@ class CourseOverview(TimeStampedModel):
                         # Remove and recreate all the course tabs
                         CourseOverviewTab.objects.filter(course_overview=course_overview).delete()
                         CourseOverviewTab.objects.bulk_create([
-                            CourseOverviewTab(tab_id=tab.tab_id, course_overview=course_overview)
+                            CourseOverviewTab(
+                                tab_id=tab.tab_id,
+                                type=tab.type,
+                                name=tab.name,
+                                course_staff_only=tab.course_staff_only,
+                                course_overview=course_overview)
                             for tab in course.tabs
                         ])
                         # Remove and recreate course images
@@ -629,13 +637,25 @@ class CourseOverview(TimeStampedModel):
         """
         Returns True if course has discussion tab and is enabled
         """
-        tabs = self.tabs.all()
+        tabs = self.tab_set.all()
         # creates circular import; hence explicitly referenced is_discussion_enabled
         for tab in tabs:
             if tab.tab_id == "discussion" and django_comment_client.utils.is_discussion_enabled(self.id):
                 return True
         return False
 
+    @property
+    def tabs(self):
+        """
+        Returns an iterator of CourseTabs.
+        """
+        for tab_dict in self.tab_set.all().values():
+            tab = CourseTab.from_json(tab_dict)
+            if tab is None:
+                log.warning("Can't instantiate CourseTab from %r", tab_dict)
+            else:
+                yield tab
+
     @property
     def image_urls(self):
         """
@@ -733,6 +753,49 @@ class CourseOverview(TimeStampedModel):
 
         return urlunparse(('', base_url, path, params, query, fragment))
 
+    @cached_property
+    def _original_course(self):
+        """
+        Returns the course from the modulestore.
+        """
+        log.warning('Falling back on modulestore to get course information for %s', self.id)
+        return modulestore().get_course(self.id)
+
+    @property
+    def allow_public_wiki_access(self):
+        """
+        TODO: move this to the model.
+        """
+        return self._original_course.allow_public_wiki_access
+
+    @property
+    def textbooks(self):
+        """
+        TODO: move this to the model.
+        """
+        return self._original_course.textbooks
+
+    @property
+    def hide_progress_tab(self):
+        """
+        TODO: move this to the model.
+        """
+        return self._original_course.hide_progress_tab
+
+    @property
+    def edxnotes(self):
+        """
+        TODO: move this to the model.
+        """
+        return self._original_course.edxnotes
+
+    @property
+    def enable_ccx(self):
+        """
+        TODO: move this to the model.
+        """
+        return self._original_course.enable_ccx
+
     def __str__(self):
         """Represent ourselves with the course key."""
         return six.text_type(self.id)
@@ -745,7 +808,13 @@ class CourseOverviewTab(models.Model):
     .. no_pii:
     """
     tab_id = models.CharField(max_length=50)
-    course_overview = models.ForeignKey(CourseOverview, db_index=True, related_name="tabs", on_delete=models.CASCADE)
+    course_overview = models.ForeignKey(CourseOverview, db_index=True, related_name="tab_set", on_delete=models.CASCADE)
+    type = models.CharField(max_length=50, null=True)
+    name = models.TextField(null=True)
+    course_staff_only = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.tab_id
 
 
 @python_2_unicode_compatible
diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
index 3b11a36be7f905d3332a5405648940a7ae93ff34..05bb64cb2243ab876af15f0fb6e46061c2f3f27e 100644
--- a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
+++ b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
@@ -202,7 +202,7 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
 
         # test tabs for both cached miss and cached hit courses
         for course_overview in [course_overview_cache_miss, course_overview_cache_hit]:
-            course_overview_tabs = course_overview.tabs.all()
+            course_overview_tabs = course_overview.tab_set.all()
             course_resp_tabs = {tab.tab_id for tab in course_overview_tabs}
             self.assertEqual(self.COURSE_OVERVIEW_TABS, course_resp_tabs)
 
@@ -1089,7 +1089,7 @@ class CourseOverviewTabTestCase(ModuleStoreTestCase):
         """
         course = CourseFactory.create(default_store=modulestore_type)
         course_overview = CourseOverview.get_from_id(course.id)
-        expected_tabs = {tab.tab_id for tab in course_overview.tabs.all()}
+        expected_tabs = {tab.tab_id for tab in course_overview.tab_set.all()}
 
         with mock.patch(
             'openedx.core.djangoapps.content.course_overviews.models.CourseOverviewTab.objects.bulk_create'
@@ -1105,6 +1105,6 @@ class CourseOverviewTabTestCase(ModuleStoreTestCase):
 
             # Asserts that the tabs deletion is properly rolled back to a save point and
             # the course overview is not updated.
-            actual_tabs = {tab.tab_id for tab in course_overview.tabs.all()}
+            actual_tabs = {tab.tab_id for tab in course_overview.tab_set.all()}
             self.assertEqual(actual_tabs, expected_tabs)
             self.assertNotEqual(course_overview.display_name, course.display_name)
diff --git a/openedx/core/djangoapps/courseware_api/__init__.py b/openedx/core/djangoapps/courseware_api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/openedx/core/djangoapps/courseware_api/apps.py b/openedx/core/djangoapps/courseware_api/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ad0dcebdc9055c205d1e43f5cd2bd69a1676021
--- /dev/null
+++ b/openedx/core/djangoapps/courseware_api/apps.py
@@ -0,0 +1,26 @@
+"""
+Courseware API Application Configuration
+
+Signal handlers are connected here.
+"""
+
+
+from django.apps import AppConfig
+
+from openedx.core.djangoapps.plugins.constants import PluginURLs, ProjectType
+
+
+class CoursewareAPIConfig(AppConfig):
+    """
+    AppConfig for courseware API app
+    """
+    name = 'openedx.core.djangoapps.courseware_api'
+    plugin_app = {
+        PluginURLs.CONFIG: {
+            ProjectType.LMS: {
+                PluginURLs.NAMESPACE: 'courseware_api',
+                PluginURLs.REGEX: 'api/courseware/',
+                PluginURLs.RELATIVE_PATH: 'urls',
+            }
+        },
+    }
diff --git a/openedx/core/djangoapps/courseware_api/docs/decisions/0001-record-architecture-decisions.rst b/openedx/core/djangoapps/courseware_api/docs/decisions/0001-record-architecture-decisions.rst
new file mode 100644
index 0000000000000000000000000000000000000000..0ab9ab44e6cf049643c536c99f09d0fcd12f7f59
--- /dev/null
+++ b/openedx/core/djangoapps/courseware_api/docs/decisions/0001-record-architecture-decisions.rst
@@ -0,0 +1,32 @@
+1. Record Architecture Decisions
+--------------------------------
+
+Status
+------
+
+Accepted
+
+Context
+-------
+
+We would like to keep a historical record on the architectural
+decisions we make with this app as it evolves over time.
+
+Decision
+--------
+
+We will use Architecture Decision Records, as described by 
+Michael Nygard in `Documenting Architecture Decisions`_
+
+.. _Documenting Architecture Decisions: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions
+
+Consequences
+------------
+
+See Michael Nygard's article, linked above.
+
+References
+----------
+
+* https://resources.sei.cmu.edu/asset_files/Presentation/2017_017_001_497746.pdf
+* https://github.com/npryce/adr-tools/tree/master/doc/adr
diff --git a/openedx/core/djangoapps/courseware_api/serializers.py b/openedx/core/djangoapps/courseware_api/serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea983641ab87609f2617484696911f1cab70e4ad
--- /dev/null
+++ b/openedx/core/djangoapps/courseware_api/serializers.py
@@ -0,0 +1,120 @@
+"""
+Course API Serializers.  Representing course catalog data
+"""
+
+from django.urls import reverse
+from rest_framework import serializers
+
+from lms.djangoapps.courseware.tabs import get_course_tab_list
+from openedx.core.lib.api.fields import AbsoluteURLField
+from student.models import CourseEnrollment
+
+
+class _MediaSerializer(serializers.Serializer):  # pylint: disable=abstract-method
+    """
+    Nested serializer to represent a media object.
+    """
+
+    def __init__(self, uri_attribute, *args, **kwargs):
+        super(_MediaSerializer, self).__init__(*args, **kwargs)
+        self.uri_attribute = uri_attribute
+
+    uri = serializers.SerializerMethodField(source='*')
+
+    class Meta:
+        ref_name = 'courseware_api'
+
+    def get_uri(self, course_overview):
+        """
+        Get the representation for the media resource's URI
+        """
+        return getattr(course_overview, self.uri_attribute)
+
+
+class ImageSerializer(serializers.Serializer):  # pylint: disable=abstract-method
+    """
+    Collection of URLs pointing to images of various sizes.
+
+    The URLs will be absolute URLs with the host set to the host of the current request. If the values to be
+    serialized are already absolute URLs, they will be unchanged.
+    """
+    raw = AbsoluteURLField()
+    small = AbsoluteURLField()
+    large = AbsoluteURLField()
+
+    class Meta:
+        ref_name = 'courseware_api'
+
+
+class _CourseApiMediaCollectionSerializer(serializers.Serializer):  # pylint: disable=abstract-method
+    """
+    Nested serializer to represent a collection of media objects
+    """
+    course_image = _MediaSerializer(source='*', uri_attribute='course_image_url')
+    course_video = _MediaSerializer(source='*', uri_attribute='course_video_url')
+    image = ImageSerializer(source='image_urls')
+
+    class Meta:
+        ref_name = 'courseware_api'
+
+
+class CourseInfoSerializer(serializers.Serializer):  # pylint: disable=abstract-method
+    """
+    Serializer for Course objects providing minimal data about the course.
+    Compare this with CourseDetailSerializer.
+    """
+
+    effort = serializers.CharField()
+    end = serializers.DateTimeField()
+    enrollment_start = serializers.DateTimeField()
+    enrollment_end = serializers.DateTimeField()
+    id = serializers.CharField()  # pylint: disable=invalid-name
+    media = _CourseApiMediaCollectionSerializer(source='*')
+    name = serializers.CharField(source='display_name_with_default_escaped')
+    number = serializers.CharField(source='display_number_with_default')
+    org = serializers.CharField(source='display_org_with_default')
+    short_description = serializers.CharField()
+    start = serializers.DateTimeField()
+    start_display = serializers.CharField()
+    start_type = serializers.CharField()
+    pacing = serializers.CharField()
+    enrollment = serializers.SerializerMethodField()
+    tabs = serializers.SerializerMethodField()
+
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize the serializer.
+        If `requested_fields` is set, then only return that subset of fields.
+        """
+        super().__init__(*args, **kwargs)
+        requested_fields = self.context['requested_fields']
+        if requested_fields is not None:
+            allowed = set(requested_fields.split(','))
+            existing = set(self.fields)
+            for field_name in existing - allowed:
+                self.fields.pop(field_name)
+
+    def get_tabs(self, course_overview):
+        """
+        Return course tab metadata.
+        """
+        tabs = []
+        for priority, tab in enumerate(get_course_tab_list(course_overview.effective_user, course_overview)):
+            tabs.append({
+                'title': tab.title,
+                'slug': tab.tab_id,
+                'priority': priority,
+                'type': tab.type,
+                'url': tab.link_func(course_overview, reverse),
+            })
+        return tabs
+
+    def get_enrollment(self, course_overview):
+        """
+        Return the enrollment for the logged in user.
+        """
+        mode, is_active = CourseEnrollment.enrollment_mode_for_user(
+            course_overview.effective_user,
+            course_overview.id
+        )
+        return {'mode': mode, 'is_active': is_active}
diff --git a/openedx/core/djangoapps/courseware_api/tests/__init__.py b/openedx/core/djangoapps/courseware_api/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a93e52ff65f8bd7acf3aa73aba05cd898108a43
--- /dev/null
+++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py
@@ -0,0 +1,108 @@
+"""
+Tests for courseware API
+"""
+from datetime import datetime
+import unittest
+import ddt
+
+from django.conf import settings
+
+from xmodule.modulestore.django import modulestore
+
+from xmodule.modulestore.tests.django_utils import (
+    TEST_DATA_SPLIT_MODULESTORE,
+    SharedModuleStoreTestCase
+)
+from xmodule.modulestore.tests.factories import ItemFactory, ToyCourseFactory
+from student.tests.factories import UserFactory
+from student.models import CourseEnrollment
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class BaseCoursewareTests(SharedModuleStoreTestCase):
+    """
+    Base class for courseware API tests
+    """
+    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.store = modulestore()
+        cls.course = ToyCourseFactory.create(
+            end=datetime(2028, 1, 1, 1, 1, 1),
+            enrollment_start=datetime(2020, 1, 1, 1, 1, 1),
+            enrollment_end=datetime(2028, 1, 1, 1, 1, 1),
+            emit_signals=True,
+            modulestore=cls.store,
+        )
+        cls.user = UserFactory(
+            username='student',
+            email=u'user@example.com',
+            password='foo',
+            is_staff=False
+        )
+        cls.url = '/api/courseware/course/{}'.format(cls.course.id)
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        cls.store.delete_course(cls.course.id, cls.user.id)
+
+    def setUp(self):
+        super().setUp()
+        self.client.login(username=self.user.username, password='foo')
+
+    def test_unauth(self):
+        self.client.logout()
+        response = self.client.get(self.url)
+        assert response.status_code == 401
+
+
+# pylint: disable=test-inherits-tests
+@ddt.ddt
+class CourseApiTestViews(BaseCoursewareTests):
+    """
+    Tests for the courseware REST API
+    """
+    @ddt.data((None,), ('audit',), ('verified',))
+    @ddt.unpack
+    def test_course_metadata(self, enrollment_mode):
+        if enrollment_mode:
+            CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode)
+        response = self.client.get(self.url)
+        assert response.status_code == 200
+        enrollment = response.data['enrollment']
+        if enrollment_mode:
+            assert enrollment_mode == enrollment['mode']
+            assert enrollment['is_active']
+            assert len(response.data['tabs']) == 4
+        else:
+            assert len(response.data['tabs']) == 2
+            assert not enrollment['is_active']
+
+
+# pylint: disable=test-inherits-tests
+class SequenceApiTestViews(BaseCoursewareTests):
+    """
+    Tests for the sequence REST API
+    """
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        chapter = ItemFactory(parent=cls.course, category='chapter')
+        cls.sequence = ItemFactory(parent=chapter, category='sequential', display_name='sequence')
+        ItemFactory.create(parent=cls.sequence, category='vertical', display_name="Vertical")
+        cls.url = '/api/courseware/sequence/{}'.format(cls.sequence.location)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.store.delete_item(cls.sequence.location, cls.user.id)
+        super().tearDownClass()
+
+    def test_sequence_metadata(self):
+        print(self.url)
+        print(self.course.location)
+        response = self.client.get(self.url)
+        assert response.status_code == 200
+        assert response.data['display_name'] == 'sequence'
+        assert len(response.data['items']) == 1
diff --git a/openedx/core/djangoapps/courseware_api/urls.py b/openedx/core/djangoapps/courseware_api/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..865955a6f1f72c929a9d94fc0afb07700b1b26a4
--- /dev/null
+++ b/openedx/core/djangoapps/courseware_api/urls.py
@@ -0,0 +1,18 @@
+"""
+Contains all the URLs
+"""
+
+
+from django.conf import settings
+from django.conf.urls import url
+
+from openedx.core.djangoapps.courseware_api import views
+
+urlpatterns = [
+    url(r'^course/{}'.format(settings.COURSE_KEY_PATTERN),
+        views.CoursewareInformation.as_view(),
+        name="courseware-api"),
+    url(r'^sequence/{}'.format(settings.USAGE_KEY_PATTERN),
+        views.SequenceMetadata.as_view(),
+        name="sequence-api"),
+]
diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..c842f1be0cee5bca05e69a852c3544e1c44e7c6a
--- /dev/null
+++ b/openedx/core/djangoapps/courseware_api/views.py
@@ -0,0 +1,133 @@
+"""
+Course API Views
+"""
+
+import json
+
+from opaque_keys.edx.keys import CourseKey, UsageKey
+from rest_framework.generics import RetrieveAPIView
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from lms.djangoapps.course_api.api import course_detail
+from lms.djangoapps.courseware.module_render import get_module_by_usage_id
+from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
+
+from .serializers import CourseInfoSerializer
+
+
+@view_auth_classes(is_authenticated=True)
+class CoursewareInformation(DeveloperErrorViewMixin, RetrieveAPIView):
+    """
+    **Use Cases**
+
+        Request details for a course
+
+    **Example Requests**
+
+        GET /api/courseware/course/{course_key}
+
+    **Response Values**
+
+        Body consists of the following fields:
+
+        * effort: A textual description of the weekly hours of effort expected
+            in the course.
+        * end: Date the course ends, in ISO 8601 notation
+        * enrollment_end: Date enrollment ends, in ISO 8601 notation
+        * enrollment_start: Date enrollment begins, in ISO 8601 notation
+        * id: A unique identifier of the course; a serialized representation
+            of the opaque key identifying the course.
+        * media: An object that contains named media items.  Included here:
+            * course_image: An image to show for the course.  Represented
+              as an object with the following fields:
+                * uri: The location of the image
+        * name: Name of the course
+        * number: Catalog number of the course
+        * org: Name of the organization that owns the course
+        * short_description: A textual description of the course
+        * start: Date the course begins, in ISO 8601 notation
+        * start_display: Readably formatted start of the course
+        * start_type: Hint describing how `start_display` is set. One of:
+            * `"string"`: manually set by the course author
+            * `"timestamp"`: generated from the `start` timestamp
+            * `"empty"`: no start date is specified
+        * pacing: Course pacing. Possible values: instructor, self
+        * tabs: Course tabs
+        * enrollment: Enrollment status of authenticated user
+            * mode: `audit`, `verified`, etc
+            * is_active: boolean
+
+    **Parameters:**
+
+        requested_fields (optional) comma separated list:
+            If set, then only those fields will be returned.
+
+    **Returns**
+
+        * 200 on success with above fields.
+        * 400 if an invalid parameter was sent or the username was not provided
+          for an authenticated request.
+        * 403 if a user who does not have permission to masquerade as
+          another user specifies a username other than their own.
+        * 404 if the course is not available or cannot be seen.
+    """
+
+    serializer_class = CourseInfoSerializer
+
+    def get_object(self):
+        """
+        Return the requested course object, if the user has appropriate
+        permissions.
+        """
+        return course_detail(
+            self.request,
+            self.request.user.username,
+            CourseKey.from_string(self.kwargs['course_key_string']),
+        )
+
+    def get_serializer_context(self):
+        """
+        Return extra context to be used by the serializer class.
+        """
+        context = super().get_serializer_context()
+        context['requested_fields'] = self.request.GET.get('requested_fields', None)
+        return context
+
+
+@view_auth_classes(is_authenticated=True)
+class SequenceMetadata(DeveloperErrorViewMixin, APIView):
+    """
+    **Use Cases**
+
+        Request details for a sequence/subsection
+
+    **Example Requests**
+
+        GET /api/courseware/sequence/{usage_key}
+
+    **Response Values**
+
+        Body consists of the following fields:
+            TODO
+
+    **Returns**
+
+        * 200 on success with above fields.
+        * 400 if an invalid parameter was sent.
+        * 403 if a user who does not have permission to masquerade as
+          another user specifies a username other than their own.
+        * 404 if the course is not available or cannot be seen.
+    """
+    def get(self, request, usage_key_string, *args, **kwargs):  # pylint: disable=unused-argument
+        """
+        Return response to a GET request.
+        """
+        usage_key = UsageKey.from_string(usage_key_string)
+
+        sequence, _ = get_module_by_usage_id(
+            self.request,
+            str(usage_key.course_key),
+            str(usage_key),
+            disable_staff_debug_info=True)
+        return Response(json.loads(sequence.handle_ajax('metadata', None)))
diff --git a/setup.py b/setup.py
index f1dff5cec670a2c9ed1c4505744cbf36f3c4e635..3a07a12e21cc07bc3573fba1a1d44ebb826b6f96 100644
--- a/setup.py
+++ b/setup.py
@@ -83,6 +83,7 @@ setup(
             "password_policy = openedx.core.djangoapps.password_policy.apps:PasswordPolicyConfig",
             "user_authn = openedx.core.djangoapps.user_authn.apps:UserAuthnConfig",
             "program_enrollments = lms.djangoapps.program_enrollments.apps:ProgramEnrollmentsConfig",
+            "courseware_api = openedx.core.djangoapps.courseware_api.apps:CoursewareAPIConfig",
         ],
         "cms.djangoapp": [
             "announcements = openedx.features.announcements.apps:AnnouncementsConfig",