diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 6dbc0b0d..c0a2258b 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,5 +1,5 @@
 {
   // See http://go.microsoft.com/fwlink/?LinkId=827846
   // for the documentation about the extensions.json format
-  "recommendations": ["dbaeumer.vscode-eslint", "apollographql.vscode-apollo"]
+  "recommendations": ["dbaeumer.vscode-eslint"]
 }
diff --git a/DEV.md b/DEV.md
index 1c20810f..7a0ed2ea 100644
--- a/DEV.md
+++ b/DEV.md
@@ -1,63 +1,13 @@
 # Setup
 
-## Development
-
-Run the postgres db and api server.
-
-Setup the extension environmental variables.
-
-/src/.env
-
-```
-LOG=true #show hide debugging logs
-```
-
-Setup the web app environmental variables.
-
-/web-app/.env.local
-
-```
-REACT_APP_DEBUG=true # show/hide web debugger
-REACT_APP_GQL_URI=http://localhost:4000/graphql
-```
-
-Run `npm run build`. Press F5 to open a new development window.
-
-Open the tutorial using `cmd+shift+p` on mac, and select the action `coderoad.start`.
-
-## Supported Programming Languages
-
-To support a new programming language, the test runner needs to support a format called TAP (https://testanything.org/).
-
-Some test frameworks can be modified to use tap, see a list of TAP reporters: https://github.com/sindresorhus/awesome-tap#reporters.
-
-### JavaScript
-
-##### Jest
-
-```json
-{
-  "scripts": {
-    "test": "jest"
-  },
-  "devDependencies": {
-    "jest-tap-reporter": "1.9.0"
-  },
-  "jest": {
-    "reporters": ["jest-tap-reporter"]
-  }
-}
-```
-
 ## Install Extension Demo
 
-1. Copy the `CodeRoad.vsix` file locally
-2. Select the extensions logo from the left hand panel
-3. In the top right of the panel, select the three dots “more” dropdown.
+1. Copy the `CodeRoad.vsix` file locally.
+2. In the top right of the panel, select the three dots “more” dropdown.
    1. Choose “Install from VSIX…”
    2. Select the `CodeRoad.vsix` file and press “Install”
-4. Reload the VSCode editor (Ctrl/Cmd + Shift + P, run "Reload Window")
-5. Open up a new folder directory in VSCode and run the extension `coderoad:start`
+3. Reload the VSCode editor (Ctrl/Cmd + Shift + P, run "Reload Window")
+4. Open up a new folder directory in VSCode and run the extension `coderoad:start`
 
 ## Known Issues
 
diff --git a/LICENSE.md b/LICENSE.md
index 4f81e246..4455ff17 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,3 +1,661 @@
-Copyright (c) 2019 Shawn McKay
+                   GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
 
-CodeRoad is a proprietary (closed source) software.
+Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+                            Preamble
+
+The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+The Corresponding Source for a work in source code form is that
+same work.
+
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.
diff --git a/package-lock.json b/package-lock.json
index d7ca09a7..74310dfe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -485,33 +485,33 @@
       }
     },
     "@jest/core": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.2.3.tgz",
-      "integrity": "sha512-Ifz3aEkGvZhwijLMmWa7sloZVEMdxpzjFv3CKHv3eRYRShTN8no6DmyvvxaZBjLalOlRalJ7HDgc733J48tSuw==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.2.4.tgz",
+      "integrity": "sha512-WcWYShl0Bqfcb32oXtjwbiR78D/djhMdJW+ulp4/bmHgeODcsieqUJfUH+kEv8M7VNV77E6jds5aA+WuGh1nmg==",
       "dev": true,
       "requires": {
         "@jest/console": "^25.2.3",
-        "@jest/reporters": "^25.2.3",
-        "@jest/test-result": "^25.2.3",
-        "@jest/transform": "^25.2.3",
+        "@jest/reporters": "^25.2.4",
+        "@jest/test-result": "^25.2.4",
+        "@jest/transform": "^25.2.4",
         "@jest/types": "^25.2.3",
         "ansi-escapes": "^4.2.1",
         "chalk": "^3.0.0",
         "exit": "^0.1.2",
         "graceful-fs": "^4.2.3",
         "jest-changed-files": "^25.2.3",
-        "jest-config": "^25.2.3",
+        "jest-config": "^25.2.4",
         "jest-haste-map": "^25.2.3",
-        "jest-message-util": "^25.2.3",
+        "jest-message-util": "^25.2.4",
         "jest-regex-util": "^25.2.1",
         "jest-resolve": "^25.2.3",
-        "jest-resolve-dependencies": "^25.2.3",
-        "jest-runner": "^25.2.3",
-        "jest-runtime": "^25.2.3",
-        "jest-snapshot": "^25.2.3",
+        "jest-resolve-dependencies": "^25.2.4",
+        "jest-runner": "^25.2.4",
+        "jest-runtime": "^25.2.4",
+        "jest-snapshot": "^25.2.4",
         "jest-util": "^25.2.3",
         "jest-validate": "^25.2.3",
-        "jest-watcher": "^25.2.3",
+        "jest-watcher": "^25.2.4",
         "micromatch": "^4.0.2",
         "p-each-series": "^2.1.0",
         "realpath-native": "^2.0.0",
@@ -594,12 +594,12 @@
       }
     },
     "@jest/environment": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.2.3.tgz",
-      "integrity": "sha512-zRypAMQnNo8rD0rCbI9+5xf+Lu+uvunKZNBcIWjb3lTATSomKbgYO+GYewGDYn7pf+30XCNBc6SH1rnBUN1ioA==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.2.4.tgz",
+      "integrity": "sha512-wA4xlhD19/gukkDpJ5HQsTle0pgnzI5qMFEjw267lpTDC8d9N7Ihqr5pI+l0p8Qn1SQhai+glSqxrGdzKy4jxw==",
       "dev": true,
       "requires": {
-        "@jest/fake-timers": "^25.2.3",
+        "@jest/fake-timers": "^25.2.4",
         "@jest/types": "^25.2.3",
         "jest-mock": "^25.2.3"
       },
@@ -669,13 +669,13 @@
       }
     },
     "@jest/fake-timers": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.2.3.tgz",
-      "integrity": "sha512-B6Qxm86fl613MV8egfvh1mRTMu23hMNdOUjzPhKl/4Nm5cceHz6nwLn0nP0sJXI/ue1vu71aLbtkgVBCgc2hYA==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.2.4.tgz",
+      "integrity": "sha512-oC1TJiwfMcBttVN7Wz+VZnqEAgYTiEMu0QLOXpypR89nab0uCB31zm/QeBZddhSstn20qe3yqOXygp6OwvKT/Q==",
       "dev": true,
       "requires": {
         "@jest/types": "^25.2.3",
-        "jest-message-util": "^25.2.3",
+        "jest-message-util": "^25.2.4",
         "jest-mock": "^25.2.3",
         "jest-util": "^25.2.3",
         "lolex": "^5.0.0"
@@ -746,15 +746,15 @@
       }
     },
     "@jest/reporters": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.2.3.tgz",
-      "integrity": "sha512-S0Zca5e7tTfGgxGRvBh6hktNdOBzqc6HthPzYHPRFYVW81SyzCqHTaNZydtDIVehb9s6NlyYZpcF/I2vco+lNw==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.2.4.tgz",
+      "integrity": "sha512-VHbLxM03jCc+bTLOluW/IqHR2G0Cl0iATwIQbuZtIUast8IXO4fD0oy4jpVGpG5b20S6REA8U3BaQoCW/CeVNQ==",
       "dev": true,
       "requires": {
         "@bcoe/v8-coverage": "^0.2.3",
         "@jest/console": "^25.2.3",
-        "@jest/test-result": "^25.2.3",
-        "@jest/transform": "^25.2.3",
+        "@jest/test-result": "^25.2.4",
+        "@jest/transform": "^25.2.4",
         "@jest/types": "^25.2.3",
         "chalk": "^3.0.0",
         "collect-v8-coverage": "^1.0.0",
@@ -853,13 +853,13 @@
       }
     },
     "@jest/test-result": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.2.3.tgz",
-      "integrity": "sha512-cNYidqERTcT+xqZZ5FPSvji7Bd2YYq9M/VJCEUmgTVRFZRPOPSu65crEzQJ4czcDChEJ9ovzZ65r3UBlajnh3w==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.2.4.tgz",
+      "integrity": "sha512-AI7eUy+q2lVhFnaibDFg68NGkrxVWZdD6KBr9Hm6EvN0oAe7GxpEwEavgPfNHQjU2mi6g+NsFn/6QPgTUwM1qg==",
       "dev": true,
       "requires": {
         "@jest/console": "^25.2.3",
-        "@jest/transform": "^25.2.3",
+        "@jest/transform": "^25.2.4",
         "@jest/types": "^25.2.3",
         "@types/istanbul-lib-coverage": "^2.0.0",
         "collect-v8-coverage": "^1.0.0"
@@ -930,21 +930,21 @@
       }
     },
     "@jest/test-sequencer": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.2.3.tgz",
-      "integrity": "sha512-trHwV/wCrxWyZyNyNBUQExsaHyBVQxJwH3butpEcR+KBJPfaTUxtpXaxfs38IXXAhH68J4kPZgAaRRfkFTLunA==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.2.4.tgz",
+      "integrity": "sha512-TEZm/Rkd6YgskdpTJdYLBtu6Gc11tfWPuSpatq0duH77ekjU8dpqX2zkPdY/ayuHxztV5LTJoV5BLtI9mZfXew==",
       "dev": true,
       "requires": {
-        "@jest/test-result": "^25.2.3",
+        "@jest/test-result": "^25.2.4",
         "jest-haste-map": "^25.2.3",
-        "jest-runner": "^25.2.3",
-        "jest-runtime": "^25.2.3"
+        "jest-runner": "^25.2.4",
+        "jest-runtime": "^25.2.4"
       }
     },
     "@jest/transform": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.2.3.tgz",
-      "integrity": "sha512-w1nfAuYP4OAiEDprFkE/2iwU86jL/hK3j1ylMcYOA3my5VOHqX0oeBcBxS2fUKWse2V4izuO2jqes0yNTDMlzw==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.2.4.tgz",
+      "integrity": "sha512-6eRigvb+G6bs4kW5j1/y8wu4nCrmVuIe0epPBbiWaYlwawJ8yi1EIyK3d/btDqmBpN5GpN4YhR6iPPnDmkYdTA==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.1.0",
@@ -1347,9 +1347,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "13.9.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
-      "integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw==",
+      "version": "13.9.8",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.8.tgz",
+      "integrity": "sha512-1WgO8hsyHynlx7nhP1kr0OFzsgKz5XDQL+Lfc3b1Q3qIln/n8cKD4m09NJ0+P1Rq7Zgnc7N0+SsMnoD1rEb0kA==",
       "dev": true
     },
     "@types/parse5": {
@@ -1392,25 +1392,25 @@
       "dev": true
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.25.0.tgz",
-      "integrity": "sha512-W2YyMtjmlrOjtXc+FtTelVs9OhuR6OlYc4XKIslJ8PUJOqgYYAPRJhAqkYRQo3G4sjvG8jSodsNycEn4W2gHUw==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.26.0.tgz",
+      "integrity": "sha512-4yUnLv40bzfzsXcTAtZyTjbiGUXMrcIJcIMioI22tSOyAxpdXiZ4r7YQUU8Jj6XXrLz9d5aMHPQf5JFR7h27Nw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "2.25.0",
+        "@typescript-eslint/experimental-utils": "2.26.0",
         "functional-red-black-tree": "^1.0.1",
         "regexpp": "^3.0.0",
         "tsutils": "^3.17.1"
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz",
-      "integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz",
+      "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/typescript-estree": "2.25.0",
+        "@typescript-eslint/typescript-estree": "2.26.0",
         "eslint-scope": "^5.0.0",
         "eslint-utils": "^2.0.0"
       },
@@ -1427,21 +1427,21 @@
       }
     },
     "@typescript-eslint/parser": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.25.0.tgz",
-      "integrity": "sha512-mccBLaBSpNVgp191CP5W+8U1crTyXsRziWliCqzj02kpxdjKMvFHGJbK33NroquH3zB/gZ8H511HEsJBa2fNEg==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.26.0.tgz",
+      "integrity": "sha512-+Xj5fucDtdKEVGSh9353wcnseMRkPpEAOY96EEenN7kJVrLqy/EVwtIh3mxcUz8lsFXW1mT5nN5vvEam/a5HiQ==",
       "dev": true,
       "requires": {
         "@types/eslint-visitor-keys": "^1.0.0",
-        "@typescript-eslint/experimental-utils": "2.25.0",
-        "@typescript-eslint/typescript-estree": "2.25.0",
+        "@typescript-eslint/experimental-utils": "2.26.0",
+        "@typescript-eslint/typescript-estree": "2.26.0",
         "eslint-visitor-keys": "^1.1.0"
       }
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz",
-      "integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz",
+      "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==",
       "dev": true,
       "requires": {
         "debug": "^4.1.1",
@@ -1655,12 +1655,12 @@
       "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
     },
     "babel-jest": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.2.3.tgz",
-      "integrity": "sha512-03JjvEwuDrEz/A45K8oggAv+Vqay0xcOdNTJxYFxiuZvB5vlHKo1iZg9Pi5vQTHhNCKpGLb7L/jvUUafyh9j7g==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.2.4.tgz",
+      "integrity": "sha512-+yDzlyJVWrqih9i2Cvjpt7COaN8vUwCsKGtxJLzg6I0xhxD54K8mvDUCliPKLufyzHh/c5C4MRj4Vk7VMjOjIg==",
       "dev": true,
       "requires": {
-        "@jest/transform": "^25.2.3",
+        "@jest/transform": "^25.2.4",
         "@jest/types": "^25.2.3",
         "@types/babel__core": "^7.1.0",
         "babel-plugin-istanbul": "^6.0.0",
@@ -1861,8 +1861,7 @@
     "browser-process-hrtime": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
-      "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
-      "dev": true
+      "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
     },
     "browser-resolve": {
       "version": "1.11.3",
@@ -2304,6 +2303,13 @@
       "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
       "requires": {
         "webidl-conversions": "^5.0.0"
+      },
+      "dependencies": {
+        "webidl-conversions": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+          "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
+        }
       }
     },
     "dotenv": {
@@ -2636,16 +2642,16 @@
       }
     },
     "expect": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.3.tgz",
-      "integrity": "sha512-kil4jFRFAK2ySyCyXPqYrphc3EiiKKFd9BthrkKAyHcqr1B84xFTuj5kO8zL+eHRRjT2jQsOPExO0+1Q/fuUXg==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.4.tgz",
+      "integrity": "sha512-hfuPhPds4yOsZtIw4kwAg70r0hqGmpqekgA+VX7pf/3wZ6FY+xIOXZhNsPMMMsspYG/YIsbAiwqsdnD4Ht+bCA==",
       "dev": true,
       "requires": {
         "@jest/types": "^25.2.3",
         "ansi-styles": "^4.0.0",
         "jest-get-type": "^25.2.1",
         "jest-matcher-utils": "^25.2.3",
-        "jest-message-util": "^25.2.3",
+        "jest-message-util": "^25.2.4",
         "jest-regex-util": "^25.2.1"
       },
       "dependencies": {
@@ -3624,14 +3630,14 @@
       "dev": true
     },
     "jest": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest/-/jest-25.2.3.tgz",
-      "integrity": "sha512-UbUmyGeZt0/sCIj/zsWOY0qFfQsx2qEFIZp0iEj8yVH6qASfR22fJOf12gFuSPsdSufam+llZBB0MdXWCg6EEQ==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest/-/jest-25.2.4.tgz",
+      "integrity": "sha512-Lu4LXxf4+durzN/IFilcAoQSisOwgHIXgl9vffopePpSSwFqfj1Pj4y+k3nL8oTbnvjxgDIsEcepy6he4bWqnQ==",
       "dev": true,
       "requires": {
-        "@jest/core": "^25.2.3",
+        "@jest/core": "^25.2.4",
         "import-local": "^3.0.2",
-        "jest-cli": "^25.2.3"
+        "jest-cli": "^25.2.4"
       },
       "dependencies": {
         "@jest/types": {
@@ -3688,19 +3694,19 @@
           "dev": true
         },
         "jest-cli": {
-          "version": "25.2.3",
-          "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.2.3.tgz",
-          "integrity": "sha512-T7G0TOkFj0wr33ki5xoq3bxkKC+liwJfjV9SmYIKBozwh91W4YjL1o1dgVCUTB1+sKJa/DiAY0p+eXYE6v2RGw==",
+          "version": "25.2.4",
+          "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.2.4.tgz",
+          "integrity": "sha512-zeY2pRDWKj2LZudIncvvguwLMEdcnJqc2jJbwza1beqi80qqLvkPF/BjbFkK2sIV3r+mfTJS+7ITrvK6pCdRjg==",
           "dev": true,
           "requires": {
-            "@jest/core": "^25.2.3",
-            "@jest/test-result": "^25.2.3",
+            "@jest/core": "^25.2.4",
+            "@jest/test-result": "^25.2.4",
             "@jest/types": "^25.2.3",
             "chalk": "^3.0.0",
             "exit": "^0.1.2",
             "import-local": "^3.0.2",
             "is-ci": "^2.0.0",
-            "jest-config": "^25.2.3",
+            "jest-config": "^25.2.4",
             "jest-util": "^25.2.3",
             "jest-validate": "^25.2.3",
             "prompts": "^2.0.1",
@@ -3884,22 +3890,22 @@
       }
     },
     "jest-config": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.2.3.tgz",
-      "integrity": "sha512-UpTNxN8DgmLLCXFizGuvwIw+ZAPB0T3jbKaFEkzJdGqhSsQrVrk1lxhZNamaVIpWirM2ptYmqwUzvoobGCEkiQ==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.2.4.tgz",
+      "integrity": "sha512-fxy3nIpwJqOUQJRVF/q+pNQb6dv5b9YufOeCbpPZJ/md1zXpiupbhfehpfODhnKOfqbzSiigtSLzlWWmbRxnqQ==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.1.0",
-        "@jest/test-sequencer": "^25.2.3",
+        "@jest/test-sequencer": "^25.2.4",
         "@jest/types": "^25.2.3",
-        "babel-jest": "^25.2.3",
+        "babel-jest": "^25.2.4",
         "chalk": "^3.0.0",
         "deepmerge": "^4.2.2",
         "glob": "^7.1.1",
-        "jest-environment-jsdom": "^25.2.3",
-        "jest-environment-node": "^25.2.3",
+        "jest-environment-jsdom": "^25.2.4",
+        "jest-environment-node": "^25.2.4",
         "jest-get-type": "^25.2.1",
-        "jest-jasmine2": "^25.2.3",
+        "jest-jasmine2": "^25.2.4",
         "jest-regex-util": "^25.2.1",
         "jest-resolve": "^25.2.3",
         "jest-util": "^25.2.3",
@@ -4160,13 +4166,13 @@
       }
     },
     "jest-environment-jsdom": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.2.3.tgz",
-      "integrity": "sha512-TLg7nizxIYJafz6tOBAVSmO5Ekswf6Cf3Soseov+mgonXfdYi1I0OZlHlZMJb2fGyXem2ndYFCLrMkwcWPKAnQ==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.2.4.tgz",
+      "integrity": "sha512-5dm+tNwrLmhELdjAwiQnVGf/U9iFMWdTL4/wyrMg2HU6RQnCiuxpWbIigLHUhuP1P2Ak0F4k3xhjrikboKyShA==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.2.3",
-        "@jest/fake-timers": "^25.2.3",
+        "@jest/environment": "^25.2.4",
+        "@jest/fake-timers": "^25.2.4",
         "@jest/types": "^25.2.3",
         "jest-mock": "^25.2.3",
         "jest-util": "^25.2.3",
@@ -4353,13 +4359,13 @@
       }
     },
     "jest-environment-node": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.2.3.tgz",
-      "integrity": "sha512-Tu/wlGXfoLtBR4Ym+isz58z3TJkMYX4VnFTkrsxaTGYAxNLN7ArCwL51Ki0WrMd89v+pbCLDj/hDjrb4a2sOrw==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.2.4.tgz",
+      "integrity": "sha512-Jkc5Y8goyXPrLRHnrUlqC7P4o5zn2m4zw6qWoRJ59kxV1f2a5wK+TTGhrhCwnhW/Ckpdl/pm+LufdvhJkvJbiw==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.2.3",
-        "@jest/fake-timers": "^25.2.3",
+        "@jest/environment": "^25.2.4",
+        "@jest/fake-timers": "^25.2.4",
         "@jest/types": "^25.2.3",
         "jest-mock": "^25.2.3",
         "jest-util": "^25.2.3",
@@ -4536,25 +4542,25 @@
       }
     },
     "jest-jasmine2": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.2.3.tgz",
-      "integrity": "sha512-x9PEGPFdnkSwJj1UG4QxG9JxFdyP8fuJ/UfKXd/eSpK8w9x7MP3VaQDuPQF0UQhCT0YeOITEPkQyqS+ptt0suA==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.2.4.tgz",
+      "integrity": "sha512-juoKrmNmLwaheNbAg71SuUF9ovwUZCFNTpKVhvCXWk+SSeORcIUMptKdPCoLXV3D16htzhTSKmNxnxSk4SrTjA==",
       "dev": true,
       "requires": {
         "@babel/traverse": "^7.1.0",
-        "@jest/environment": "^25.2.3",
+        "@jest/environment": "^25.2.4",
         "@jest/source-map": "^25.2.1",
-        "@jest/test-result": "^25.2.3",
+        "@jest/test-result": "^25.2.4",
         "@jest/types": "^25.2.3",
         "chalk": "^3.0.0",
         "co": "^4.6.0",
-        "expect": "^25.2.3",
+        "expect": "^25.2.4",
         "is-generator-fn": "^2.0.0",
         "jest-each": "^25.2.3",
         "jest-matcher-utils": "^25.2.3",
-        "jest-message-util": "^25.2.3",
-        "jest-runtime": "^25.2.3",
-        "jest-snapshot": "^25.2.3",
+        "jest-message-util": "^25.2.4",
+        "jest-runtime": "^25.2.4",
+        "jest-snapshot": "^25.2.4",
         "jest-util": "^25.2.3",
         "pretty-format": "^25.2.3",
         "throat": "^5.0.0"
@@ -4841,13 +4847,13 @@
       }
     },
     "jest-message-util": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.2.3.tgz",
-      "integrity": "sha512-DcyDmdO5LVIeS0ngRvd7rk701XL60dAakUeQJ1tQRby27fyLYXD+V0nqVaC194W7fIlohjVQOZPHmKXIjn+Byw==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.2.4.tgz",
+      "integrity": "sha512-9wWMH3Bf+GVTv0GcQLmH/FRr0x0toptKw9TA8U5YFLVXx7Tq9pvcNzTyJrcTJ+wLqNbMPPJlJNft4MnlcrtF5Q==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@jest/test-result": "^25.2.3",
+        "@jest/test-result": "^25.2.4",
         "@jest/types": "^25.2.3",
         "@types/stack-utils": "^1.0.1",
         "chalk": "^3.0.0",
@@ -5084,14 +5090,14 @@
       }
     },
     "jest-resolve-dependencies": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.3.tgz",
-      "integrity": "sha512-mcWlvjXLlNzgdE9EQxHuaeWICNxozanim87EfyvPwTY0ryWusFZbgF6F8u3E0syJ4FFSooEm0lQ6fgYcnPGAFw==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.4.tgz",
+      "integrity": "sha512-qhUnK4PfNHzNdca7Ub1mbAqE0j5WNyMTwxBZZJjQlUrdqsiYho/QGK65FuBkZuSoYtKIIqriR9TpGrPEc3P5Gg==",
       "dev": true,
       "requires": {
         "@jest/types": "^25.2.3",
         "jest-regex-util": "^25.2.1",
-        "jest-snapshot": "^25.2.3"
+        "jest-snapshot": "^25.2.4"
       },
       "dependencies": {
         "@jest/types": {
@@ -5159,26 +5165,26 @@
       }
     },
     "jest-runner": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.2.3.tgz",
-      "integrity": "sha512-E+u2Zm2TmtTOFEbKs5jllLiV2fwiX77cYc08RdyYZNe/s06wQT3P47aV6a8Rv61L7E2Is7OmozLd0KI/DITRpg==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.2.4.tgz",
+      "integrity": "sha512-5xaIfqqxck9Wg2CV4b9KmJtf/sWO7zWQx7O+34GCLGPzoPcVmB3mZtdrQI1/jS3Reqjru9ycLjgLHSf6XoxRqA==",
       "dev": true,
       "requires": {
         "@jest/console": "^25.2.3",
-        "@jest/environment": "^25.2.3",
-        "@jest/test-result": "^25.2.3",
+        "@jest/environment": "^25.2.4",
+        "@jest/test-result": "^25.2.4",
         "@jest/types": "^25.2.3",
         "chalk": "^3.0.0",
         "exit": "^0.1.2",
         "graceful-fs": "^4.2.3",
-        "jest-config": "^25.2.3",
+        "jest-config": "^25.2.4",
         "jest-docblock": "^25.2.3",
         "jest-haste-map": "^25.2.3",
-        "jest-jasmine2": "^25.2.3",
+        "jest-jasmine2": "^25.2.4",
         "jest-leak-detector": "^25.2.3",
-        "jest-message-util": "^25.2.3",
+        "jest-message-util": "^25.2.4",
         "jest-resolve": "^25.2.3",
-        "jest-runtime": "^25.2.3",
+        "jest-runtime": "^25.2.4",
         "jest-util": "^25.2.3",
         "jest-worker": "^25.2.1",
         "source-map-support": "^0.5.6",
@@ -5250,16 +5256,16 @@
       }
     },
     "jest-runtime": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.2.3.tgz",
-      "integrity": "sha512-PZRFeUVF08N24v2G73SDF0b0VpLG7cRNOJ3ggj5TnArBVHkkrWzM3z7txB9OupWu7OO8bH/jFogk6sSjnHLFXQ==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.2.4.tgz",
+      "integrity": "sha512-6ehOUizgIghN+aV5YSrDzTZ+zJ9omgEjJbTHj3Jqes5D52XHfhzT7cSfdREwkNjRytrR7mNwZ7pRauoyNLyJ8Q==",
       "dev": true,
       "requires": {
         "@jest/console": "^25.2.3",
-        "@jest/environment": "^25.2.3",
+        "@jest/environment": "^25.2.4",
         "@jest/source-map": "^25.2.1",
-        "@jest/test-result": "^25.2.3",
-        "@jest/transform": "^25.2.3",
+        "@jest/test-result": "^25.2.4",
+        "@jest/transform": "^25.2.4",
         "@jest/types": "^25.2.3",
         "@types/yargs": "^15.0.0",
         "chalk": "^3.0.0",
@@ -5267,13 +5273,13 @@
         "exit": "^0.1.2",
         "glob": "^7.1.3",
         "graceful-fs": "^4.2.3",
-        "jest-config": "^25.2.3",
+        "jest-config": "^25.2.4",
         "jest-haste-map": "^25.2.3",
-        "jest-message-util": "^25.2.3",
+        "jest-message-util": "^25.2.4",
         "jest-mock": "^25.2.3",
         "jest-regex-util": "^25.2.1",
         "jest-resolve": "^25.2.3",
-        "jest-snapshot": "^25.2.3",
+        "jest-snapshot": "^25.2.4",
         "jest-util": "^25.2.3",
         "jest-validate": "^25.2.3",
         "realpath-native": "^2.0.0",
@@ -5353,20 +5359,20 @@
       "dev": true
     },
     "jest-snapshot": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.2.3.tgz",
-      "integrity": "sha512-HlFVbE6vOZ541mtkwjuAe0rfx9EWhB+QXXneLNOP/s3LlHxGQtX7WFXY5OiH4CkAnCc6BpzLNYS9nfINNRb4Zg==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.2.4.tgz",
+      "integrity": "sha512-nIwpW7FZCq5p0AE3Oyqyb6jL0ENJixXzJ5/CD/XRuOqp3gS5OM3O/k+NnTrniCXxPFV4ry6s9HNfiPQBi0wcoA==",
       "dev": true,
       "requires": {
         "@babel/types": "^7.0.0",
         "@jest/types": "^25.2.3",
         "@types/prettier": "^1.19.0",
         "chalk": "^3.0.0",
-        "expect": "^25.2.3",
+        "expect": "^25.2.4",
         "jest-diff": "^25.2.3",
         "jest-get-type": "^25.2.1",
         "jest-matcher-utils": "^25.2.3",
-        "jest-message-util": "^25.2.3",
+        "jest-message-util": "^25.2.4",
         "jest-resolve": "^25.2.3",
         "make-dir": "^3.0.0",
         "natural-compare": "^1.4.0",
@@ -5653,12 +5659,12 @@
       }
     },
     "jest-watcher": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.2.3.tgz",
-      "integrity": "sha512-F6ERbdvJk8nbaRon9lLQVl4kp+vToCCHmy+uWW5QQ8/8/g2jkrZKJQnlQINrYQp0ewg31Bztkhs4nxsZMx6wDg==",
+      "version": "25.2.4",
+      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.2.4.tgz",
+      "integrity": "sha512-p7g7s3zqcy69slVzQYcphyzkB2FBmJwMbv6k6KjI5mqd6KnUnQPfQVKuVj2l+34EeuxnbXqnrjtUFmxhcL87rg==",
       "dev": true,
       "requires": {
-        "@jest/test-result": "^25.2.3",
+        "@jest/test-result": "^25.2.4",
         "@jest/types": "^25.2.3",
         "ansi-escapes": "^4.2.1",
         "chalk": "^3.0.0",
@@ -5787,9 +5793,9 @@
       "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
     },
     "jsdom": {
-      "version": "16.2.1",
-      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.1.tgz",
-      "integrity": "sha512-3p0gHs5EfT7PxW9v8Phz3mrq//4Dy8MQenU/PoKxhdT+c45S7NjIjKbGT3Ph0nkICweE1r36+yaknXA5WfVNAg==",
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.2.tgz",
+      "integrity": "sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg==",
       "requires": {
         "abab": "^2.0.3",
         "acorn": "^7.1.1",
@@ -5811,11 +5817,11 @@
         "tough-cookie": "^3.0.1",
         "w3c-hr-time": "^1.0.2",
         "w3c-xmlserializer": "^2.0.0",
-        "webidl-conversions": "^5.0.0",
+        "webidl-conversions": "^6.0.0",
         "whatwg-encoding": "^1.0.5",
         "whatwg-mimetype": "^2.3.0",
         "whatwg-url": "^8.0.0",
-        "ws": "^7.2.1",
+        "ws": "^7.2.3",
         "xml-name-validator": "^3.0.0"
       },
       "dependencies": {
@@ -5833,11 +5839,6 @@
           "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz",
           "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ=="
         },
-        "browser-process-hrtime": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
-          "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
-        },
         "request": {
           "version": "2.88.2",
           "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -5876,13 +5877,10 @@
             }
           }
         },
-        "w3c-hr-time": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
-          "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
-          "requires": {
-            "browser-process-hrtime": "^1.0.0"
-          }
+        "ws": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
+          "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
         }
       }
     },
@@ -7830,7 +7828,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
       "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
-      "dev": true,
       "requires": {
         "browser-process-hrtime": "^1.0.0"
       }
@@ -7853,9 +7850,9 @@
       }
     },
     "webidl-conversions": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
-      "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.0.0.tgz",
+      "integrity": "sha512-jTZAeJnc6D+yAOjygbJOs33kVQIk5H6fj9SFDOhIKjsf9HiAzL/c+tAJsc8ASWafvhNkH+wJZms47pmajkhatA=="
     },
     "whatwg-encoding": {
       "version": "1.0.5",
@@ -7878,6 +7875,13 @@
         "lodash.sortby": "^4.7.0",
         "tr46": "^2.0.0",
         "webidl-conversions": "^5.0.0"
+      },
+      "dependencies": {
+        "webidl-conversions": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+          "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
+        }
       }
     },
     "which": {
@@ -7966,9 +7970,10 @@
       }
     },
     "ws": {
-      "version": "7.2.1",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
-      "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A=="
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
+      "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==",
+      "dev": true
     },
     "xml-name-validator": {
       "version": "3.0.0",
diff --git a/package.json b/package.json
index b51b25f1..8fef539d 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "homepage": "https://github.com/shmck/coderoad-vscode/README.md",
   "bugs": {
     "url": "https://github.com/shmck/coderoad-vscode/issues",
-    "email": "shawn.j.mckay@gmail.com"
+    "email": "coderoadapp@gmail.com"
   },
   "repository": {
     "type": "git",
@@ -34,7 +34,7 @@
     "@sentry/node": "^5.15.4",
     "chokidar": "^3.3.0",
     "dotenv": "^8.2.0",
-    "jsdom": "^16.2.1"
+    "jsdom": "^16.2.2"
   },
   "devDependencies": {
     "@types/assert": "^1.4.6",
@@ -42,14 +42,14 @@
     "@types/dotenv": "^8.2.0",
     "@types/jest": "^25.1.4",
     "@types/jsdom": "^16.2.0",
-    "@types/node": "^13.9.5",
-    "@typescript-eslint/eslint-plugin": "^2.25.0",
-    "@typescript-eslint/parser": "^2.25.0",
+    "@types/node": "^13.9.8",
+    "@typescript-eslint/eslint-plugin": "^2.26.0",
+    "@typescript-eslint/parser": "^2.26.0",
     "eslint": "^6.8.0",
     "eslint-config-prettier": "^6.10.1",
     "eslint-plugin-prettier": "^3.1.2",
     "graphql": "^14.6.0",
-    "jest": "^25.2.3",
+    "jest": "^25.2.4",
     "prettier": "^2.0.2",
     "ts-jest": "^25.2.1",
     "typescript": "^3.8.3",
diff --git a/src/actions/setupActions.ts b/src/actions/setupActions.ts
index 8c188515..c4de8e56 100644
--- a/src/actions/setupActions.ts
+++ b/src/actions/setupActions.ts
@@ -1,4 +1,5 @@
 import * as T from 'typings'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import * as git from '../services/git'
 import loadWatchers from './utils/loadWatchers'
@@ -8,7 +9,7 @@ import onError from '../services/sentry/onError'
 
 const setupActions = async (
   workspaceRoot: vscode.WorkspaceFolder,
-  actions: T.StepActions,
+  actions: TT.StepActions,
   send: (action: T.Action) => void, // send messages to client
 ): Promise<void> => {
   const { commands, commits, files, watchers } = actions
diff --git a/src/actions/solutionActions.ts b/src/actions/solutionActions.ts
index cf58b3c9..0f26b61f 100644
--- a/src/actions/solutionActions.ts
+++ b/src/actions/solutionActions.ts
@@ -1,4 +1,5 @@
 import * as T from 'typings'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import * as git from '../services/git'
 import setupActions from './setupActions'
@@ -6,7 +7,7 @@ import onError from '../services/sentry/onError'
 
 const solutionActions = async (
   workspaceRoot: vscode.WorkspaceFolder,
-  stepActions: T.StepActions,
+  stepActions: TT.StepActions,
   send: (action: T.Action) => void,
 ): Promise<void> => {
   await git.clear()
diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts
index 458b3676..9bf93f31 100644
--- a/src/actions/tutorialConfig.ts
+++ b/src/actions/tutorialConfig.ts
@@ -1,13 +1,12 @@
 import * as T from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import { COMMANDS } from '../editor/commands'
-import languageMap from '../editor/languageMap'
 import * as git from '../services/git'
 import onError from '../services/sentry/onError'
 
 interface TutorialConfigParams {
-  config: T.TutorialConfig
+  config: TT.TutorialConfig
   alreadyConfigured?: boolean
   onComplete?(): void
 }
diff --git a/src/channel/context.ts b/src/channel/context.ts
index a2826c9c..3e61919c 100644
--- a/src/channel/context.ts
+++ b/src/channel/context.ts
@@ -1,5 +1,5 @@
 import * as CR from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import Position from './state/Position'
 import Progress from './state/Progress'
@@ -17,7 +17,7 @@ class Context {
   }
   public setTutorial = async (
     workspaceState: vscode.Memento,
-    tutorial: G.Tutorial,
+    tutorial: TT.Tutorial,
   ): Promise<{ progress: CR.Progress; position: CR.Position }> => {
     this.tutorial.set(tutorial)
     const progress: CR.Progress = await this.progress.setTutorial(workspaceState, tutorial)
diff --git a/src/channel/index.ts b/src/channel/index.ts
index 03529759..157b887c 100644
--- a/src/channel/index.ts
+++ b/src/channel/index.ts
@@ -1,5 +1,5 @@
 import * as T from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import saveCommit from '../actions/saveCommit'
 import setupActions from '../actions/setupActions'
@@ -54,10 +54,10 @@ class Channel implements Channel {
         return
       // continue from tutorial from local storage
       case 'EDITOR_TUTORIAL_LOAD':
-        const tutorial: G.Tutorial | null = this.context.tutorial.get()
+        const tutorial: TT.Tutorial | null = this.context.tutorial.get()
 
         // new tutorial
-        if (!tutorial || !tutorial.id || !tutorial.version) {
+        if (!tutorial || !tutorial.id) {
           this.send({ type: 'START_NEW_TUTORIAL' })
           return
         }
@@ -81,11 +81,9 @@ class Channel implements Channel {
         return
       // configure test runner, language, git
       case 'EDITOR_TUTORIAL_CONFIG':
-        const tutorialData: G.Tutorial = action.payload.tutorial
+        const data: TT.Tutorial = action.payload.tutorial
         // setup tutorial config (save watcher, test runner, etc)
-        this.context.setTutorial(this.workspaceState, tutorialData)
-
-        const data: G.TutorialData = tutorialData.version.data
+        this.context.setTutorial(this.workspaceState, data)
 
         await tutorialConfig({ config: data.config }, onError)
 
@@ -93,11 +91,11 @@ class Channel implements Channel {
         this.send({ type: 'TUTORIAL_CONFIGURED' })
         return
       case 'EDITOR_TUTORIAL_CONTINUE_CONFIG':
-        const tutorialContinue: G.Tutorial | null = this.context.tutorial.get()
+        const tutorialContinue: TT.Tutorial | null = this.context.tutorial.get()
         if (!tutorialContinue) {
           throw new Error('Invalid tutorial to continue')
         }
-        const continueConfig: T.TutorialConfig = tutorialContinue.version.data.config
+        const continueConfig: TT.TutorialConfig = tutorialContinue.config
         await tutorialConfig(
           {
             config: continueConfig,
@@ -148,7 +146,7 @@ class Channel implements Channel {
           throw new Error('Error with current tutorial')
         }
         // update local storage stepProgress
-        const progress = this.context.progress.setStepComplete(tutorial.version.data, action.payload.stepId)
+        const progress = this.context.progress.setStepComplete(tutorial, action.payload.stepId)
         this.context.position.setPositionFromProgress(tutorial, progress)
         saveCommit()
     }
diff --git a/src/channel/state/Position.ts b/src/channel/state/Position.ts
index c8b67a54..88caca58 100644
--- a/src/channel/state/Position.ts
+++ b/src/channel/state/Position.ts
@@ -1,5 +1,5 @@
 import * as CR from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 
 const defaultValue: CR.Position = {
   levelId: '',
@@ -22,26 +22,26 @@ class Position {
     this.value = defaultValue
   }
   // calculate the current position based on the saved progress
-  public setPositionFromProgress = (tutorial: G.Tutorial, progress: CR.Progress): CR.Position => {
+  public setPositionFromProgress = (tutorial: TT.Tutorial, progress: CR.Progress): CR.Position => {
     // tutorial already completed
     // TODO handle start again?
     if (progress.complete) {
       return this.value
     }
 
-    if (!tutorial || !tutorial.version || !tutorial.version.data || !tutorial.version.data.levels) {
+    if (!tutorial || !tutorial.levels) {
       throw new Error('Error setting position from progress')
     }
 
     // get level
-    const { levels } = tutorial.version.data
-    const lastLevelIndex: number | undefined = levels.findIndex((l: G.Level) => !progress.levels[l.id])
+    const { levels } = tutorial
+    const lastLevelIndex: number | undefined = levels.findIndex((l: TT.Level) => !progress.levels[l.id])
     if (lastLevelIndex >= levels.length) {
       throw new Error('Error setting progress level')
     }
 
     // get step
-    const currentLevel: G.Level = levels[lastLevelIndex]
+    const currentLevel: TT.Level = levels[lastLevelIndex]
     let currentStepId: string | null
     if (!currentLevel.steps.length) {
       // no steps available for level
@@ -49,7 +49,7 @@ class Position {
     } else {
       // find current step id
       const { steps } = currentLevel
-      const lastStepIndex: number | undefined = steps.findIndex((s: G.Step) => !progress.steps[s.id])
+      const lastStepIndex: number | undefined = steps.findIndex((s: TT.Step) => !progress.steps[s.id])
       if (lastStepIndex >= steps.length) {
         throw new Error('Error setting progress step')
       }
diff --git a/src/channel/state/Progress.ts b/src/channel/state/Progress.ts
index 5e60d1d6..839614cb 100644
--- a/src/channel/state/Progress.ts
+++ b/src/channel/state/Progress.ts
@@ -1,9 +1,9 @@
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
+import * as T from 'typings'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import Storage from '../../services/storage'
 
-const defaultValue: CR.Progress = {
+const defaultValue: T.Progress = {
   levels: {},
   steps: {},
   complete: false,
@@ -11,13 +11,13 @@ const defaultValue: CR.Progress = {
 
 // hold current progress and sync to storage based on tutorial.id/version
 class Progress {
-  private value: CR.Progress
-  private storage: Storage<CR.Progress> | undefined
+  private value: T.Progress
+  private storage: Storage<T.Progress> | undefined
   constructor() {
     this.value = defaultValue
   }
-  public setTutorial = async (workspaceState: vscode.Memento, tutorial: G.Tutorial): Promise<CR.Progress> => {
-    this.storage = new Storage<CR.Progress>({
+  public setTutorial = async (workspaceState: vscode.Memento, tutorial: TT.Tutorial): Promise<T.Progress> => {
+    this.storage = new Storage<T.Progress>({
       key: `coderoad:progress:${tutorial.id}:${tutorial.version}`,
       storage: workspaceState,
       defaultValue,
@@ -28,7 +28,7 @@ class Progress {
   public get = () => {
     return this.value
   }
-  public set = (value: CR.Progress) => {
+  public set = (value: T.Progress) => {
     this.value = value
     if (!this.storage) {
       return defaultValue
@@ -39,12 +39,12 @@ class Progress {
   public reset = () => {
     this.set(defaultValue)
   }
-  public setStepComplete = (tutorialData: G.TutorialData, stepId: string): CR.Progress => {
+  public setStepComplete = (tutorial: TT.Tutorial, stepId: string): T.Progress => {
     const next = this.value
     // mark step complete
     next.steps[stepId] = true
 
-    const currentLevel = tutorialData.levels.find((l) => l.steps.find((s) => s.id === stepId))
+    const currentLevel = tutorial.levels.find((l) => l.steps.find((s) => s.id === stepId))
     if (!currentLevel) {
       throw new Error(`setStepComplete level not found for stepId ${stepId}`)
     }
@@ -53,7 +53,7 @@ class Progress {
       // final step for level is complete
       next.levels[currentLevel.id] = true
 
-      if (tutorialData.levels[tutorialData.levels.length - 1].id === currentLevel.id) {
+      if (tutorial.levels[tutorial.levels.length - 1].id === currentLevel.id) {
         //final level complete so tutorial is complete
         next.complete = true
       }
diff --git a/src/channel/state/Tutorial.ts b/src/channel/state/Tutorial.ts
index 58afbed4..3a91d19c 100644
--- a/src/channel/state/Tutorial.ts
+++ b/src/channel/state/Tutorial.ts
@@ -1,26 +1,26 @@
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import Storage from '../../services/storage'
 
 // Tutorial
 class Tutorial {
-  private storage: Storage<G.Tutorial | null>
-  private value: G.Tutorial | null = null
+  private storage: Storage<TT.Tutorial | null>
+  private value: TT.Tutorial | null = null
   constructor(workspaceState: vscode.Memento) {
-    this.storage = new Storage<G.Tutorial | null>({
+    this.storage = new Storage<TT.Tutorial | null>({
       key: 'coderoad:currentTutorial',
       storage: workspaceState,
       defaultValue: null,
     })
     // set value from storage
-    this.storage.get().then((value: G.Tutorial | null) => {
+    this.storage.get().then((value: TT.Tutorial | null) => {
       this.value = value
     })
   }
   public get = () => {
     return this.value
   }
-  public set = (value: G.Tutorial | null) => {
+  public set = (value: TT.Tutorial | null) => {
     this.value = value
     this.storage.set(value)
   }
diff --git a/src/editor/commands.ts b/src/editor/commands.ts
index 22a08340..cd8e53f3 100644
--- a/src/editor/commands.ts
+++ b/src/editor/commands.ts
@@ -1,4 +1,4 @@
-import * as T from 'typings'
+import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import createTestRunner, { Payload } from '../services/testRunner'
 import createWebView from '../webview'
@@ -49,7 +49,7 @@ export const createCommands = ({ extensionPath, workspaceState, workspaceRoot }:
       // setup 1x1 horizontal layout
       webview.createOrShow()
     },
-    [COMMANDS.CONFIG_TEST_RUNNER]: (config: T.TutorialTestRunner) => {
+    [COMMANDS.CONFIG_TEST_RUNNER]: (config: TT.TutorialTestRunner) => {
       testRunner = createTestRunner(config, {
         onSuccess: (payload: Payload) => {
           // send test pass message back to client
diff --git a/src/editor/languageMap.ts b/src/editor/languageMap.ts
deleted file mode 100644
index 1760e37c..00000000
--- a/src/editor/languageMap.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as G from 'typings/graphql'
-// sourced from https://code.visualstudio.com/docs/languages/identifiers
-const languageMap: {
-  [lang: string]: G.FileFormat
-} = {
-  go: 'GO',
-  javascript: 'JS',
-  javascriptreact: 'JSX',
-  json: 'JSON',
-  less: 'LESS',
-  lua: 'LUA',
-  php: 'PHP',
-  python: 'PY',
-  ruby: 'RB',
-  sass: 'SASS',
-  scss: 'SCSS',
-  sql: 'SQL',
-  typescript: 'TS',
-  typescriptreact: 'TSX',
-  yaml: 'YAML',
-}
-
-export default languageMap
diff --git a/tsconfig.json b/tsconfig.json
index 598c0d55..9c8ae641 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,7 +20,7 @@
     "emitDecoratorMetadata": true,
     "paths": {
       "typings": ["../typings/index.d.ts"],
-      "typings/graphql": ["../typings/graphql.d.ts"]
+      "typings/tutorial": ["../typings/tutorial.d.ts"]
     },
     "allowJs": true,
     "removeComments": true
diff --git a/typings/graphql.d.ts b/typings/graphql.d.ts
deleted file mode 100644
index c92c6f34..00000000
--- a/typings/graphql.d.ts
+++ /dev/null
@@ -1,603 +0,0 @@
-import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'
-export type Maybe<T> = T | null
-export type RequireFields<T, K extends keyof T> = { [X in Exclude<keyof T, K>]?: T[X] } &
-  { [P in K]-?: NonNullable<T[P]> }
-/** All built-in and custom scalars, mapped to their actual values */
-export type Scalars = {
-  ID: string
-  String: string
-  Boolean: boolean
-  Int: number
-  Float: number
-  DateTime: any
-  JSON: any
-  JSONObject: any
-  Sha1: any
-}
-
-export type CreateTutorialInput = {
-  /** TODO: tutorial type */
-  id: Scalars['ID']
-  summaryTitle: Scalars['String']
-  summaryDescription: Scalars['String']
-}
-
-export type CreateTutorialVersionInput = {
-  /** TODO: tutorial version type */
-  id: Scalars['ID']
-}
-
-/** Supported Editors */
-export type Editor = 'VSCODE'
-
-/**
- * Login input from an editor extension/plugin
- * Accepts a unique machineId from the editor
- */
-export type EditorLoginInput = {
-  editor: Editor
-  machineId: Scalars['String']
-  sessionId: Scalars['String']
-}
-
-export type EditorLoginOutput = {
-  __typename?: 'editorLoginOutput'
-  user: User
-  token: Scalars['String']
-}
-
-/**
- * File formats supported by VSCode
- * See a complete list at https://code.visualstudio.com/docs/languages/identifiers
- */
-export type FileFormat =
-  | 'CLOJURE'
-  | 'C'
-  | 'CPP'
-  | 'CSHARP'
-  | 'CSS'
-  | 'DIFF'
-  | 'DOCKERFILE'
-  | 'FSHARP'
-  | 'GO'
-  | 'HTML'
-  | 'INI'
-  | 'JAVA'
-  | 'JS'
-  | 'JSON'
-  | 'JSONC'
-  | 'JSX'
-  | 'LATEX'
-  | 'LESS'
-  | 'LUA'
-  | 'MARKDOWN'
-  | 'PHP'
-  | 'PY'
-  | 'R'
-  | 'RB'
-  | 'RUST'
-  | 'SASS'
-  | 'SCSS'
-  | 'SQL'
-  | 'SWIFT'
-  | 'TS'
-  | 'TSX'
-  | 'XML'
-  | 'YAML'
-
-/** Information linked from a GitHub account */
-export type GithubUser = {
-  __typename?: 'GithubUser'
-  id: Scalars['ID']
-  name?: Maybe<Scalars['String']>
-  email?: Maybe<Scalars['String']>
-  location?: Maybe<Scalars['String']>
-  avatarUrl?: Maybe<Scalars['String']>
-}
-
-/** Logical groupings of tasks */
-export type Level = {
-  __typename?: 'Level'
-  id: Scalars['ID']
-  title: Scalars['String']
-  /** A summary of the level */
-  summary: Scalars['String']
-  /** The lesson content of the level, parsed as markdown */
-  content: Scalars['String']
-  /** Actions run on level start up for configuring setup */
-  setup?: Maybe<Scalars['JSON']>
-  /** A set of tasks for users linked to unit tests */
-  steps: Array<Step>
-}
-
-export type Mutation = {
-  __typename?: 'Mutation'
-  /** Login used from a coding editor */
-  editorLogin?: Maybe<EditorLoginOutput>
-  /** Update a users tutorial progress */
-  updateTutorialProgress?: Maybe<Scalars['Boolean']>
-  /** Create a new tutorial */
-  createTutorial?: Maybe<Tutorial>
-  /** Create a new tutorial version */
-  createTutorialVersion?: Maybe<TutorialVersion>
-  /** Update a tutorial version */
-  updateTutorialVersion?: Maybe<TutorialVersion>
-  /** Publish a tutorial version */
-  publishTutorialVersion?: Maybe<Tutorial>
-  /** Deprecate a tutorial version */
-  deprecateTutorialVersion?: Maybe<Scalars['Boolean']>
-}
-
-export type MutationEditorLoginArgs = {
-  input: EditorLoginInput
-}
-
-export type MutationUpdateTutorialProgressArgs = {
-  input: UpdateTutorialProgressInput
-}
-
-export type MutationCreateTutorialArgs = {
-  input: Scalars['String']
-}
-
-export type MutationCreateTutorialVersionArgs = {
-  input: CreateTutorialVersionInput
-}
-
-export type MutationUpdateTutorialVersionArgs = {
-  input: UpdateTutorialVersionInput
-}
-
-export type MutationPublishTutorialVersionArgs = {
-  tutorialId: Scalars['ID']
-  versionID: Scalars['ID']
-}
-
-export type MutationDeprecateTutorialVersionArgs = {
-  tutorialId: Scalars['ID']
-  versionID: Scalars['ID']
-}
-
-export type Query = {
-  __typename?: 'Query'
-  /** Return a tutorial based on it's ID */
-  tutorial?: Maybe<Tutorial>
-  /** Returns a list of tutorials */
-  tutorials: Array<Maybe<Tutorial>>
-  viewer?: Maybe<User>
-}
-
-export type QueryTutorialArgs = {
-  id: Scalars['ID']
-}
-
-export type Role = 'ADMIN' | 'EDITOR_USER'
-
-/** A level task */
-export type Step = {
-  __typename?: 'Step'
-  id: Scalars['ID']
-  content: Scalars['String']
-  setup?: Maybe<Scalars['JSON']>
-  solution?: Maybe<Scalars['JSON']>
-}
-
-/** A tutorial for use in VSCode CodeRoad */
-export type Tutorial = {
-  __typename?: 'Tutorial'
-  id: Scalars['ID']
-  createdBy?: Maybe<User>
-  version: TutorialVersion
-  versions: Array<TutorialVersion>
-  summary: TutorialSummary
-}
-
-/** A tutorial for use in VSCode CodeRoad */
-export type TutorialVersionArgs = {
-  id?: Maybe<Scalars['String']>
-}
-
-/** Data for tutorial */
-export type TutorialData = {
-  __typename?: 'TutorialData'
-  config: Scalars['JSON']
-  levels: Array<Level>
-}
-
-export type TutorialProgressStatus = 'IN_PROGRESS' | 'COMPLETED' | 'SKIPPED'
-
-export type TutorialProgressType = 'LEVEL' | 'STEP' | 'TUTORIAL'
-
-export type TutorialRepoInput = {
-  uri: Scalars['String']
-  branch: Scalars['String']
-}
-
-/** Summary of tutorial used when selecting tutorial */
-export type TutorialSummary = {
-  __typename?: 'TutorialSummary'
-  title: Scalars['String']
-  description: Scalars['String']
-}
-
-/** A version of a tutorial */
-export type TutorialVersion = {
-  __typename?: 'TutorialVersion'
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdBy: User
-  updatedAt: Scalars['DateTime']
-  updatedBy: User
-  publishedAt?: Maybe<Scalars['DateTime']>
-  publishedBy?: Maybe<User>
-  data: TutorialData
-}
-
-export type UpdateTutorialProgressInput = {
-  tutorialId: Scalars['ID']
-  versionId: Scalars['ID']
-  type: TutorialProgressType
-  entityId: Scalars['ID']
-  status: TutorialProgressStatus
-}
-
-export type UpdateTutorialVersionInput = {
-  /** TODO: tutorial version type */
-  id: Scalars['ID']
-}
-
-/**
- * Users is useful for tracking completion progress
- * & credit for tutorial creation/contributions
- */
-export type User = {
-  __typename?: 'User'
-  id: Scalars['ID']
-  name?: Maybe<Scalars['String']>
-  email?: Maybe<Scalars['String']>
-  createdAt: Scalars['DateTime']
-  updatedAt: Scalars['DateTime']
-}
-
-export type ResolverTypeWrapper<T> = Promise<T> | T
-
-export type ResolverFn<TResult, TParent, TContext, TArgs> = (
-  parent: TParent,
-  args: TArgs,
-  context: TContext,
-  info: GraphQLResolveInfo,
-) => Promise<TResult> | TResult
-
-export type StitchingResolver<TResult, TParent, TContext, TArgs> = {
-  fragment: string
-  resolve: ResolverFn<TResult, TParent, TContext, TArgs>
-}
-
-export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =
-  | ResolverFn<TResult, TParent, TContext, TArgs>
-  | StitchingResolver<TResult, TParent, TContext, TArgs>
-
-export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
-  parent: TParent,
-  args: TArgs,
-  context: TContext,
-  info: GraphQLResolveInfo,
-) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>
-
-export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
-  parent: TParent,
-  args: TArgs,
-  context: TContext,
-  info: GraphQLResolveInfo,
-) => TResult | Promise<TResult>
-
-export interface SubscriptionSubscriberObject<TResult, TKey extends string, TParent, TContext, TArgs> {
-  subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>
-  resolve?: SubscriptionResolveFn<TResult, { [key in TKey]: TResult }, TContext, TArgs>
-}
-
-export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
-  subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>
-  resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>
-}
-
-export type SubscriptionObject<TResult, TKey extends string, TParent, TContext, TArgs> =
-  | SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
-  | SubscriptionResolverObject<TResult, TParent, TContext, TArgs>
-
-export type SubscriptionResolver<TResult, TKey extends string, TParent = {}, TContext = {}, TArgs = {}> =
-  | ((...args: any[]) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
-  | SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>
-
-export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
-  parent: TParent,
-  context: TContext,
-  info: GraphQLResolveInfo,
-) => Maybe<TTypes>
-
-export type NextResolverFn<T> = () => Promise<T>
-
-export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs = {}> = (
-  next: NextResolverFn<TResult>,
-  parent: TParent,
-  args: TArgs,
-  context: TContext,
-  info: GraphQLResolveInfo,
-) => TResult | Promise<TResult>
-
-/** Mapping between all available schema types and the resolvers types */
-export type ResolversTypes = {
-  Query: ResolverTypeWrapper<{}>
-  ID: ResolverTypeWrapper<Scalars['ID']>
-  Tutorial: ResolverTypeWrapper<Tutorial>
-  User: ResolverTypeWrapper<User>
-  String: ResolverTypeWrapper<Scalars['String']>
-  DateTime: ResolverTypeWrapper<Scalars['DateTime']>
-  TutorialVersion: ResolverTypeWrapper<TutorialVersion>
-  TutorialData: ResolverTypeWrapper<TutorialData>
-  JSON: ResolverTypeWrapper<Scalars['JSON']>
-  Level: ResolverTypeWrapper<Level>
-  Step: ResolverTypeWrapper<Step>
-  TutorialSummary: ResolverTypeWrapper<TutorialSummary>
-  Mutation: ResolverTypeWrapper<{}>
-  editorLoginInput: EditorLoginInput
-  Editor: Editor
-  editorLoginOutput: ResolverTypeWrapper<EditorLoginOutput>
-  updateTutorialProgressInput: UpdateTutorialProgressInput
-  TutorialProgressType: TutorialProgressType
-  TutorialProgressStatus: TutorialProgressStatus
-  Boolean: ResolverTypeWrapper<Scalars['Boolean']>
-  createTutorialVersionInput: CreateTutorialVersionInput
-  updateTutorialVersionInput: UpdateTutorialVersionInput
-  JSONObject: ResolverTypeWrapper<Scalars['JSONObject']>
-  Sha1: ResolverTypeWrapper<Scalars['Sha1']>
-  Role: Role
-  FileFormat: FileFormat
-  tutorialRepoInput: TutorialRepoInput
-  createTutorialInput: CreateTutorialInput
-  GithubUser: ResolverTypeWrapper<GithubUser>
-}
-
-/** Mapping between all available schema types and the resolvers parents */
-export type ResolversParentTypes = {
-  Query: {}
-  ID: Scalars['ID']
-  Tutorial: Tutorial
-  User: User
-  String: Scalars['String']
-  DateTime: Scalars['DateTime']
-  TutorialVersion: TutorialVersion
-  TutorialData: TutorialData
-  JSON: Scalars['JSON']
-  Level: Level
-  Step: Step
-  TutorialSummary: TutorialSummary
-  Mutation: {}
-  editorLoginInput: EditorLoginInput
-  Editor: Editor
-  editorLoginOutput: EditorLoginOutput
-  updateTutorialProgressInput: UpdateTutorialProgressInput
-  TutorialProgressType: TutorialProgressType
-  TutorialProgressStatus: TutorialProgressStatus
-  Boolean: Scalars['Boolean']
-  createTutorialVersionInput: CreateTutorialVersionInput
-  updateTutorialVersionInput: UpdateTutorialVersionInput
-  JSONObject: Scalars['JSONObject']
-  Sha1: Scalars['Sha1']
-  Role: Role
-  FileFormat: FileFormat
-  tutorialRepoInput: TutorialRepoInput
-  createTutorialInput: CreateTutorialInput
-  GithubUser: GithubUser
-}
-
-export type AuthDirectiveResolver<
-  Result,
-  Parent,
-  ContextType = any,
-  Args = { requires?: Maybe<Maybe<Role>> }
-> = DirectiveResolverFn<Result, Parent, ContextType, Args>
-
-export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['DateTime'], any> {
-  name: 'DateTime'
-}
-
-export type EditorLoginOutputResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['editorLoginOutput'] = ResolversParentTypes['editorLoginOutput']
-> = {
-  user?: Resolver<ResolversTypes['User'], ParentType, ContextType>
-  token?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-}
-
-export type GithubUserResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['GithubUser'] = ResolversParentTypes['GithubUser']
-> = {
-  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>
-  name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
-  email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
-  location?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
-  avatarUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
-}
-
-export interface JsonScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['JSON'], any> {
-  name: 'JSON'
-}
-
-export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['JSONObject'], any> {
-  name: 'JSONObject'
-}
-
-export type LevelResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['Level'] = ResolversParentTypes['Level']
-> = {
-  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>
-  title?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-  summary?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-  content?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-  setup?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>
-  steps?: Resolver<Array<ResolversTypes['Step']>, ParentType, ContextType>
-}
-
-export type MutationResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']
-> = {
-  editorLogin?: Resolver<
-    Maybe<ResolversTypes['editorLoginOutput']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationEditorLoginArgs, 'input'>
-  >
-  updateTutorialProgress?: Resolver<
-    Maybe<ResolversTypes['Boolean']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationUpdateTutorialProgressArgs, 'input'>
-  >
-  createTutorial?: Resolver<
-    Maybe<ResolversTypes['Tutorial']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationCreateTutorialArgs, 'input'>
-  >
-  createTutorialVersion?: Resolver<
-    Maybe<ResolversTypes['TutorialVersion']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationCreateTutorialVersionArgs, 'input'>
-  >
-  updateTutorialVersion?: Resolver<
-    Maybe<ResolversTypes['TutorialVersion']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationUpdateTutorialVersionArgs, 'input'>
-  >
-  publishTutorialVersion?: Resolver<
-    Maybe<ResolversTypes['Tutorial']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationPublishTutorialVersionArgs, 'tutorialId' | 'versionID'>
-  >
-  deprecateTutorialVersion?: Resolver<
-    Maybe<ResolversTypes['Boolean']>,
-    ParentType,
-    ContextType,
-    RequireFields<MutationDeprecateTutorialVersionArgs, 'tutorialId' | 'versionID'>
-  >
-}
-
-export type QueryResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']
-> = {
-  tutorial?: Resolver<
-    Maybe<ResolversTypes['Tutorial']>,
-    ParentType,
-    ContextType,
-    RequireFields<QueryTutorialArgs, 'id'>
-  >
-  tutorials?: Resolver<Array<Maybe<ResolversTypes['Tutorial']>>, ParentType, ContextType>
-  viewer?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>
-}
-
-export interface Sha1ScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['Sha1'], any> {
-  name: 'Sha1'
-}
-
-export type StepResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['Step'] = ResolversParentTypes['Step']
-> = {
-  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>
-  content?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-  setup?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>
-  solution?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>
-}
-
-export type TutorialResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['Tutorial'] = ResolversParentTypes['Tutorial']
-> = {
-  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>
-  createdBy?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>
-  version?: Resolver<ResolversTypes['TutorialVersion'], ParentType, ContextType, TutorialVersionArgs>
-  versions?: Resolver<Array<ResolversTypes['TutorialVersion']>, ParentType, ContextType>
-  summary?: Resolver<ResolversTypes['TutorialSummary'], ParentType, ContextType>
-}
-
-export type TutorialDataResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['TutorialData'] = ResolversParentTypes['TutorialData']
-> = {
-  config?: Resolver<ResolversTypes['JSON'], ParentType, ContextType>
-  levels?: Resolver<Array<ResolversTypes['Level']>, ParentType, ContextType>
-}
-
-export type TutorialSummaryResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['TutorialSummary'] = ResolversParentTypes['TutorialSummary']
-> = {
-  title?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-  description?: Resolver<ResolversTypes['String'], ParentType, ContextType>
-}
-
-export type TutorialVersionResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['TutorialVersion'] = ResolversParentTypes['TutorialVersion']
-> = {
-  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>
-  createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>
-  createdBy?: Resolver<ResolversTypes['User'], ParentType, ContextType>
-  updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>
-  updatedBy?: Resolver<ResolversTypes['User'], ParentType, ContextType>
-  publishedAt?: Resolver<Maybe<ResolversTypes['DateTime']>, ParentType, ContextType>
-  publishedBy?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>
-  data?: Resolver<ResolversTypes['TutorialData'], ParentType, ContextType>
-}
-
-export type UserResolvers<
-  ContextType = any,
-  ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']
-> = {
-  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>
-  name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
-  email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
-  createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>
-  updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>
-}
-
-export type Resolvers<ContextType = any> = {
-  DateTime?: GraphQLScalarType
-  editorLoginOutput?: EditorLoginOutputResolvers<ContextType>
-  GithubUser?: GithubUserResolvers<ContextType>
-  JSON?: GraphQLScalarType
-  JSONObject?: GraphQLScalarType
-  Level?: LevelResolvers<ContextType>
-  Mutation?: MutationResolvers<ContextType>
-  Query?: QueryResolvers<ContextType>
-  Sha1?: GraphQLScalarType
-  Step?: StepResolvers<ContextType>
-  Tutorial?: TutorialResolvers<ContextType>
-  TutorialData?: TutorialDataResolvers<ContextType>
-  TutorialSummary?: TutorialSummaryResolvers<ContextType>
-  TutorialVersion?: TutorialVersionResolvers<ContextType>
-  User?: UserResolvers<ContextType>
-}
-
-/**
- * @deprecated
- * Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config.
- */
-export type IResolvers<ContextType = any> = Resolvers<ContextType>
-export type DirectiveResolvers<ContextType = any> = {
-  auth?: AuthDirectiveResolver<any, any, ContextType>
-}
-
-/**
- * @deprecated
- * Use "DirectiveResolvers" root object instead. If you wish to get "IDirectiveResolvers", add "typesPrefix: I" to your config.
- */
-export type IDirectiveResolvers<ContextType = any> = DirectiveResolvers<ContextType>
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 1e65f7a9..6436a047 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -1,4 +1,4 @@
-import * as G from './graphql'
+import * as TT from './tutorial'
 
 export type ProgressStatus = 'ACTIVE' | 'COMPLETE' | 'INCOMPLETE'
 
@@ -51,7 +51,7 @@ export interface TestStatus {
 export interface MachineContext {
   env: Environment
   error: ErrorMessage | null
-  tutorial: G.Tutorial | null
+  tutorial: TT.Tutorial | null
   position: Position
   progress: Progress
   processes: ProcessEvent[]
@@ -69,16 +69,12 @@ export interface MachineStateSchema {
     Setup: {
       states: {
         Startup: {}
-        Authenticate: {}
         Error: {}
         LoadStoredTutorial: {}
         Start: {}
         CheckEmptyWorkspace: {}
         NonEmptyWorkspace: {}
         SelectTutorial: {}
-        LoadTutorialSummary: {}
-        Summary: {}
-        LoadTutorialData: {}
         SetupNewTutorial: {}
       }
     }
@@ -126,26 +122,3 @@ export interface ProcessEvent {
   description: string
   status: 'RUNNING' | 'SUCCESS' | 'FAIL' | 'ERROR'
 }
-
-export interface StepActions {
-  id: string
-  commands: string[]
-  commits: string[]
-  files: string[]
-  watchers: string[]
-}
-
-export interface TutorialTestRunner {
-  command: string
-  fileFormats: string[]
-}
-
-export interface TutorialRepo {
-  uri: string
-  branch: string
-}
-
-export interface TutorialConfig {
-  testRunner: TutorialTestRunner
-  repo: TutorialRepo
-}
diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts
new file mode 100644
index 00000000..313c386e
--- /dev/null
+++ b/typings/tutorial.d.ts
@@ -0,0 +1,59 @@
+export type Maybe<T> = T | null
+
+export type TutorialConfig = {
+  testRunner: TutorialTestRunner
+  repo: TutorialRepo
+}
+
+/** Logical groupings of tasks */
+export type Level = {
+  id: string
+  title: string
+  /** A summary of the level */
+  summary: string
+  /** The lesson content of the level, parsed as markdown */
+  content: string
+  /** Actions run on level start up for configuring setup */
+  setup?: Maybe<StepActions>
+  /** A set of tasks for users linked to unit tests */
+  steps: Array<Step>
+}
+
+/** A level task */
+export type Step = {
+  id: string
+  content: string
+  setup: StepActions
+  solution: Maybe<StepActions>
+}
+
+/** A tutorial for use in VSCode CodeRoad */
+export type Tutorial = {
+  id: string
+  version: string
+  summary: TutorialSummary
+  config: TutorialConfig
+  levels: Array<Level>
+}
+
+/** Summary of tutorial used when selecting tutorial */
+export type TutorialSummary = {
+  title: string
+  description: string
+}
+
+export type StepActions = {
+  commands: string[]
+  commits: string[]
+  files: string[]
+  watchers: string[]
+}
+
+export interface TutorialTestRunner {
+  command: string
+}
+
+export interface TutorialRepo {
+  uri: string
+  branch: string
+}
diff --git a/web-app/.eslintrc.js b/web-app/.eslintrc.js
index 511c5e8c..e18d5f24 100644
--- a/web-app/.eslintrc.js
+++ b/web-app/.eslintrc.js
@@ -16,7 +16,6 @@ module.exports = {
   rules: {
     // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
     // e.g.
-    'arrow-parens': 'on',
     'comma-dangles': 0,
     'global-require': 0,
     'import/no-extraneous-dependencies': 0,
diff --git a/web-app/package-lock.json b/web-app/package-lock.json
index 38988971..bba82400 100644
--- a/web-app/package-lock.json
+++ b/web-app/package-lock.json
@@ -14,9 +14,9 @@
       }
     },
     "@alifd/next": {
-      "version": "1.19.19",
-      "resolved": "https://registry.npmjs.org/@alifd/next/-/next-1.19.19.tgz",
-      "integrity": "sha512-rFi+2LsDkkHs8ssb3FgLSzJjDyN0A8GW1GIy3EzE5eYmg/+X5wQeNeHZX2UjgX+1i4aZK0xZu9SaHh8xf63+JA==",
+      "version": "1.19.21",
+      "resolved": "https://registry.npmjs.org/@alifd/next/-/next-1.19.21.tgz",
+      "integrity": "sha512-ZX8Im8EQnib014fidil113fjpxJtUjSRSr8k2u4/o4x7c0L/FJDe6UsuiX+yz0TqBB9bmxLuWI5SgG8wd5qwaQ==",
       "requires": {
         "@alifd/field": "~1.3.3",
         "@alifd/validate": "~1.1.4",
@@ -42,26 +42,6 @@
       "resolved": "https://registry.npmjs.org/@alifd/validate/-/validate-1.1.5.tgz",
       "integrity": "sha512-uinc+qLTrgKtGzXIkbit8spHLe17fzxAeXYe6ND5AgqqnhvZ4IAmZEClrE8HhaZ4QfnsaYR9MDx8CaagpQ+Y3A=="
     },
-    "@apollo/react-common": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.3.tgz",
-      "integrity": "sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg==",
-      "requires": {
-        "ts-invariant": "^0.4.4",
-        "tslib": "^1.10.0"
-      }
-    },
-    "@apollo/react-hooks": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-3.1.3.tgz",
-      "integrity": "sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA==",
-      "requires": {
-        "@apollo/react-common": "^3.1.3",
-        "@wry/equality": "^0.1.9",
-        "ts-invariant": "^0.4.4",
-        "tslib": "^1.10.0"
-      }
-    },
     "@babel/code-frame": {
       "version": "7.5.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
@@ -4829,9 +4809,10 @@
       "dev": true
     },
     "@types/node": {
-      "version": "13.9.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.5.tgz",
-      "integrity": "sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw=="
+      "version": "13.9.8",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.8.tgz",
+      "integrity": "sha512-1WgO8hsyHynlx7nhP1kr0OFzsgKz5XDQL+Lfc3b1Q3qIln/n8cKD4m09NJ0+P1Rq7Zgnc7N0+SsMnoD1rEb0kA==",
+      "dev": true
     },
     "@types/npmlog": {
       "version": "4.1.2",
@@ -4873,9 +4854,9 @@
       }
     },
     "@types/react": {
-      "version": "16.9.26",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.26.tgz",
-      "integrity": "sha512-dGuSM+B0Pq1MKXYUMlUQWeS6Jj9IhSAUf9v8Ikaimj+YhkBcQrihWBkmyEhK/1fzkJTwZQkhZp5YhmWa2CH+Rw==",
+      "version": "16.9.29",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.29.tgz",
+      "integrity": "sha512-aE5sV9XVqKvIR8Lqa73hXvlqBzz5hBG0jtV9jZ1uuEWRmW8KN/mdQQmsYlPx6z/b2xa8zR3jtk7WoT+2/m4suA==",
       "dev": true,
       "requires": {
         "@types/prop-types": "*",
@@ -4964,31 +4945,26 @@
       "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==",
       "dev": true
     },
-    "@types/zen-observable": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
-      "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg=="
-    },
     "@typescript-eslint/eslint-plugin": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.25.0.tgz",
-      "integrity": "sha512-W2YyMtjmlrOjtXc+FtTelVs9OhuR6OlYc4XKIslJ8PUJOqgYYAPRJhAqkYRQo3G4sjvG8jSodsNycEn4W2gHUw==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.26.0.tgz",
+      "integrity": "sha512-4yUnLv40bzfzsXcTAtZyTjbiGUXMrcIJcIMioI22tSOyAxpdXiZ4r7YQUU8Jj6XXrLz9d5aMHPQf5JFR7h27Nw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "2.25.0",
+        "@typescript-eslint/experimental-utils": "2.26.0",
         "functional-red-black-tree": "^1.0.1",
         "regexpp": "^3.0.0",
         "tsutils": "^3.17.1"
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.25.0.tgz",
-      "integrity": "sha512-0IZ4ZR5QkFYbaJk+8eJ2kYeA+1tzOE1sBjbwwtSV85oNWYUBep+EyhlZ7DLUCyhMUGuJpcCCFL0fDtYAP1zMZw==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz",
+      "integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/typescript-estree": "2.25.0",
+        "@typescript-eslint/typescript-estree": "2.26.0",
         "eslint-scope": "^5.0.0",
         "eslint-utils": "^2.0.0"
       },
@@ -5015,21 +4991,21 @@
       }
     },
     "@typescript-eslint/parser": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.25.0.tgz",
-      "integrity": "sha512-mccBLaBSpNVgp191CP5W+8U1crTyXsRziWliCqzj02kpxdjKMvFHGJbK33NroquH3zB/gZ8H511HEsJBa2fNEg==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.26.0.tgz",
+      "integrity": "sha512-+Xj5fucDtdKEVGSh9353wcnseMRkPpEAOY96EEenN7kJVrLqy/EVwtIh3mxcUz8lsFXW1mT5nN5vvEam/a5HiQ==",
       "dev": true,
       "requires": {
         "@types/eslint-visitor-keys": "^1.0.0",
-        "@typescript-eslint/experimental-utils": "2.25.0",
-        "@typescript-eslint/typescript-estree": "2.25.0",
+        "@typescript-eslint/experimental-utils": "2.26.0",
+        "@typescript-eslint/typescript-estree": "2.26.0",
         "eslint-visitor-keys": "^1.1.0"
       }
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "2.25.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.25.0.tgz",
-      "integrity": "sha512-VUksmx5lDxSi6GfmwSK7SSoIKSw9anukWWNitQPqt58LuYrKalzsgeuignbqnB+rK/xxGlSsCy8lYnwFfB6YJg==",
+      "version": "2.26.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz",
+      "integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==",
       "dev": true,
       "requires": {
         "debug": "^4.1.1",
@@ -5239,23 +5215,6 @@
         "@xtuc/long": "4.2.2"
       }
     },
-    "@wry/context": {
-      "version": "0.4.4",
-      "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz",
-      "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==",
-      "requires": {
-        "@types/node": ">=6",
-        "tslib": "^1.9.3"
-      }
-    },
-    "@wry/equality": {
-      "version": "0.1.9",
-      "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.9.tgz",
-      "integrity": "sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==",
-      "requires": {
-        "tslib": "^1.9.3"
-      }
-    },
     "@xtuc/ieee754": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -5593,110 +5552,6 @@
         }
       }
     },
-    "apollo-boost": {
-      "version": "0.4.7",
-      "resolved": "https://registry.npmjs.org/apollo-boost/-/apollo-boost-0.4.7.tgz",
-      "integrity": "sha512-jfc3aqO0vpCV+W662EOG5gq4AH94yIsvSgAUuDvS3o/Z+8Joqn4zGC9CgLCDHusK30mFgtsEgwEe0pZoedohsQ==",
-      "requires": {
-        "apollo-cache": "^1.3.4",
-        "apollo-cache-inmemory": "^1.6.5",
-        "apollo-client": "^2.6.7",
-        "apollo-link": "^1.0.6",
-        "apollo-link-error": "^1.0.3",
-        "apollo-link-http": "^1.3.1",
-        "graphql-tag": "^2.4.2",
-        "ts-invariant": "^0.4.0",
-        "tslib": "^1.10.0"
-      }
-    },
-    "apollo-cache": {
-      "version": "1.3.4",
-      "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.4.tgz",
-      "integrity": "sha512-7X5aGbqaOWYG+SSkCzJNHTz2ZKDcyRwtmvW4mGVLRqdQs+HxfXS4dUS2CcwrAj449se6tZ6NLUMnjko4KMt3KA==",
-      "requires": {
-        "apollo-utilities": "^1.3.3",
-        "tslib": "^1.10.0"
-      }
-    },
-    "apollo-cache-inmemory": {
-      "version": "1.6.5",
-      "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.5.tgz",
-      "integrity": "sha512-koB76JUDJaycfejHmrXBbWIN9pRKM0Z9CJGQcBzIOtmte1JhEBSuzsOUu7NQgiXKYI4iGoMREcnaWffsosZynA==",
-      "requires": {
-        "apollo-cache": "^1.3.4",
-        "apollo-utilities": "^1.3.3",
-        "optimism": "^0.10.0",
-        "ts-invariant": "^0.4.0",
-        "tslib": "^1.10.0"
-      }
-    },
-    "apollo-client": {
-      "version": "2.6.8",
-      "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.8.tgz",
-      "integrity": "sha512-0zvJtAcONiozpa5z5zgou83iEKkBaXhhSSXJebFHRXs100SecDojyUWKjwTtBPn9HbM6o5xrvC5mo9VQ5fgAjw==",
-      "requires": {
-        "@types/zen-observable": "^0.8.0",
-        "apollo-cache": "1.3.4",
-        "apollo-link": "^1.0.0",
-        "apollo-utilities": "1.3.3",
-        "symbol-observable": "^1.0.2",
-        "ts-invariant": "^0.4.0",
-        "tslib": "^1.10.0",
-        "zen-observable": "^0.8.0"
-      }
-    },
-    "apollo-link": {
-      "version": "1.2.13",
-      "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.13.tgz",
-      "integrity": "sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw==",
-      "requires": {
-        "apollo-utilities": "^1.3.0",
-        "ts-invariant": "^0.4.0",
-        "tslib": "^1.9.3",
-        "zen-observable-ts": "^0.8.20"
-      }
-    },
-    "apollo-link-error": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/apollo-link-error/-/apollo-link-error-1.1.12.tgz",
-      "integrity": "sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA==",
-      "requires": {
-        "apollo-link": "^1.2.13",
-        "apollo-link-http-common": "^0.2.15",
-        "tslib": "^1.9.3"
-      }
-    },
-    "apollo-link-http": {
-      "version": "1.5.16",
-      "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.16.tgz",
-      "integrity": "sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw==",
-      "requires": {
-        "apollo-link": "^1.2.13",
-        "apollo-link-http-common": "^0.2.15",
-        "tslib": "^1.9.3"
-      }
-    },
-    "apollo-link-http-common": {
-      "version": "0.2.15",
-      "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz",
-      "integrity": "sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg==",
-      "requires": {
-        "apollo-link": "^1.2.13",
-        "ts-invariant": "^0.4.0",
-        "tslib": "^1.9.3"
-      }
-    },
-    "apollo-utilities": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.3.tgz",
-      "integrity": "sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw==",
-      "requires": {
-        "@wry/equality": "^0.1.2",
-        "fast-json-stable-stringify": "^2.0.0",
-        "ts-invariant": "^0.4.0",
-        "tslib": "^1.10.0"
-      }
-    },
     "app-root-dir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz",
@@ -10942,7 +10797,8 @@
     "fast-json-stable-stringify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
     },
     "fast-levenshtein": {
       "version": "2.0.6",
@@ -12376,11 +12232,6 @@
         "iterall": "^1.2.2"
       }
     },
-    "graphql-tag": {
-      "version": "2.10.1",
-      "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.1.tgz",
-      "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg=="
-    },
     "growly": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
@@ -17054,14 +16905,6 @@
         "is-wsl": "^1.1.0"
       }
     },
-    "optimism": {
-      "version": "0.10.3",
-      "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz",
-      "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==",
-      "requires": {
-        "@wry/context": "^0.4.0"
-      }
-    },
     "optimize-css-assets-webpack-plugin": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz",
@@ -23281,11 +23124,6 @@
         }
       }
     },
-    "symbol-observable": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
-      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
-    },
     "symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -23897,14 +23735,6 @@
       "integrity": "sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg==",
       "dev": true
     },
-    "ts-invariant": {
-      "version": "0.4.4",
-      "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
-      "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
-      "requires": {
-        "tslib": "^1.9.3"
-      }
-    },
     "ts-pnp": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.6.tgz",
@@ -25379,20 +25209,6 @@
         "camelcase": "^5.0.0",
         "decamelize": "^1.2.0"
       }
-    },
-    "zen-observable": {
-      "version": "0.8.15",
-      "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
-      "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
-    },
-    "zen-observable-ts": {
-      "version": "0.8.20",
-      "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz",
-      "integrity": "sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA==",
-      "requires": {
-        "tslib": "^1.9.3",
-        "zen-observable": "^0.8.0"
-      }
     }
   }
 }
diff --git a/web-app/package.json b/web-app/package.json
index 25a0bfbd..e378d680 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -25,12 +25,10 @@
     "extends": "react-app"
   },
   "dependencies": {
-    "@alifd/next": "^1.19.19",
+    "@alifd/next": "^1.19.21",
     "@alifd/theme-4": "^0.2.3",
-    "@apollo/react-hooks": "^3.1.3",
     "@emotion/core": "^10.0.28",
     "@sentry/browser": "^5.15.4",
-    "apollo-boost": "^0.4.7",
     "graphql": "^14.6.0",
     "markdown-it": "^10.0.0",
     "markdown-it-emoji": "^1.4.0",
@@ -56,13 +54,13 @@
     "@types/highlight.js": "^9.12.3",
     "@types/jest": "^25.1.4",
     "@types/markdown-it": "0.0.9",
-    "@types/node": "^13.9.5",
+    "@types/node": "^13.9.8",
     "@types/prismjs": "^1.16.0",
-    "@types/react": "^16.9.26",
+    "@types/react": "^16.9.29",
     "@types/react-addons-css-transition-group": "^15.0.5",
     "@types/react-dom": "^16.9.5",
-    "@typescript-eslint/eslint-plugin": "^2.25.0",
-    "@typescript-eslint/parser": "^2.25.0",
+    "@typescript-eslint/eslint-plugin": "^2.26.0",
+    "@typescript-eslint/parser": "^2.26.0",
     "babel-loader": "8.0.5",
     "babel-plugin-import": "^1.12.1",
     "customize-cra": "^0.9.1",
diff --git a/web-app/src/App.tsx b/web-app/src/App.tsx
index 59df8f3f..60b46832 100644
--- a/web-app/src/App.tsx
+++ b/web-app/src/App.tsx
@@ -1,14 +1,10 @@
-import { ApolloProvider } from '@apollo/react-hooks'
 import * as React from 'react'
 import ErrorBoundary from './components/ErrorBoundary'
 import Routes from './Routes'
-import client from './services/apollo'
 
 const App = () => (
   <ErrorBoundary>
-    <ApolloProvider client={client}>
-      <Routes />
-    </ApolloProvider>
+    <Routes />
   </ErrorBoundary>
 )
 
diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx
index 90db8f2a..6cb4bc3a 100644
--- a/web-app/src/Routes.tsx
+++ b/web-app/src/Routes.tsx
@@ -4,7 +4,6 @@ import Workspace from './components/Workspace'
 import LoadingPage from './containers/Loading'
 import StartPage from './containers/Start'
 import SelectTutorialPage from './containers/SelectTutorial'
-import OverviewPage from './containers/Overview'
 import CompletedPage from './containers/Tutorial/CompletedPage'
 import LevelSummaryPage from './containers/Tutorial/LevelPage'
 import SelectEmptyWorkspace from './containers/Check/SelectWorkspace'
@@ -33,9 +32,6 @@ const Routes = () => {
         <Route path="Setup.SelectTutorial">
           <SelectTutorialPage send={send} context={context} />
         </Route>
-        <Route path="Setup.Summary">
-          <OverviewPage send={send} context={context} />
-        </Route>
         <Route path="Setup.SetupNewTutorial">
           <LoadingPage text="Configuring tutorial..." context={context} />
         </Route>
diff --git a/web-app/src/components/Error/index.tsx b/web-app/src/components/Error/index.tsx
index 42b9c029..2bb26eef 100644
--- a/web-app/src/components/Error/index.tsx
+++ b/web-app/src/components/Error/index.tsx
@@ -1,5 +1,3 @@
-import { ApolloError } from 'apollo-boost'
-import { GraphQLError } from 'graphql'
 import * as React from 'react'
 import { css, jsx } from '@emotion/core'
 import onError from '../../services/sentry/onError'
@@ -15,7 +13,7 @@ const styles = {
 }
 
 interface Props {
-  error?: ApolloError
+  error?: Error
 }
 
 const ErrorView = ({ error }: Props) => {
@@ -34,25 +32,7 @@ const ErrorView = ({ error }: Props) => {
   return (
     <div css={styles.container}>
       <h1>Error</h1>
-      {error.graphQLErrors && (
-        <div>
-          {error.graphQLErrors.map(({ message, locations, path }: GraphQLError, index: number) => (
-            <h5 key={index}>
-              <b>[GraphQL error]:</b> Message: {message}, Location: {locations}, Path: {path}
-            </h5>
-          ))}
-        </div>
-      )}
-      {error.networkError && (
-        <h5>
-          <b>[Network error]:</b> {error.networkError.message}
-        </h5>
-      )}
-      {error.extraInfo && (
-        <p>
-          <b>[Extra info]:</b> {JSON.stringify(error.extraInfo)}
-        </p>
-      )}
+      <div>{JSON.stringify(error)}</div>
     </div>
   )
 }
diff --git a/web-app/src/containers/Overview/OverviewPage.tsx b/web-app/src/components/TutorialOverview/index.tsx
similarity index 53%
rename from web-app/src/containers/Overview/OverviewPage.tsx
rename to web-app/src/components/TutorialOverview/index.tsx
index 662907d4..981583df 100644
--- a/web-app/src/containers/Overview/OverviewPage.tsx
+++ b/web-app/src/components/TutorialOverview/index.tsx
@@ -1,8 +1,8 @@
 import * as React from 'react'
-import * as G from 'typings/graphql'
-import moment from 'moment'
-import Button from '../../components/Button'
-import Markdown from '../../components/Markdown'
+import * as TT from 'typings/tutorial'
+// import moment from 'moment'
+import Button from '../Button'
+import Markdown from '../Markdown'
 import { Breadcrumb } from '@alifd/next'
 
 const footerHeight = '3rem'
@@ -69,59 +69,57 @@ const styles = {
 }
 
 interface Props {
-  title: string
-  description: string
-  createdBy: G.User
-  updatedAt: string
-  levels: G.Level[]
+  tutorial: TT.Tutorial
   onNext(): void
-  onBack(): void
+  onClear(): void
 }
 
-const Summary = (props: Props) => (
-  <div css={styles.page}>
-    <div css={styles.content}>
-      <div css={styles.header}>
-        <div css={styles.nav}>
-          <Breadcrumb separator="/">
-            <Breadcrumb.Item>
-              <div css={styles.navLink} onClick={props.onBack}>
-                &lt; Back to Tutorials
-              </div>
-            </Breadcrumb.Item>
-          </Breadcrumb>
-        </div>
-        <h1 css={styles.title}>{props.title}</h1>
-        <h3>{props.description}</h3>
-        <h5 css={styles.meta}>
+const Summary = (props: Props) => {
+  return (
+    <div css={styles.page}>
+      <div css={styles.content}>
+        <div css={styles.header}>
+          <div css={styles.nav}>
+            <Breadcrumb separator="/">
+              <Breadcrumb.Item>
+                <div css={styles.navLink} onClick={props.onClear}>
+                  &lt; Back to Tutorials
+                </div>
+              </Breadcrumb.Item>
+            </Breadcrumb>
+          </div>
+          <h1 css={styles.title}>{props.tutorial.summary.title}</h1>
+          <h3>{props.tutorial.summary.description}</h3>
+          {/* <h5 css={styles.meta}>
           <div css={{ marginRight: '2rem' }}>Created by {props.createdBy.name}</div>
           <div>Last updated {moment(props.updatedAt).format('M/YYYY')}</div>
-        </h5>
-      </div>
-      <div>
-        <div css={styles.levelList}>
-          <h2>Content</h2>
-          {props.levels.map((level: G.Level, index: number) => (
-            <div key={index}>
-              <h3>
-                {index + 1}. {level.title}
-              </h3>
-              <div css={styles.levelSummary}>
-                <Markdown>{level.summary}</Markdown>
+        </h5> */}
+        </div>
+        <div>
+          <div css={styles.levelList}>
+            <h2>Content</h2>
+            {props.tutorial.levels.map((level: TT.Level, index: number) => (
+              <div key={index}>
+                <h3>
+                  {index + 1}. {level.title}
+                </h3>
+                <div css={styles.levelSummary}>
+                  <Markdown>{level.summary}</Markdown>
+                </div>
               </div>
-            </div>
-          ))}
+            ))}
+          </div>
         </div>
       </div>
-    </div>
 
-    <div css={styles.footer}>
-      {/* TODO Add back button */}
-      <Button type="primary" onClick={props.onNext}>
-        Start
-      </Button>
+      <div css={styles.footer}>
+        {/* TODO Add back button */}
+        <Button type="primary" onClick={props.onNext}>
+          Start
+        </Button>
+      </div>
     </div>
-  </div>
-)
+  )
+}
 
 export default Summary
diff --git a/web-app/src/containers/Loading/index.tsx b/web-app/src/containers/Loading/index.tsx
index b11981ec..a57079bd 100644
--- a/web-app/src/containers/Loading/index.tsx
+++ b/web-app/src/containers/Loading/index.tsx
@@ -13,10 +13,10 @@ const styles = {
   page: {
     position: 'relative' as 'relative',
     display: 'flex',
-    flexDirection: 'column' as 'column',
     alignItems: 'center',
     justifyContent: 'center',
     height: '100%',
+    width: '100%',
   },
 }
 
diff --git a/web-app/src/containers/Overview/index.tsx b/web-app/src/containers/Overview/index.tsx
deleted file mode 100644
index 503e345b..00000000
--- a/web-app/src/containers/Overview/index.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useQuery } from '@apollo/react-hooks'
-import * as React from 'react'
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
-import ErrorView from '../../components/Error'
-import queryTutorial from '../../services/apollo/queries/tutorial'
-import OverviewPage from './OverviewPage'
-import LoadingPage from '../Loading'
-
-interface PageProps {
-  context: CR.MachineContext
-  send(action: CR.Action): void
-}
-
-interface TutorialData {
-  tutorial: G.Tutorial
-}
-
-interface TutorialDataVariables {
-  tutorialId: string
-  // version: string
-}
-
-const Overview = (props: PageProps) => {
-  const { tutorial } = props.context
-
-  if (!tutorial) {
-    throw new Error('Tutorial not found in summary page')
-  }
-  const { loading, error, data } = useQuery<TutorialData, TutorialDataVariables>(queryTutorial, {
-    fetchPolicy: 'no-cache', // to ensure latest
-    variables: {
-      tutorialId: tutorial.id,
-      // version: tutorial.version.version, // TODO: re-enable latest
-    },
-  })
-
-  if (loading) {
-    return <LoadingPage text="Loading Summary..." context={props.context} />
-  }
-
-  if (error) {
-    return <ErrorView error={error} />
-  }
-
-  if (!data) {
-    return null
-  }
-
-  const onNext = () =>
-    props.send({
-      type: 'TUTORIAL_START',
-      payload: {
-        tutorial: data.tutorial,
-      },
-    })
-
-  const onBack = () => props.send({ type: 'BACK' })
-
-  const { title, description } = data.tutorial.summary
-  const { createdBy, updatedAt, data: tutorialData } = data.tutorial.version
-  const { levels } = tutorialData
-
-  return (
-    <OverviewPage
-      title={title}
-      description={description}
-      createdBy={createdBy}
-      updatedAt={updatedAt}
-      levels={levels}
-      onNext={onNext}
-      onBack={onBack}
-    />
-  )
-}
-
-export default Overview
diff --git a/web-app/src/containers/SelectTutorial/LoadTutorialSummary.tsx b/web-app/src/containers/SelectTutorial/LoadTutorialSummary.tsx
new file mode 100644
index 00000000..898deceb
--- /dev/null
+++ b/web-app/src/containers/SelectTutorial/LoadTutorialSummary.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react'
+import useFetch from '../../services/hooks/useFetch'
+import * as TT from 'typings/tutorial'
+import TutorialOverview from '../../components/TutorialOverview'
+import Loading from '../Loading'
+
+interface Props {
+  url: string
+  send: any
+  onClear(): void
+}
+
+const LoadTutorialSummary = (props: Props) => {
+  const { data, error, loading } = useFetch<TT.Tutorial>(props.url)
+  if (loading) {
+    return <Loading text="Loading tutorial summary..." />
+  }
+  // TODO: improve error handling
+  if (error) {
+    return <div>{JSON.stringify(error)}</div>
+  }
+  if (!data) {
+    return <div>No data returned for tutorial</div>
+  }
+  const onNext = () => {
+    props.send({
+      type: 'TUTORIAL_START',
+      payload: {
+        tutorial: data,
+      },
+    })
+  }
+  return <TutorialOverview onNext={onNext} tutorial={data} onClear={props.onClear} />
+}
+
+export default LoadTutorialSummary
diff --git a/web-app/src/containers/SelectTutorial/SelectTutorial.tsx b/web-app/src/containers/SelectTutorial/SelectTutorial.tsx
deleted file mode 100644
index 55566abc..00000000
--- a/web-app/src/containers/SelectTutorial/SelectTutorial.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as React from 'react'
-import * as T from 'typings'
-import * as G from 'typings/graphql'
-import { css, jsx } from '@emotion/core'
-import TutorialItem from './TutorialItem'
-
-const styles = {
-  page: {
-    position: 'relative' as 'relative',
-    width: '100%',
-  },
-  banner: {
-    minHeight: '3rem',
-    fontSize: '1rem',
-    padding: '1rem',
-  },
-}
-
-interface Props {
-  send(action: T.Action): void
-  tutorialList: G.Tutorial[]
-}
-
-const SelectTutorial = (props: Props) => {
-  const onSelect = (tutorial: G.Tutorial) => {
-    props.send({
-      type: 'SELECT_TUTORIAL',
-      payload: {
-        tutorial,
-      },
-    })
-  }
-  return (
-    <div css={styles.page}>
-      <div css={styles.banner}>
-        <span>Select a tutorial to launch in this workspace:</span>
-      </div>
-      <div>
-        {props.tutorialList.map((tutorial: G.Tutorial) => (
-          <TutorialItem
-            key={tutorial.id}
-            onSelect={() => onSelect(tutorial)}
-            title={tutorial.summary.title || ''}
-            description={tutorial.summary.description || ''}
-            createdBy={tutorial.createdBy}
-          />
-        ))}
-      </div>
-    </div>
-  )
-}
-
-export default SelectTutorial
diff --git a/web-app/src/containers/SelectTutorial/SelectTutorialForm.tsx b/web-app/src/containers/SelectTutorial/SelectTutorialForm.tsx
new file mode 100644
index 00000000..e938954d
--- /dev/null
+++ b/web-app/src/containers/SelectTutorial/SelectTutorialForm.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react'
+import useFetch from '../../services/hooks/useFetch'
+import { Form, Select } from '@alifd/next'
+import { TUTORIAL_URL } from '../../environment'
+
+const FormItem = Form.Item
+const Option = Select.Option
+
+const styles = {
+  formWrapper: {
+    padding: '1rem',
+    width: '100%',
+    height: 'auto',
+  },
+}
+
+type TutorialList = Array<{ id: string; title: string; configUrl: string }>
+
+interface Props {
+  onUrlChange(url: string): void
+}
+
+const SelectTutorialForm = (props: Props) => {
+  // load tutorial from a path to a tutorial list json
+  const { data, error, loading } = useFetch<TutorialList>(TUTORIAL_URL)
+  // TODO: display errors
+  const selectState = loading ? 'loading' : error || !data ? 'error' : undefined
+  return (
+    <div css={styles.formWrapper}>
+      <Form style={{ maxWidth: '500px' }}>
+        <FormItem label="Select Tutorial:">
+          <Select onChange={props.onUrlChange} style={{ width: '100%' }} placeholder="Tutorials..." state={selectState}>
+            {data &&
+              data.map((tutorial) => (
+                <Option key={tutorial.id} value={tutorial.configUrl}>
+                  {tutorial.title}
+                </Option>
+              ))}
+          </Select>
+        </FormItem>
+      </Form>
+    </div>
+  )
+}
+
+export default SelectTutorialForm
diff --git a/web-app/src/containers/SelectTutorial/TutorialItem.tsx b/web-app/src/containers/SelectTutorial/TutorialItem.tsx
deleted file mode 100644
index 898ac09f..00000000
--- a/web-app/src/containers/SelectTutorial/TutorialItem.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import * as React from 'react'
-import * as G from 'typings/graphql'
-import { css, jsx } from '@emotion/core'
-import Card from '../../components/Card'
-import Tag from '../../components/Tag'
-
-const styles = {
-  card: {
-    cursor: 'pointer',
-    display: 'inline-flex' as 'inline-flex',
-    flexDirection: 'row' as 'row',
-    minWidth: 500,
-  },
-  left: {
-    width: 80,
-    display: 'flex' as 'flex',
-    alignItems: 'center' as 'center',
-  },
-  right: {
-    flex: '1',
-    display: 'flex' as 'flex',
-    flexDirection: 'column' as 'column',
-  },
-  title: {
-    margin: 0,
-  },
-  author: {
-    margin: '0 0 2px 0',
-    color: 'grey',
-  },
-  tags: {
-    display: 'flex' as 'flex',
-    alignItems: 'center' as 'center',
-    padding: '2px',
-  },
-  languages: {
-    display: 'flex' as 'flex',
-    flexDirection: 'row' as 'row',
-    alignItems: 'center' as 'center',
-    justifyContent: 'flex-end' as 'flex-end',
-    width: '100%',
-  },
-  languageIcon: {
-    width: 30,
-    height: 30,
-  },
-}
-
-interface Props {
-  title: string
-  description: string
-  createdBy?: G.User | null
-  onSelect(): void
-}
-
-// icons from https://konpa.github.io/devicon/
-const LanguageIcon = () => (
-  <svg viewBox="0 0 128 128" css={styles.languageIcon}>
-    <path fill="#F0DB4F" d="M1.408 1.408h125.184v125.185h-125.184z"></path>
-    <path
-      fill="#323330"
-      d="M116.347 96.736c-.917-5.711-4.641-10.508-15.672-14.981-3.832-1.761-8.104-3.022-9.377-5.926-.452-1.69-.512-2.642-.226-3.665.821-3.32 4.784-4.355 7.925-3.403 2.023.678 3.938 2.237 5.093 4.724 5.402-3.498 5.391-3.475 9.163-5.879-1.381-2.141-2.118-3.129-3.022-4.045-3.249-3.629-7.676-5.498-14.756-5.355l-3.688.477c-3.534.893-6.902 2.748-8.877 5.235-5.926 6.724-4.236 18.492 2.975 23.335 7.104 5.332 17.54 6.545 18.873 11.531 1.297 6.104-4.486 8.08-10.234 7.378-4.236-.881-6.592-3.034-9.139-6.949-4.688 2.713-4.688 2.713-9.508 5.485 1.143 2.499 2.344 3.63 4.26 5.795 9.068 9.198 31.76 8.746 35.83-5.176.165-.478 1.261-3.666.38-8.581zm-46.885-37.793h-11.709l-.048 30.272c0 6.438.333 12.34-.714 14.149-1.713 3.558-6.152 3.117-8.175 2.427-2.059-1.012-3.106-2.451-4.319-4.485-.333-.584-.583-1.036-.667-1.071l-9.52 5.83c1.583 3.249 3.915 6.069 6.902 7.901 4.462 2.678 10.459 3.499 16.731 2.059 4.082-1.189 7.604-3.652 9.448-7.401 2.666-4.915 2.094-10.864 2.07-17.444.06-10.735.001-21.468.001-32.237z"
-    ></path>
-  </svg>
-)
-
-const TutorialItem = (props: Props) => (
-  <Card onClick={props.onSelect}>
-    <div style={styles.card}>
-      <div css={styles.left}>
-        <img src="https://via.placeholder.com/75/75" height="75px" width="75px" />
-      </div>
-      <div css={styles.right}>
-        <h2 css={styles.title}>{props.title}</h2>
-        {props.createdBy && <h3 css={styles.author}>{props.createdBy.name}</h3>}
-        <div css={styles.tags}>
-          <Tag>javascript</Tag>
-        </div>
-      </div>
-    </div>
-  </Card>
-)
-
-export default TutorialItem
diff --git a/web-app/src/containers/SelectTutorial/index.tsx b/web-app/src/containers/SelectTutorial/index.tsx
index 9265d1c0..a649f2da 100644
--- a/web-app/src/containers/SelectTutorial/index.tsx
+++ b/web-app/src/containers/SelectTutorial/index.tsx
@@ -1,39 +1,31 @@
-import { useQuery } from '@apollo/react-hooks'
 import * as React from 'react'
-import * as T from 'typings'
-import * as G from 'typings/graphql'
-import ErrorView from '../../components/Error'
-import queryTutorials from '../../services/apollo/queries/tutorials'
-import LoadingPage from '../Loading'
-import SelectTutorial from './SelectTutorial'
-
-interface ContainerProps {
-  send(action: T.Action): void
-  context: T.MachineContext
+import SelectTutorialForm from './SelectTutorialForm'
+import LoadTutorialSummary from './LoadTutorialSummary'
+
+const styles = {
+  page: {
+    position: 'relative' as 'relative',
+    height: 'auto',
+    width: '100%',
+  },
+  selectPage: {
+    padding: '1rem',
+  },
 }
 
-interface TutorialsData {
-  tutorials: G.Tutorial[]
+interface Props {
+  send: any
+  context: any
 }
 
-const SelectPageContainer = (props: ContainerProps) => {
-  const { data, loading, error } = useQuery<TutorialsData>(queryTutorials, {
-    fetchPolicy: 'no-cache',
-  })
-
-  if (error) {
-    return <ErrorView error={error} />
-  }
-
-  if (loading) {
-    return <LoadingPage text="Loading tutorials" context={props.context} />
-  }
-
-  if (!data) {
-    return null
-  }
-
-  return <SelectTutorial tutorialList={data.tutorials} send={props.send} />
+const SelectTutorialPage = (props: Props) => {
+  const [url, setUrl] = React.useState<string | null>(null)
+  return (
+    <div css={styles.page}>
+      {!url && <SelectTutorialForm onUrlChange={setUrl} />}
+      {url && <LoadTutorialSummary url={url} send={props.send} onClear={() => setUrl(null)} />}
+    </div>
+  )
 }
 
-export default SelectPageContainer
+export default SelectTutorialPage
diff --git a/web-app/src/containers/Start/index.tsx b/web-app/src/containers/Start/index.tsx
index 27fa5462..257f25c4 100644
--- a/web-app/src/containers/Start/index.tsx
+++ b/web-app/src/containers/Start/index.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react'
 import * as CR from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import BetaBadge from '../../components/BetaBadge'
 import { css, jsx } from '@emotion/core'
 import Button from '../../components/Button'
@@ -46,7 +46,7 @@ const styles = {
 interface Props {
   onContinue(): void
   onNew(): void
-  tutorial?: G.Tutorial
+  tutorial?: TT.Tutorial
 }
 
 export const StartPage = (props: Props) => (
diff --git a/web-app/src/containers/Tutorial/CompletedPage.tsx b/web-app/src/containers/Tutorial/CompletedPage.tsx
index 79db0513..eeba9ebd 100644
--- a/web-app/src/containers/Tutorial/CompletedPage.tsx
+++ b/web-app/src/containers/Tutorial/CompletedPage.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react'
-import * as CR from 'typings'
+import * as T from 'typings'
 import { css, jsx } from '@emotion/core'
 import Button from '../../components/Button'
 
@@ -17,8 +17,8 @@ const styles = {
 }
 
 interface Props {
-  context: CR.MachineContext
-  send(action: CR.Action | string): void
+  context: T.MachineContext
+  send(action: T.Action | string): void
 }
 
 const CompletedPage = (props: Props) => {
diff --git a/web-app/src/containers/Tutorial/LevelPage/Level.tsx b/web-app/src/containers/Tutorial/LevelPage/Level.tsx
index ae0155b5..bf782377 100644
--- a/web-app/src/containers/Tutorial/LevelPage/Level.tsx
+++ b/web-app/src/containers/Tutorial/LevelPage/Level.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react'
 import * as T from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import { css, jsx } from '@emotion/core'
 import Button from '../../../components/Button'
 import Markdown from '../../../components/Markdown'
@@ -77,7 +77,7 @@ const styles = {
 }
 
 interface Props {
-  level: G.Level & { status: T.ProgressStatus; index: number; steps: Array<G.Step & { status: T.ProgressStatus }> }
+  level: TT.Level & { status: T.ProgressStatus; index: number; steps: Array<TT.Step & { status: T.ProgressStatus }> }
   processes: T.ProcessEvent[]
   testStatus: T.TestStatus | null
   onContinue(): void
@@ -114,7 +114,7 @@ const Level = ({ level, onContinue, onLoadSolution, processes, testStatus }: Pro
           <div css={styles.tasks}>
             <div css={styles.header}>Tasks</div>
             <div css={styles.steps}>
-              {level.steps.map((step: (G.Step & { status: T.ProgressStatus }) | null, index: number) => {
+              {level.steps.map((step: (TT.Step & { status: T.ProgressStatus }) | null, index: number) => {
                 if (!step) {
                   return null
                 }
diff --git a/web-app/src/containers/Tutorial/LevelPage/Step.tsx b/web-app/src/containers/Tutorial/LevelPage/Step.tsx
index 97ef775a..d8fbe9dd 100644
--- a/web-app/src/containers/Tutorial/LevelPage/Step.tsx
+++ b/web-app/src/containers/Tutorial/LevelPage/Step.tsx
@@ -3,7 +3,6 @@ import * as T from 'typings'
 import { css, jsx } from '@emotion/core'
 import Checkbox from '../../../components/Checkbox'
 import Markdown from '../../../components/Markdown'
-import StepHelp from '../../../components/StepHelp'
 
 interface Props {
   order: number
diff --git a/web-app/src/containers/Tutorial/LevelPage/index.tsx b/web-app/src/containers/Tutorial/LevelPage/index.tsx
index 4c74179f..ab945e60 100644
--- a/web-app/src/containers/Tutorial/LevelPage/index.tsx
+++ b/web-app/src/containers/Tutorial/LevelPage/index.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react'
 import * as T from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import * as selectors from '../../../services/selectors'
 import Level from './Level'
 
@@ -12,8 +12,8 @@ interface PageProps {
 const LevelSummaryPageContainer = (props: PageProps) => {
   const { position, progress, processes, testStatus, error } = props.context
 
-  const version = selectors.currentVersion(props.context)
-  const levelData: G.Level = selectors.currentLevel(props.context)
+  const tutorial = selectors.currentTutorial(props.context)
+  const levelData: TT.Level = selectors.currentLevel(props.context)
 
   const onContinue = (): void => {
     props.send({
@@ -28,15 +28,15 @@ const LevelSummaryPageContainer = (props: PageProps) => {
     props.send({ type: 'STEP_SOLUTION_LOAD' })
   }
 
-  const level: G.Level & {
+  const level: TT.Level & {
     status: T.ProgressStatus
     index: number
-    steps: Array<G.Step & { status: T.ProgressStatus }>
+    steps: Array<TT.Step & { status: T.ProgressStatus }>
   } = {
     ...levelData,
-    index: version.data.levels.findIndex((l: G.Level) => l.id === position.levelId),
+    index: tutorial.levels.findIndex((l: TT.Level) => l.id === position.levelId),
     status: progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE',
-    steps: levelData.steps.map((step: G.Step) => {
+    steps: levelData.steps.map((step: TT.Step) => {
       // label step status for step component
       let status: T.ProgressStatus = 'INCOMPLETE'
       if (progress.steps[step.id]) {
diff --git a/web-app/src/environment.ts b/web-app/src/environment.ts
index ff5f11cf..79fbe34c 100644
--- a/web-app/src/environment.ts
+++ b/web-app/src/environment.ts
@@ -1,14 +1,13 @@
 // validate .env
-const requiredKeys = ['REACT_APP_GQL_URI']
+const requiredKeys = ['REACT_APP_TUTORIAL_URL']
 for (const required of requiredKeys) {
   if (!process.env[required]) {
-    throw new Error(`Missing Environmental Variables: ${required}`)
+    throw new Error(`Missing Environmental Variable: ${required}`)
   }
 }
 
-export const GQL_URI: string = process.env.REACT_APP_GQL_URI || 'NO API URI PROVIDED'
 export const DEBUG: boolean = (process.env.REACT_APP_DEBUG || '').toLowerCase() === 'true'
 export const VERSION: string = process.env.VERSION || 'unknown'
-export const NODE_ENV: string = process.env.NODE_ENV || 'production'
-export const AUTH_TOKEN: string | null = process.env.AUTH_TOKEN || null
-export const LOG_STATE: boolean = (process.env.LOG_STATE || '').toLowerCase() === 'true'
+export const NODE_ENV: string = process.env.NODE_ENV || 'development'
+export const LOG_STATE: boolean = (process.env.REACT_APP_LOG_STATE || '').toLowerCase() === 'true'
+export const TUTORIAL_URL: string = process.env.REACT_APP_TUTORIAL_URL || ''
diff --git a/web-app/src/services/apollo/auth.ts b/web-app/src/services/apollo/auth.ts
deleted file mode 100644
index 18020809..00000000
--- a/web-app/src/services/apollo/auth.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Operation } from 'apollo-boost'
-import { AUTH_TOKEN } from '../../environment'
-
-let authToken: string | null = AUTH_TOKEN || null
-
-export const setAuthToken = (token: string | null) => {
-  authToken = token
-}
-
-export const authorizeHeaders = (operation: Operation) => {
-  if (authToken) {
-    operation.setContext({
-      headers: {
-        Authorization: authToken,
-      },
-    })
-  }
-}
diff --git a/web-app/src/services/apollo/index.ts b/web-app/src/services/apollo/index.ts
deleted file mode 100644
index dab3c450..00000000
--- a/web-app/src/services/apollo/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import ApolloClient from 'apollo-boost'
-import { GQL_URI } from '../../environment'
-import { authorizeHeaders } from './auth'
-
-const client = new ApolloClient({
-  uri: GQL_URI,
-  request: authorizeHeaders,
-})
-
-export default client
diff --git a/web-app/src/services/apollo/mutations/authenticate.ts b/web-app/src/services/apollo/mutations/authenticate.ts
deleted file mode 100644
index 44d44fd9..00000000
--- a/web-app/src/services/apollo/mutations/authenticate.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { gql } from 'apollo-boost'
-
-export default gql`
-  mutation Authenticate($machineId: String!, $sessionId: String!, $editor: Editor!) {
-    editorLogin(input: { machineId: $machineId, sessionId: $sessionId, editor: $editor }) {
-      token
-      user {
-        id
-        name
-        email
-      }
-    }
-  }
-`
diff --git a/web-app/src/services/apollo/queries/summary.ts b/web-app/src/services/apollo/queries/summary.ts
deleted file mode 100644
index 33588cc0..00000000
--- a/web-app/src/services/apollo/queries/summary.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { gql } from 'apollo-boost'
-
-// TODO: add version to query
-
-export default gql`
-  query getTutorial($tutorialId: ID!) {
-    tutorial(id: $tutorialId) {
-      id
-      createdBy {
-        id
-        name
-        email
-      }
-      summary {
-        title
-        description
-      }
-      version {
-        id
-        createdAt
-        createdBy {
-          id
-          name
-        }
-        updatedAt
-        updatedBy {
-          id
-          name
-        }
-        publishedAt
-        publishedBy {
-          name
-        }
-        data {
-          levels {
-            id
-            title
-            summary
-          }
-        }
-      }
-    }
-  }
-`
diff --git a/web-app/src/services/apollo/queries/tutorial.ts b/web-app/src/services/apollo/queries/tutorial.ts
deleted file mode 100644
index 765d1910..00000000
--- a/web-app/src/services/apollo/queries/tutorial.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { gql } from 'apollo-boost'
-
-// TODO: add version to query
-
-export default gql`
-  query getTutorial($tutorialId: ID!) {
-    tutorial(id: $tutorialId) {
-      id
-      createdBy {
-        id
-        name
-        email
-      }
-      summary {
-        title
-        description
-      }
-      version {
-        id
-        createdAt
-        createdBy {
-          id
-          name
-        }
-        updatedAt
-        updatedBy {
-          id
-          name
-        }
-        publishedAt
-        publishedBy {
-          name
-        }
-        data {
-          config
-          levels {
-            id
-            title
-            summary
-            content
-            setup
-            steps {
-              id
-              content
-              setup
-              solution
-            }
-          }
-        }
-      }
-    }
-  }
-`
diff --git a/web-app/src/services/apollo/queries/tutorials.ts b/web-app/src/services/apollo/queries/tutorials.ts
deleted file mode 100644
index 5b3fe79c..00000000
--- a/web-app/src/services/apollo/queries/tutorials.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { gql } from 'apollo-boost'
-
-export default gql`
-  query getTutorials {
-    tutorials {
-      id
-      createdBy {
-        id
-        name
-        email
-      }
-      summary {
-        title
-        description
-      }
-      version {
-        id
-        publishedAt
-        publishedBy {
-          id
-          name
-          email
-        }
-      }
-    }
-  }
-`
diff --git a/web-app/src/services/hooks/useFetch.ts b/web-app/src/services/hooks/useFetch.ts
new file mode 100644
index 00000000..524cf3df
--- /dev/null
+++ b/web-app/src/services/hooks/useFetch.ts
@@ -0,0 +1,23 @@
+import * as React from 'react'
+
+const useFetch = <T>(url: string, options?: object): { data: T | null; error: string | null; loading: boolean } => {
+  const [data, setData] = React.useState(null)
+  const [error, setError] = React.useState(null)
+  const [loading, setLoading] = React.useState(true)
+  React.useEffect(() => {
+    const fetchData = async () => {
+      try {
+        const res = await fetch(url, options)
+        setLoading(false)
+        const json = await res.json()
+        setData(json)
+      } catch (error) {
+        setError(error)
+      }
+    }
+    fetchData()
+  }, [url])
+  return { data, error, loading }
+}
+
+export default useFetch
diff --git a/web-app/src/services/selectors/position.ts b/web-app/src/services/selectors/position.ts
index 7beafeda..d422b816 100644
--- a/web-app/src/services/selectors/position.ts
+++ b/web-app/src/services/selectors/position.ts
@@ -1,18 +1,19 @@
-import { createSelector } from 'reselect'
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
-import * as tutorial from './tutorial'
+import * as T from 'typings'
+import * as TT from 'typings/tutorial'
 
 export const defaultPosition = () => ({
   levelId: '',
   stepId: null,
 })
 
-export const initialPosition = createSelector(tutorial.currentVersion, (version: G.TutorialVersion) => {
-  const level = version.data.levels[0]
-  const position: CR.Position = {
+export const initialPosition = (context: T.MachineContext) => {
+  if (!context.tutorial) {
+    throw new Error('Tutorial not found at initialPosition check')
+  }
+  const level: TT.Level = context.tutorial.levels[0]
+  const position: T.Position = {
     levelId: level.id,
     stepId: level.steps.length ? level.steps[0].id : null,
   }
   return position
-})
+}
diff --git a/web-app/src/services/selectors/tutorial.ts b/web-app/src/services/selectors/tutorial.ts
index 31912d67..4b1cfa0f 100644
--- a/web-app/src/services/selectors/tutorial.ts
+++ b/web-app/src/services/selectors/tutorial.ts
@@ -1,9 +1,9 @@
 import { createSelector } from 'reselect'
 import { MachineContext } from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import onError from '../../services/sentry/onError'
 
-export const currentTutorial = ({ tutorial }: MachineContext): G.Tutorial => {
+export const currentTutorial = ({ tutorial }: MachineContext): TT.Tutorial => {
   if (!tutorial) {
     const error = new Error('Tutorial not found')
     onError(error)
@@ -12,38 +12,29 @@ export const currentTutorial = ({ tutorial }: MachineContext): G.Tutorial => {
   return tutorial
 }
 
-export const currentVersion = createSelector(currentTutorial, (tutorial: G.Tutorial) => {
-  if (!tutorial.version) {
-    const error = new Error('Tutorial version not found')
-    onError(error)
-    throw error
-  }
-  return tutorial.version
-})
-
-export const currentLevel = (context: MachineContext): G.Level =>
+export const currentLevel = (context: MachineContext): TT.Level =>
   createSelector(
-    currentVersion,
-    (version: G.TutorialVersion): G.Level => {
+    currentTutorial,
+    (tutorial: TT.Tutorial): TT.Level => {
       // merge in the updated position
       // sent with the test to ensure consistency
-      const levels: G.Level[] = version.data.levels
+      const levels: TT.Level[] = tutorial.levels
 
-      const levelIndex = levels.findIndex((l: G.Level) => l.id === context.position.levelId)
+      const levelIndex = levels.findIndex((l: TT.Level) => l.id === context.position.levelId)
       if (levelIndex < 0) {
-        const error = new Error(`Level not found when selecting level for ${version}`)
+        const error = new Error(`Level not found when selecting level for ${tutorial.id}`)
         onError(error)
         throw error
       }
-      const level: G.Level = levels[levelIndex]
+      const level: TT.Level = levels[levelIndex]
 
       return level
     },
   )(context)
 
-export const currentStep = (context: MachineContext): G.Step | null =>
-  createSelector(currentLevel, (level: G.Level): G.Step | null => {
-    const steps: G.Step[] = level.steps
-    const step: G.Step | null = steps.find((s: G.Step) => s.id === context.position.stepId) || null
+export const currentStep = (context: MachineContext): TT.Step | null =>
+  createSelector(currentLevel, (level: TT.Level): TT.Step | null => {
+    const steps: TT.Step[] = level.steps
+    const step: TT.Step | null = steps.find((s: TT.Step) => s.id === context.position.stepId) || null
     return step
   })(context)
diff --git a/web-app/src/services/state/actions/context.ts b/web-app/src/services/state/actions/context.ts
index 5729452e..e60d8881 100644
--- a/web-app/src/services/state/actions/context.ts
+++ b/web-app/src/services/state/actions/context.ts
@@ -1,5 +1,5 @@
 import * as T from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import { assign, send, ActionFunctionMap } from 'xstate'
 import * as selectors from '../../selectors'
 import onError from '../../../services/sentry/onError'
@@ -27,12 +27,6 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
     },
   }),
   // @ts-ignore
-  selectTutorialById: assign({
-    tutorial: (context: T.MachineContext, event: T.MachineEvent): any => {
-      return event.payload.tutorial
-    },
-  }),
-  // @ts-ignore
   startNewTutorial: assign({
     position: (context: T.MachineContext, event: T.MachineEvent): any => {
       const position: T.Position = selectors.initialPosition(context)
@@ -50,17 +44,17 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
       const { position } = context
       // merge in the updated position
       // sent with the test to ensure consistency
-      const level: G.Level = selectors.currentLevel(context)
-      const steps: G.Step[] = level.steps
+      const level: TT.Level = selectors.currentLevel(context)
+      const steps: TT.Step[] = level.steps
 
       // final step but not completed
       if (steps[steps.length - 1].id === position.stepId) {
         return position
       }
 
-      const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId)
+      const stepIndex = steps.findIndex((s: TT.Step) => s.id === position.stepId)
 
-      const step: G.Step = steps[stepIndex + 1]
+      const step: TT.Step = steps[stepIndex + 1]
 
       const nextPosition: T.Position = {
         ...position,
@@ -74,13 +68,13 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
   updateLevelPosition: assign({
     position: (context: T.MachineContext): any => {
       const { position } = context
-      const version = selectors.currentVersion(context)
+      const tutorial = selectors.currentTutorial(context)
       // merge in the updated position
       // sent with the test to ensure consistency
-      const levels: G.Level[] = version.data.levels
+      const levels: TT.Level[] = tutorial.levels
 
-      const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId)
-      const level: G.Level = levels[levelIndex + 1]
+      const levelIndex = levels.findIndex((l: TT.Level) => l.id === position.levelId)
+      const level: TT.Level = levels[levelIndex + 1]
 
       const nextPosition: T.Position = {
         levelId: level.id,
@@ -129,10 +123,10 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
 
       const level = selectors.currentLevel(context)
 
-      const steps: G.Step[] = level.steps
+      const steps: TT.Step[] = level.steps
 
       if (steps.length && position.stepId) {
-        const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId)
+        const stepIndex = steps.findIndex((s: TT.Step) => s.id === position.stepId)
         const stepComplete = progress.steps[position.stepId]
         const finalStep = stepIndex > -1 && stepIndex === steps.length - 1
         const hasNextStep = !finalStep && !stepComplete
@@ -152,8 +146,8 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
       }
 
       // @ts-ignore
-      const levels = context.tutorial.version.data.levels || []
-      const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId)
+      const levels = context.tutorial.levels || []
+      const levelIndex = levels.findIndex((l: TT.Level) => l.id === position.levelId)
       const finalLevel = levelIndex > -1 && levelIndex === levels.length - 1
       const hasNextLevel = !finalLevel
 
@@ -175,12 +169,12 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
     (context: T.MachineContext): T.Action => {
       const { position, progress } = context
 
-      const level: G.Level = selectors.currentLevel(context)
+      const level: TT.Level = selectors.currentLevel(context)
 
       const { steps } = level
 
       if (steps.length && position.stepId) {
-        const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId)
+        const stepIndex = steps.findIndex((s: TT.Step) => s.id === position.stepId)
         const finalStep = stepIndex === steps.length - 1
         const stepComplete = progress.steps[position.stepId]
         // not final step, or final step but not complete
@@ -232,6 +226,12 @@ const contextActions: ActionFunctionMap<T.MachineContext, T.MachineEvent> = {
       type: context.position.stepId === null ? 'START_COMPLETED_LEVEL' : 'START_LEVEL',
     }
   }),
+  // @ts-ignore
+  setTutorialContext: assign({
+    tutorial: (context: T.MachineContext, event: T.MachineEvent): any => {
+      return event.payload.tutorial
+    },
+  }),
 }
 
 export default contextActions
diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts
index 8efc450f..2b51bddf 100644
--- a/web-app/src/services/state/actions/editor.ts
+++ b/web-app/src/services/state/actions/editor.ts
@@ -1,5 +1,5 @@
 import * as CR from 'typings'
-import * as G from 'typings/graphql'
+import * as TT from 'typings/tutorial'
 import * as selectors from '../../selectors'
 
 export default (editorSend: any) => ({
@@ -34,7 +34,7 @@ export default (editorSend: any) => ({
     })
   },
   loadLevel(context: CR.MachineContext): void {
-    const level: G.Level = selectors.currentLevel(context)
+    const level: TT.Level = selectors.currentLevel(context)
     if (level.setup) {
       // load step actions
       editorSend({
@@ -44,7 +44,7 @@ export default (editorSend: any) => ({
     }
   },
   loadStep(context: CR.MachineContext): void {
-    const step: G.Step | null = selectors.currentStep(context)
+    const step: TT.Step | null = selectors.currentStep(context)
     if (step && step.setup) {
       // load step actions
       editorSend({
@@ -57,7 +57,7 @@ export default (editorSend: any) => ({
     }
   },
   editorLoadSolution(context: CR.MachineContext): void {
-    const step: G.Step | null = selectors.currentStep(context)
+    const step: TT.Step | null = selectors.currentStep(context)
     // tell editor to load solution commit
     if (step && step.solution) {
       editorSend({
diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts
index 4cbca2c5..1099aa24 100644
--- a/web-app/src/services/state/machine.ts
+++ b/web-app/src/services/state/machine.ts
@@ -1,7 +1,6 @@
 import * as CR from 'typings'
 import { assign, Machine, MachineOptions } from 'xstate'
 import createActions from './actions'
-import * as services from './services'
 
 const createOptions = ({ editorSend }: any): MachineOptions<CR.MachineContext, CR.MachineEvent> => ({
   activities: {},
@@ -38,23 +37,11 @@ export const createMachine = (options: any) => {
               onEntry: ['loadEnv'],
               on: {
                 ENV_LOAD: {
-                  target: 'Authenticate',
+                  target: 'LoadStoredTutorial',
                   actions: ['setEnv'],
                 },
               },
             },
-            Authenticate: {
-              invoke: {
-                src: services.authenticate,
-                onDone: 'LoadStoredTutorial',
-                onError: {
-                  target: 'Error',
-                  actions: assign({
-                    error: (context, event) => event.data,
-                  }),
-                },
-              },
-            },
             Error: {},
             LoadStoredTutorial: {
               onEntry: ['loadStoredTutorial'],
@@ -95,50 +82,9 @@ export const createMachine = (options: any) => {
               onEntry: ['clearStorage'],
               id: 'select-new-tutorial',
               on: {
-                SELECT_TUTORIAL: {
-                  target: 'LoadTutorialSummary',
-                  actions: ['selectTutorialById'],
-                },
-              },
-            },
-            // TODO move Initialize into New Tutorial setup
-            LoadTutorialSummary: {
-              invoke: {
-                src: services.loadTutorialSummary,
-                onDone: {
-                  target: 'Summary',
-                  actions: assign({
-                    tutorial: (context, event) => event.data,
-                  }),
-                },
-                onError: {
-                  target: 'Error',
-                  actions: assign({
-                    error: (context, event) => event.data,
-                  }),
-                },
-              },
-            },
-            Summary: {
-              on: {
-                BACK: 'SelectTutorial',
-                TUTORIAL_START: 'LoadTutorialData',
-              },
-            },
-            LoadTutorialData: {
-              invoke: {
-                src: services.loadTutorialData,
-                onDone: {
+                TUTORIAL_START: {
                   target: 'SetupNewTutorial',
-                  actions: assign({
-                    tutorial: (context, event) => event.data,
-                  }),
-                },
-                onError: {
-                  target: 'Error',
-                  actions: assign({
-                    error: (context, event) => event.data,
-                  }),
+                  actions: ['setTutorialContext'],
                 },
               },
             },
diff --git a/web-app/src/services/state/services/authenticate.ts b/web-app/src/services/state/services/authenticate.ts
deleted file mode 100644
index 6785be61..00000000
--- a/web-app/src/services/state/services/authenticate.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
-import client from '../../apollo'
-import { setAuthToken } from '../../apollo/auth'
-import authenticateMutation from '../../apollo/mutations/authenticate'
-import onError from '../../../services/sentry/onError'
-
-interface AuthenticateData {
-  editorLogin: {
-    token: string
-    user: G.User
-  }
-}
-
-interface AuthenticateVariables {
-  machineId: string
-  sessionId: string
-  editor: 'VSCODE'
-}
-
-export async function authenticate(context: CR.MachineContext): Promise<any> {
-  const result = await client
-    .mutate<AuthenticateData, AuthenticateVariables>({
-      mutation: authenticateMutation,
-      variables: {
-        machineId: context.env.machineId,
-        sessionId: context.env.sessionId,
-        editor: 'VSCODE',
-      },
-    })
-    .catch((error) => {
-      onError(error)
-      console.log('ERROR: Authentication failed')
-      console.log(error.message)
-      // let message
-      if (error.message.match(/Network error:/)) {
-        return Promise.reject({
-          title: 'Network Error',
-          description: 'Make sure you have an Internet connection. Restart and try again',
-        })
-      } else {
-        return Promise.reject({
-          title: 'Server Error',
-          description: error.message,
-        })
-      }
-    })
-
-  if (!result || !result.data) {
-    const message = 'Authentication request responded with no data'
-    const error = new Error()
-    console.log(error)
-    onError(error)
-    return Promise.reject({
-      title: message,
-      description: 'Something went wrong.',
-    })
-  }
-  const { token } = result.data.editorLogin
-  // add token to headers
-  setAuthToken(token)
-  // pass authenticated action back to state machine
-  return Promise.resolve()
-}
diff --git a/web-app/src/services/state/services/index.ts b/web-app/src/services/state/services/index.ts
deleted file mode 100644
index 63d753bb..00000000
--- a/web-app/src/services/state/services/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { authenticate } from './authenticate'
-export { loadTutorialData } from './loadTutorialData'
-export { loadTutorialSummary } from './loadTutorialSummary'
diff --git a/web-app/src/services/state/services/loadTutorialData.ts b/web-app/src/services/state/services/loadTutorialData.ts
deleted file mode 100644
index a7aeafbb..00000000
--- a/web-app/src/services/state/services/loadTutorialData.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
-import client from '../../apollo'
-import tutorialQuery from '../../apollo/queries/tutorial'
-import onError from '../../../services/sentry/onError'
-
-interface TutorialData {
-  tutorial: G.Tutorial
-}
-
-interface TutorialDataVariables {
-  tutorialId: string
-  // version: string
-}
-
-export async function loadTutorialData(context: CR.MachineContext): Promise<any> {
-  // setup test runner and git
-  if (!context.tutorial) {
-    const error = new Error('Tutorial not available to load')
-    onError(error)
-    throw error
-  }
-
-  try {
-    const result = await client.query<TutorialData, TutorialDataVariables>({
-      query: tutorialQuery,
-      variables: {
-        tutorialId: context.tutorial.id,
-        // version: context.tutorial.version.version, // TODO: reimplement version
-      },
-    })
-    if (!result || !result.data || !result.data.tutorial) {
-      const message = 'No tutorial returned from tutorial config query'
-      onError(new Error(message))
-      return Promise.reject(message)
-    }
-    return Promise.resolve(result.data.tutorial)
-  } catch (error) {
-    const message: CR.ErrorMessage = { title: 'Failed to load tutorial config', description: error.message }
-    onError(error)
-    return Promise.reject(message)
-  }
-}
diff --git a/web-app/src/services/state/services/loadTutorialSummary.ts b/web-app/src/services/state/services/loadTutorialSummary.ts
deleted file mode 100644
index b4236003..00000000
--- a/web-app/src/services/state/services/loadTutorialSummary.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as CR from 'typings'
-import * as G from 'typings/graphql'
-import client from '../../apollo'
-import summaryQuery from '../../apollo/queries/summary'
-import onError from '../../../services/sentry/onError'
-
-interface TutorialData {
-  tutorial: G.Tutorial
-}
-
-interface TutorialDataVariables {
-  tutorialId: string
-  // version: string
-}
-
-export async function loadTutorialSummary(context: CR.MachineContext): Promise<any> {
-  // setup test runner and git
-  if (!context.tutorial) {
-    const error = new Error('Tutorial not available to load')
-    onError(error)
-    throw error
-  }
-
-  try {
-    const result = await client.query<TutorialData, TutorialDataVariables>({
-      query: summaryQuery,
-      variables: {
-        tutorialId: context.tutorial.id,
-        // version: context.tutorial.version.version, // TODO: reimplement version
-      },
-    })
-    if (!result || !result.data || !result.data.tutorial) {
-      const message = 'No tutorial returned from tutorial config query'
-      onError(new Error(message))
-      return Promise.reject(message)
-    }
-    return Promise.resolve(result.data.tutorial)
-  } catch (error) {
-    const message: CR.ErrorMessage = { title: 'Failed to load tutorial config', description: error.message }
-    onError(error)
-    return Promise.reject(message)
-  }
-}
diff --git a/web-app/stories/GitHubFetch.stories.tsx b/web-app/stories/GitHubFetch.stories.tsx
new file mode 100644
index 00000000..c3daba5b
--- /dev/null
+++ b/web-app/stories/GitHubFetch.stories.tsx
@@ -0,0 +1,19 @@
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import React from 'react'
+import { css, jsx } from '@emotion/core'
+import SideBarDecorator from './utils/SideBarDecorator'
+import SelectTutorialPage from '../src/containers/SelectTutorial'
+
+const styles = {
+  container: {
+    display: 'flex' as 'flex',
+    flexDirection: 'column' as 'column',
+  },
+}
+
+storiesOf('GitHub Fetch', module)
+  .addDecorator(SideBarDecorator)
+  .add('Select Tutorial', () => {
+    return <SelectTutorialPage send={action('send')} context={{}} />
+  })
diff --git a/web-app/stories/Level.stories.tsx b/web-app/stories/Level.stories.tsx
index 49fcba1a..12d39dbb 100644
--- a/web-app/stories/Level.stories.tsx
+++ b/web-app/stories/Level.stories.tsx
@@ -3,14 +3,14 @@ import { withKnobs } from '@storybook/addon-knobs'
 import { storiesOf } from '@storybook/react'
 import React from 'react'
 import * as T from '../../typings'
-import * as G from '../../typings/graphql'
+import * as TT from '../../typings/tutorial'
 import Level from '../src/containers/Tutorial/LevelPage/Level'
 import SideBarDecorator from './utils/SideBarDecorator'
 
-type ModifiedLevel = G.Level & {
+type ModifiedLevel = TT.Level & {
   status: T.ProgressStatus
   index: number
-  steps: Array<G.Step & { status: T.ProgressStatus }>
+  steps: Array<TT.Step & { status: T.ProgressStatus }>
 }
 
 storiesOf('Level', module)
diff --git a/web-app/stories/data/basic.ts b/web-app/stories/data/basic.ts
deleted file mode 100644
index 395ac008..00000000
--- a/web-app/stories/data/basic.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import * as CR from 'typings'
-
-const basic: CR.Tutorial = {
-  id: 'tutorialId',
-  meta: {
-    version: '0.1.0',
-    repo: 'https://github.com/ShMcK/coderoad-vscode.git',
-    createdBy: 'shmck',
-    createdAt: 'Sat, 11 May 2019 18:25:24 GMT',
-    updatedBy: 'shmck',
-    updatedAt: 'Sat, 11 May 2019 18:25:24 GMT',
-    contributors: ['shmck'],
-    languages: ['javascript'],
-    testRunner: 'jest',
-  },
-  data: {
-    summary: {
-      title: 'Basic Test',
-      description: 'A basic coding skills example',
-      levelList: ['level1Id'],
-    },
-    levels: {
-      level1Id: {
-        stageList: ['stage1Id'],
-        content: {
-          title: 'Sum Level',
-          text: 'A description of this stage',
-        },
-      },
-    },
-    stages: {
-      stage1Id: {
-        stepList: ['step1Id', 'step2Id', 'step3Id'],
-        content: {
-          title: 'Sum Stage',
-          text: 'A description of this stage',
-        },
-      },
-    },
-    steps: {
-      step1Id: {
-        content: {
-          title: 'Sum',
-          text: 'Write a function that adds two numbers together',
-        },
-        actions: {
-          setup: {
-            commits: ['430500f', '8383061'],
-            commands: ['npm install'],
-            files: ['src/sum.js'],
-          },
-          solution: {
-            commits: ['abbe136'],
-          },
-        },
-        hints: [],
-      },
-      step2Id: {
-        content: {
-          title: 'Multiply',
-          text: 'Write a function that multiplies two numbers together',
-        },
-        actions: {
-          setup: {
-            commits: ['9cbb518'],
-            files: ['src/multiply.js'],
-          },
-          solution: {
-            commits: ['5ae011f'],
-          },
-        },
-        hints: [],
-      },
-      step3Id: {
-        content: {
-          title: 'Divide',
-          text: 'Write a function that divides',
-        },
-        actions: {
-          setup: {
-            commits: ['70c774c'],
-            files: ['src/divide.js'],
-          },
-          solution: {
-            commits: ['3180bed'],
-          },
-        },
-        hints: [],
-      },
-    },
-  },
-}
-
-export default basic
diff --git a/web-app/tsconfig.paths.json b/web-app/tsconfig.paths.json
index 886c8bff..3adaa5e2 100644
--- a/web-app/tsconfig.paths.json
+++ b/web-app/tsconfig.paths.json
@@ -2,7 +2,7 @@
   "compilerOptions": {
     "paths": {
       "typings": ["../../typings/index.d.ts"],
-      "typings/graphql": ["../../typings/graphql.d.ts"]
+      "typings/tutorial": ["../../typings/tutorial.d.ts"]
     },
     "allowSyntheticDefaultImports": true
   },