diff --git a/.eslintrc.json b/.eslintrc.json
index b47bc599df..f1eade9305 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -24,6 +24,7 @@
         "no-await-in-loop" : 1
     },
     "globals" : {
-        "Parse" : true
+        "Parse" : true,
+        "document": true
     }
 }
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8a3d4ae74b..fbe3152d94 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        node-version: [12.x]
+        node-version: [15.x]
     name: ${{ matrix.name }}
     steps:
     - uses: actions/checkout@v2
diff --git a/README.md b/README.md
index b627d4e64a..96c1a4e992 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,15 @@
 [![License][license-svg]][license-link]
 [![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform)
 
-Example project using the [parse-server](https://github.com/ParsePlatform/parse-server) module on Express. Read the full [Parse Server Guide](https://docs.parseplatform.org/parse-server/guide/) for more information.
+Example project using the [parse-server](https://github.com/ParsePlatform/parse-server) module on Express, utilising AWS Secret Manager Read the full [Parse Server Guide](https://docs.parseplatform.org/parse-server/guide/) for more information.
+
+Please note: this example uses top level await which is only available in Node >= v14.8.0.
 
 # Table of Contents <!-- omit in toc -->
 
 - [Local Development](#local-development)
+  - [Creating AWS Secrets](#creating-aws-secrets)
+  - [File Setup](#file-setup)
   - [Helpful Scripts](#helpful-scripts)
 - [Remote Deployment](#remote-deployment)
   - [Heroku](#heroku)
@@ -29,6 +33,20 @@ Example project using the [parse-server](https://github.com/ParsePlatform/parse-
 
 # Local Development
 
+## Creating AWS Secrets
+* Log into the AWS Console and navigate to AWS Secrets Manager
+* Click "store a new secret"
+* Select "other type of secret"
+* Enter the initial secret value
+* Name the secret (`/src/config` will reference this secret name). If you have selected key pairs, make sure you properly destructure the returned secret.
+* If you would like to automatically rotate the key, follow [this](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_turn-on-for-other.html) guide.
+
+
+## Local Development
+
+* Install AWS SDK with `npm install aws-sdk -g`
+* Create an AWS profile with `aws configure --profile profileName`
+* Update `npm start`'s `AWS_Profile` and `AWS_REGION`
 * Make sure you have at least Node 4.3. `node --version`
 * Clone this repo and change directory to it.
 * `npm install`
@@ -39,6 +57,16 @@ Example project using the [parse-server](https://github.com/ParsePlatform/parse-
 * You now have a database named "dev" that contains your Parse data
 * Install ngrok and you can test with devices
 
+## File Setup
+Feel free to change this at your discretion. Example projects are just that - an example.
+
+* `/spec` contains unit tests you can write to validate your Parse Server.
+* `/src/cloud` contains Parse.Cloud files to run custom cloud code.
+* `/src/public` contains public assets.
+* `/src/views` contains views that express can render.
+* `/src/config.js` contains all Parse Server settings.
+* `index.js` is the main entry point for `npm start`, and includes express routing.
+
 ## Helpful Scripts
 These scripts can help you to develop your app for Parse Server:
 
@@ -83,7 +111,7 @@ Detailed information is available here:
 
 ## Google App Engine
 
-1. Clone the repo and change directory to it 
+1. Clone the repo and change directory to it
 1. Create a project in the [Google Cloud Platform Console](https://console.cloud.google.com/).
 1. [Enable billing](https://console.cloud.google.com/project/_/settings) for your project.
 1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/).
@@ -164,9 +192,11 @@ curl -X POST \
 
 ### JavaScript
 
+We have built an example page to show JS SDK usage, available at [http://localhost:1337/](http://localhost:1337/).
+
 ```js
 // Initialize SDK
-Parse.initialize("YOUR_APP_ID", "unused");
+Parse.initialize("YOUR_APP_ID");
 Parse.serverURL = 'http://localhost:1337/parse';
 
 // Save object
diff --git a/index.js b/index.js
index e720980bf0..b4c57bda09 100644
--- a/index.js
+++ b/index.js
@@ -1,65 +1,33 @@
 // Example express application adding the parse-server module to expose Parse
 // compatible API routes.
 
-const express = require('express');
-const ParseServer = require('parse-server').ParseServer;
-const path = require('path');
-const args = process.argv || [];
-const test = args.some(arg => arg.includes('jasmine'));
+import express from 'express';
+import { ParseServer } from 'parse-server';
+import { createServer } from 'http';
+import { config } from './src/config.js';
+import { renderFile } from 'ejs';
 
-const databaseUri = process.env.DATABASE_URI || process.env.MONGODB_URI;
-
-if (!databaseUri) {
-  console.log('DATABASE_URI not specified, falling back to localhost.');
-}
-const config = {
-  databaseURI: databaseUri || 'mongodb://localhost:27017/dev',
-  cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',
-  appId: process.env.APP_ID || 'myAppId',
-  masterKey: process.env.MASTER_KEY || '', //Add your master key here. Keep it secret!
-  serverURL: process.env.SERVER_URL || 'http://localhost:1337/parse', // Don't forget to change to https if needed
-  liveQuery: {
-    classNames: ['Posts', 'Comments'], // List of classes to support for query subscriptions
-  },
-};
-// Client-keys like the javascript key or the .NET key are not necessary with parse-server
-// If you wish you require them, you can set them as options in the initialization above:
-// javascriptKey, restAPIKey, dotNetKey, clientKey
-
-const app = express();
+export const app = express();
+app.set('view engine', 'ejs');
+app.engine('html', renderFile);
+app.set('views', `./src/views`);
 
 // Serve static assets from the /public folder
-app.use('/public', express.static(path.join(__dirname, '/public')));
-
-// Serve the Parse API on the /parse URL prefix
-const mountPath = process.env.PARSE_MOUNT || '/parse';
-if (!test) {
-  const api = new ParseServer(config);
-  app.use(mountPath, api);
-}
+app.use('/public', express.static('./src/public'));
 
 // Parse Server plays nicely with the rest of your web routes
-app.get('/', function (req, res) {
-  res.status(200).send('I dream of being a website.  Please star the parse-server repo on GitHub!');
+app.get('/', (req, res) => {
+  res.render('test.html', { appId: config.appId, serverUrl: config.serverURL });
 });
 
-// There will be a test page available on the /test path of your server url
-// Remove this before launching your app
-app.get('/test', function (req, res) {
-  res.sendFile(path.join(__dirname, '/public/test.html'));
-});
+if (!process.env.TESTING) {
+  const api = new ParseServer(config);
+  app.use('/parse', api);
 
-const port = process.env.PORT || 1337;
-if (!test) {
-  const httpServer = require('http').createServer(app);
-  httpServer.listen(port, function () {
-    console.log('parse-server-example running on port ' + port + '.');
+  const httpServer = createServer(app);
+  const port = 1337;
+  httpServer.listen(port, () => {
+    console.log(`parse-server-example running on port ${port}.`);
   });
-  // This will enable the Live Query real-time server
   ParseServer.createLiveQueryServer(httpServer);
 }
-
-module.exports = {
-  app,
-  config,
-};
diff --git a/package.json b/package.json
index 32c43e7a7f..cb3cc7196d 100644
--- a/package.json
+++ b/package.json
@@ -9,34 +9,38 @@
   },
   "license": "MIT",
   "dependencies": {
+    "aws-sdk": "2.994.0",
+    "ejs": "3.1.6",
     "express": "4.17.1",
-    "kerberos": "1.1.4",
-    "parse": "2.19.0",
-    "parse-server": "4.5.0"
+    "kerberos": "1.1.6",
+    "parse": "3.3.0",
+    "parse-server": "4.10.3"
   },
   "scripts": {
-    "start": "node index.js",
-    "lint": "eslint --cache ./cloud && eslint --cache index.js && eslint --cache ./spec",
-    "lint-fix": "eslint --cache --fix ./cloud && eslint --cache --fix index.js && eslint --cache --fix ./spec",
-    "test": "mongodb-runner start && jasmine",
+    "start": "AWS_PROFILE=aws_profile AWS_REGION=aws_region node index.js",
+    "lint": "eslint --cache ./src && eslint --cache index.js && eslint --cache ./spec",
+    "lint-fix": "eslint --cache --fix ./src && eslint --cache --fix index.js && eslint --cache --fix ./spec",
+    "test": "mongodb-runner start && TESTING=true jasmine",
+    "test:kill": "kill $(lsof -ti:27017) && npm test",
     "coverage": "nyc jasmine",
-    "prettier": "prettier --write '{cloud,spec}/{**/*,*}.js' 'index.js'",
+    "prettier": "prettier --write '{src,spec}/{**/*,*}{.js,.html,.css}' 'index.js'",
     "watch": "babel-watch index.js"
   },
   "engines": {
     "node": ">=4.3"
   },
+  "type": "module",
   "devDependencies": {
     "babel-eslint": "10.1.0",
-    "babel-watch": "7.4.0",
-    "eslint": "7.19.0",
-    "eslint-config-standard": "16.0.2",
-    "eslint-plugin-import": "2.22.1",
+    "babel-watch": "7.5.0",
+    "eslint": "7.32.0",
+    "eslint-config-standard": "16.0.3",
+    "eslint-plugin-import": "2.24.2",
     "eslint-plugin-node": "11.1.0",
-    "eslint-plugin-promise": "4.2.1",
-    "jasmine": "3.6.4",
-    "mongodb-runner": "4.8.1",
+    "eslint-plugin-promise": "5.1.0",
+    "jasmine": "3.9.0",
+    "mongodb-runner": "4.8.3",
     "nyc": "15.1.0",
-    "prettier": "2.2.1"
+    "prettier": "2.3.2"
   }
 }
diff --git a/public/assets/css/style.css b/public/assets/css/style.css
deleted file mode 100644
index 699311429d..0000000000
--- a/public/assets/css/style.css
+++ /dev/null
@@ -1,243 +0,0 @@
-body {
-  margin: 0;
-  padding: 0;
-  font-family: Helvetica, Arial, sans-serif;
-  font-size: 14px;
-  letter-spacing: 0.2px;
-  line-height: 24px;
-  color: #585858;
-}
-
-a {
-  color: #169CEE;
-  text-decoration: underline;
-}
-
-a:hover {
-  color: #2C3D50;
-}
-
-a:visited {
-  color: #2a6496;
-}
-
-
-/*
-  helpers
- */
-
-.align-center {
-  text-align: center;
-}
-
-.hidden {
-  display: none;
-}
-
-/*
-  app css
- */
-
-.container {
-  margin: 0 auto;
-  margin-top: 45px;
-  max-width: 860px;
-}
-
-#parse-logo {
-  width: 109px;
-  height: 110px;
-  margin: 0 0 20px;
-  text-align: center;
-}
-
-.up-and-running, .time-to-deploy {
-  font-weight: bold;
-}
-
-.advice {
-  margin-bottom: 40px;
-}
-
-.advice {
-  background: #f4f4f4;
-  border-radius: 4px;
-  -webkit-border-radius: 4px 4px;
-  -moz-border-radius: 4px 4px;
-  -ms-border-radius: 4px 4px;
-  -o-border-radius: 4px 4px;
-  padding: 10px 20px;
-}
-
-#parse-url {
-  color: #169CEE;
-  font-weight: bold;
-}
-
-.step--container {
-  margin: 30px 0 20px;
-  border-top: 1px solid #E2E2E2;
-  padding-top: 30px;
-}
-
-/* Disabled step */
-.step--disabled .step--number {
-  background: #fff;
-  border-color: #B5B5B5;
-  color: #B5B5B5;
-}
-
-.step--disabled .step--info {
-  border-color: #B5B5B5;
-  color: #B5B5B5;
-}
-
-.step--disabled .step--action-btn,
-  .step--disabled .step--action-btn:hover {
-  border-color: #B5B5B5;
-  background: #fff;
-  color: #B5B5B5;
-  cursor: default;
-}
-
-/* Disabled step eof */
-
-.step--action-btn.success,
-.step--action-btn.success:hover {
-  background: #57C689;
-  border-color: #57C689;
-  color: #fff;
-  cursor: default;
-  font-weight: bold;
-}
-
-.step--number {
-  background: #169CEE;
-  border: 1px solid #169CEE;
-  border-radius: 28px;
-  -webkit-border-radius: 28px 28px;
-  -moz-border-radius: 28px 28px;
-  -ms-border-radius: 28px 28px;
-  -o-border-radius: 28px 28px;
-  display: block;
-  margin: auto;
-  width: 47px;
-  height: 47px;
-  font-weight: bolder;
-  font-size: 20px;
-  color: #FFFFFF;
-  line-height: 47px; /* follows width and height */
-}
-
-.step--info { }
-
-.step--action-btn {
-  color: #169CEE;
-  font-size: 14px;
-  font-weight: 100;
-  border: 1px solid #169CEE;
-  padding: 12px 18px;
-  border-radius: 28px;
-  -webkit-border-radius: 28px 28px;
-  -moz-border-radius: 28px 28px;
-  -ms-border-radius: 28px 28px;
-  -o-border-radius: 28px 28px;
-  cursor: pointer;
-  text-decoration: none;
-  display:inline-block;
-  text-align: center;
-  text-transform: uppercase;
-}
-
-.step--action-btn:hover {
-  background: #169CEE;
-  color: white;
-}
-
-.step--pre {
-  margin-top: 4px;
-  margin-bottom: 0;
-  background: #f4f4f4;
-  border-radius: 4px;
-  -webkit-border-radius: 4px 4px;
-  -moz-border-radius: 4px 4px;
-  -ms-border-radius: 4px 4px;
-  -o-border-radius: 4px 4px;
-  padding: 10px 20px;
-  word-wrap: break-word;
-  white-space: inherit;
-  font-size: 13px;
-}
-
-#local-parse-working {
-  font-size: 18px;
-  line-height: 24px;
-  color: #57C689;
-  font-weight: bold;
-}
-
-#step-4 .step--number {
-  background: #57C689;
-  border-color: #57C689;
-  color: #fff;
-  display: inline-block;
-}
-
-.step--deploy-btn {
-  display: block;
-  margin-top: 20px;
-  width: 170px;
-  color: #57C689 !important;
-  font-weight: bold;
-  border-color: #57C689;
-}
-
-.step--deploy-btn:hover {
-  background: #57C689;
-  color: #fff !important;
-}
-
-.step--error {
-  color: red;
-  font-weight: bold;
-}
-
-#prod-test {
-  margin-bottom: 60px;
-}
-
-#prod-test input {
-  background-color: #fff;
-  border: 1px solid #B5B5B5;
-  color: #000000;
-  font-family: "Inconsolata";
-  font-size: 16px;
-  line-height: 17px;
-  padding: 12px;
-  width: 260px;
-  border-radius: 4px;
-  -webkit-border-radius: 4px 4px;
-  -moz-border-radius: 4px 4px;
-  -ms-border-radius: 4px 4px;
-  -o-border-radius: 4px 4px;
-  display:block;
-  margin-bottom: 10px;
-}
-
-#footer {
-  border-top: 1px solid #E2E2E2;
-  padding: 20px;
-}
-
-#footer ul li {
-  list-style-type: none;
-  display:inline-block;
-}
-#footer ul li:after {
-  content: "-";
-  padding: 10px;
-}
-#footer ul li:last-child:after {
-  content: "";
-}
-
diff --git a/public/assets/js/script.js b/public/assets/js/script.js
deleted file mode 100644
index 96cc688e53..0000000000
--- a/public/assets/js/script.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- *  Steps handler
- */
-
-var Steps = {};
-
-Steps.init = function() {
-  this.buildParseUrl();
-  this.bindBtn('#step-1-btn', function(e){
-    ParseRequest.postData();
-    e.preventDefault();
-  })
-}
-
-Steps.buildParseUrl = function() {
-  var url = Config.getUrl();
-  $('#parse-url').html(url + '/parse');
-}
-
-Steps.bindBtn = function(id, callback) {
-  $(id).click(callback);
-}
-
-Steps.closeStep = function(id) {
-  $(id).addClass('step--disabled');
-}
-
-Steps.openStep  = function(id) {
-  $(id).removeClass('step--disabled');
-}
-
-Steps.fillStepOutput  = function(id, data) {
-  $(id).html('Output: ' + data).slideDown();
-}
-
-Steps.fillStepError  = function(id, errorMsg) {
-  $(id).html(errorMsg).slideDown();
-}
-
-
-Steps.fillBtn  = function(id, message) {
-  $(id).addClass('success').html('✓  ' + message);
-}
-
-Steps.showWorkingMessage = function() {
-  $('#step-4').delay(500).slideDown();
-}
-
-
-/**
- *  Parse requests handler
- */
-
-var ParseRequest = {};
-
-ParseRequest.postData = function() {
-  XHR.setCallback(function(data){
-    // store objectID
-    Store.objectId = JSON.parse(data).objectId;
-    // close first step
-    Steps.closeStep('#step-1');
-    Steps.fillStepOutput('#step-1-output', data);
-    Steps.fillBtn('#step-1-btn', 'Posted');
-    // open second step
-    Steps.openStep('#step-2');
-    Steps.bindBtn('#step-2-btn', function(e){
-      ParseRequest.getData();
-      e.preventDefault();
-    });
-  },
-  function(error) {
-       Steps.fillStepError('#step-1-error', 'There was a failure: ' + error);
-   });
-  XHR.POST('/parse/classes/GameScore');
-};
-
-ParseRequest.getData = function() {
-  XHR.setCallback(function(data){
-    // close second step
-    Steps.closeStep('#step-2');
-    Steps.fillStepOutput('#step-2-output', data);
-    Steps.fillBtn('#step-2-btn', 'Fetched');
-    // open third step
-    Steps.openStep('#step-3');
-    Steps.bindBtn('#step-3-btn', function(e){
-      ParseRequest.postCloudCodeData();
-      e.preventDefault();
-      });
-    },
-    function(error) {
-    	Steps.fillStepError('#step-2-error', 'There was a failure: ' + error);
-  });  
-  XHR.GET('/parse/classes/GameScore');
-};
-
-ParseRequest.postCloudCodeData = function() {
-  XHR.setCallback(function(data){
-    // close second step
-    Steps.closeStep('#step-3');
-    Steps.fillStepOutput('#step-3-output', data);
-    Steps.fillBtn('#step-3-btn', 'Tested');
-    // open third step
-    Steps.showWorkingMessage();
-    },
-    function(error) {
-    	Steps.fillStepError('#step-3-error', 'There was a failure: ' + error);
-    });  
-  XHR.POST('/parse/functions/hello');
-}
-
-
-/**
- * Store objectId and other references
- */
-
-var Store = {
-  objectId: ""
-};
-
-var Config = {};
-
-Config.getUrl = function() {
-  if (url) return url;
-  var port = window.location.port;
-  var url = window.location.protocol + '//' + window.location.hostname;
-  if (port) url = url + ':' + port;
-  return url;
-}
-
-
-/**
- * XHR object
- */
-
-var XHR = {};
-
-XHR.setCallback = function(callback, failureCallback) {
-  this.xhttp = new XMLHttpRequest();
-  var _self = this;
-  this.xhttp.onreadystatechange = function() {
-    if (_self.xhttp.readyState == 4) {
-      if (_self.xhttp.status >= 200 && _self.xhttp.status <= 299) {
-        callback(_self.xhttp.responseText);
-      } else {
-        failureCallback(_self.xhttp.responseText);
-      }
-    }
-  };
-}
-
-XHR.POST = function(path, callback) {
-  var seed = {"score":1337,"playerName":"Sean Plott","cheatMode":false}
-  this.xhttp.open("POST", Config.getUrl() + path, true);
-  this.xhttp.setRequestHeader("X-Parse-Application-Id", $('#appId').val());
-  this.xhttp.setRequestHeader("Content-type", "application/json");
-  this.xhttp.send(JSON.stringify(seed));
-}
-
-XHR.GET = function(path, callback) {
-  this.xhttp.open("GET", Config.getUrl() + path + '/' + Store.objectId, true);
-  this.xhttp.setRequestHeader("X-Parse-Application-Id", $('#appId').val());
-  this.xhttp.setRequestHeader("Content-type", "application/json");
-  this.xhttp.send(null);
-}
-
-
-/**
- *  Boot
- */
-
-Steps.init();
diff --git a/public/test.html b/public/test.html
deleted file mode 100644
index 429470bd0c..0000000000
--- a/public/test.html
+++ /dev/null
@@ -1,108 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <title>Parse Server Example</title>
-    <link href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.0/grids-min.css" rel="stylesheet">
-    <link href="/public/assets/css/style.css" rel="stylesheet">
-  </head>
-
-  <body>
-    <div class="container">
-      <div class="align-center">
-        <img id="parse-logo" src="/public/assets/images/parse-logo.png">
-      </div>
-
-      <div class="advice">
-        <p><strong>Hi</strong>! We've prepared a small 3-steps page to assist you testing your local Parse server.</p>
-        <p>These first steps will help you run and test the Parse server locally and were referrenced by the <a href="https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App">migration guide</a> provided by <a href="https://github.com/ParsePlatform/">Parse Platform</a>.</p>
-      </div>
-
-      <p class="up-and-running">Looks like our local Parse Serve is running under <span id="parse-url">...</span>. Let’s test it?
</p>
-
-      <p>We'll use an app id of "myAppId" to connect to Parse Server.  Or, you can 
-        <a href="javascript: var newAppId = prompt('What app id should we use to talk to Parse Server?');  if (newAppId) { $('#appId').val(newAppId); }">change it</a>.
-
-      <p>We have an express server with Parse server running on top of it connected to a MongoDB.</p>
-
-      <p>The following steps will try to save some data on parse server and then fetch it back. Hey ho?</p>
-
-      <div id="step-1" class="pure-g step--container">
-        <div class="pure-u-1-5 align-center">
-          <span class="step--number">1</span>
-        </div>
-        <div class="pure-u-4-5">
-          <p class="step--info">Post data to local parse server:</p>
-          <div class="pure-g">
-            <div class="pure-u-1-5">
-              <a href="#" id="step-1-btn" class="step--action-btn">Post</a>
-            </div>
-            <div class="pure-u-4-5">
-              <!-- <div class="code-label " title="Output">Output</div> -->
-              <pre id="step-1-output" class="step--pre hidden">...</pre>
-            </div>
-          </div>
-        </div>
-        <div id="step-1-error" class="hidden pure-u-4-5 step--error"></div>
-      </div>
-
-      <div id="step-2" class="pure-g step--container step--disabled">
-        <div class="pure-u-1-5  align-center">
-          <span class="step--number">2</span>
-        </div>
-        <div class="pure-u-4-5">
-          <p class="step--info">Fetch data from local parse server:</p>
-          <div class="pure-g">
-            <div class="pure-u-1-5">
-              <a href="#" id="step-2-btn" class="step--action-btn">Fetch</a>
-            </div>
-            <div class="pure-u-4-5">
-              <pre id="step-2-output" class="step--pre hidden">...</pre>
-            </div>
-          </div>
-        </div>
-        <div id="step-2-error" class="hidden pure-u-4-5 step--error"></div>
-      </div>
-
-      <div id="step-3" class="pure-g step--container step--disabled">
-        <div class="pure-u-1-5  align-center">
-          <span class="step--number">3</span>
-        </div>
-        <div class="pure-u-4-5">
-          <p class="step--info">Test Cloud Code function from ./cloud/main.js:</p>
-          <div class="pure-g">
-            <div class="pure-u-1-5">
-              <a href="#" id="step-3-btn" class="step--action-btn">TEST</a>
-            </div>
-            <div class="pure-u-4-5">
-              <pre id="step-3-output" class="step--pre hidden">...</pre>
-            </div>
-          </div>
-        </div>
-        <div id="step-3-error" class="hidden pure-u-4-5 step--error"></div>
-      </div>
-
-       <div id="step-4" class="pure-g step--container hidden">
-        <div class="pure-u-1-5  align-center">
-          <span class="step--number">✓</span>
-        </div>
-        <div class="pure-u-4-5">
-          <p id="local-parse-working">
-          Congrats! Our local Parse server is working. :)
-          </p>
-        </div>
-      </div>
-
-      <footer id="footer" class="align-center">
-        <ul>
-          <li><a href="https://parse.com" target="_blank">Parse.com</a></li>
-          <li><a href="https://parse.com/docs" target="_blank">Docs</a></li>
-          <li><a href="https://github.com/ParsePlatform/parse-server-example" target="_blank">Github</a></li>
-        </ul>
-      </footer>
-
-    </div>
-    <script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>
-    <script src="/public/assets/js/script.js"></script>
-    <input type="hidden" id="appId" value="myAppId" />
-  </body>
-</html>
diff --git a/spec/Tests.spec.js b/spec/Tests.spec.js
index 8893f067f8..96fd5add13 100644
--- a/spec/Tests.spec.js
+++ b/spec/Tests.spec.js
@@ -10,27 +10,8 @@ describe('Parse Server example', () => {
   });
   it('failing test', async () => {
     const obj = new Parse.Object('Test');
-    try {
-      await obj.save();
-      fail('should not have been able to save test object.');
-    } catch (e) {
-      expect(e).toBeDefined();
-      expect(e.code).toBe(9001);
-      expect(e.message).toBe('Saving test objects is not available.');
-    }
-  });
-  it('coverage for /', async () => {
-    const { text, headers } = await Parse.Cloud.httpRequest({
-      url: 'http://localhost:30001/',
-    });
-    expect(headers['content-type']).toContain('text/html');
-    expect(text).toBe('I dream of being a website.  Please star the parse-server repo on GitHub!');
-  });
-  it('coverage for /test', async () => {
-    const { text, headers } = await Parse.Cloud.httpRequest({
-      url: 'http://localhost:30001/test',
-    });
-    expect(headers['content-type']).toContain('text/html');
-    expect(text).toContain('<title>Parse Server Example</title>');
+    await expectAsync(obj.save()).toBeRejectedWith(
+      new Parse.Error(9001, 'Saving test objects is not available.')
+    );
   });
 });
diff --git a/spec/helper.js b/spec/helper.js
index a68b934dfb..c6caeb6074 100644
--- a/spec/helper.js
+++ b/spec/helper.js
@@ -1,8 +1,8 @@
-const Parse = require('parse/node');
+import Parse from 'parse';
 Parse.initialize('test');
 Parse.serverURL = 'http://localhost:30001/test';
 Parse.masterKey = 'test';
-const { startParseServer, stopParseServer, dropDB } = require('./utils/test-runner.js');
+import { startParseServer, stopParseServer, dropDB } from './utils/test-runner.js';
 beforeAll(async () => {
   await startParseServer();
 }, 100 * 60 * 2);
diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json
index cf56823161..97dcf80389 100644
--- a/spec/support/jasmine.json
+++ b/spec/support/jasmine.json
@@ -5,5 +5,6 @@
   ],
   "helpers": ["helper.js"],
   "stopSpecOnExpectationFailure": false,
-  "random": false
+  "random": false,
+  "jsLoader": "import"
 }
diff --git a/spec/utils/test-runner.js b/spec/utils/test-runner.js
index 1220bb5c9d..b7ae02569f 100644
--- a/spec/utils/test-runner.js
+++ b/spec/utils/test-runner.js
@@ -1,10 +1,11 @@
-const http = require('http');
-const { ParseServer } = require('parse-server');
-const { config, app } = require('../../index.js');
-const Config = require('../../node_modules/parse-server/lib/Config');
+import http from 'http';
+import { ParseServer } from 'parse-server';
+import { app } from './../../index.js';
+import { config } from './../../src/config.js';
+import Config from './../../node_modules/parse-server/lib/Config.js';
 
-let parseServerState = {};
-const dropDB = async () => {
+export let parseServerState = {};
+export const dropDB = async () => {
   await Parse.User.logOut();
   const app = Config.get('test');
   return await app.database.deleteEverything(true);
@@ -15,7 +16,7 @@ const dropDB = async () => {
  * @param {Object} parseServerOptions Used for creating the `ParseServer`
  * @return {Promise} Runner state
  */
-async function startParseServer() {
+export async function startParseServer() {
   delete config.databaseAdapter;
   const parseServerOptions = Object.assign(config, {
     databaseURI: 'mongodb://localhost:27017/parse-test',
@@ -26,18 +27,19 @@ async function startParseServer() {
     mountPath: '/test',
     serverURL: `http://localhost:30001/test`,
     logLevel: 'error',
-    silent: true
+    silent: true,
   });
   const parseServer = new ParseServer(parseServerOptions);
   app.use(parseServerOptions.mountPath, parseServer);
   const httpServer = http.createServer(app);
-  await new Promise((resolve) => httpServer.listen(parseServerOptions.port, resolve));
+  await new Promise(resolve => httpServer.listen(parseServerOptions.port, resolve));
   Object.assign(parseServerState, {
     parseServer,
     httpServer,
     expressApp: app,
     parseServerOptions,
   });
+  await new Promise(resolve => setTimeout(resolve, 500));
   return parseServerOptions;
 }
 
@@ -45,15 +47,8 @@ async function startParseServer() {
  * Stops the ParseServer instance
  * @return {Promise}
  */
-async function stopParseServer() {
+export async function stopParseServer() {
   const { httpServer } = parseServerState;
-  await new Promise((resolve) => httpServer.close(resolve));
+  await new Promise(resolve => httpServer.close(resolve));
   parseServerState = {};
 }
-
-module.exports = {
-  dropDB,
-  startParseServer,
-  stopParseServer,
-  parseServerState,
-};
diff --git a/src/cloud/TestObject.js b/src/cloud/TestObject.js
new file mode 100644
index 0000000000..24d38701bc
--- /dev/null
+++ b/src/cloud/TestObject.js
@@ -0,0 +1,28 @@
+Parse.Cloud.beforeSave(
+  'TestObject',
+  ({ object, user }) => {
+    if (!object.existed()) {
+      object.set('creator', user);
+      const acl = new Parse.ACL(user);
+      // this creates a private TestObject that only the creator can view and edit
+      object.setACL(acl);
+      return object;
+    }
+    object.revert('creator');
+  },
+  {
+    requireUser: true,
+    skipWithMasterKey: true,
+    fields: ['name'],
+  }
+);
+Parse.Cloud.beforeFind(
+  'TestObject',
+  ({ query, user }) => {
+    query.equalTo('creator', user);
+  },
+  {
+    requireUser: true,
+    skipWithMasterKey: true,
+  }
+);
diff --git a/src/cloud/User.js b/src/cloud/User.js
new file mode 100644
index 0000000000..e5f67eb806
--- /dev/null
+++ b/src/cloud/User.js
@@ -0,0 +1,13 @@
+Parse.Cloud.beforeSave(
+  Parse.User,
+  ({ object }) => {
+    if (!object.existed()) {
+      // new Parse.User. Let's set their ACL to them only.
+      object.setACL(new Parse.ACL());
+      return object;
+    }
+  },
+  {
+    skipWithMasterKey: true,
+  }
+);
diff --git a/cloud/functions.js b/src/cloud/functions.js
similarity index 100%
rename from cloud/functions.js
rename to src/cloud/functions.js
diff --git a/cloud/main.js b/src/cloud/main.js
similarity index 62%
rename from cloud/main.js
rename to src/cloud/main.js
index 7c8d64a859..e5faafc954 100644
--- a/cloud/main.js
+++ b/src/cloud/main.js
@@ -1,2 +1,4 @@
 // It is best practise to organize your cloud functions group into their own file. You can then import them in your main.js.
-require('./functions.js');
+import('./functions.js');
+import('./User.js');
+import('./TestObject.js');
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000000..9c8311f481
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,9 @@
+import { getSecrets } from './utils/secrets.js';
+const { MASTER_KEY, DATABASE_URI } = await getSecrets('MASTER_KEY', 'DATABASE_URI');
+export const config = {
+  databaseURI: DATABASE_URI,
+  cloud: () => import('./cloud/main.js'),
+  appId: 'myAppId',
+  masterKey: MASTER_KEY,
+  serverURL: 'http://localhost:1337/parse',
+};
diff --git a/src/public/assets/css/style.css b/src/public/assets/css/style.css
new file mode 100644
index 0000000000..80aa5590a0
--- /dev/null
+++ b/src/public/assets/css/style.css
@@ -0,0 +1,163 @@
+body {
+  margin: 0;
+  padding: 0;
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 14px;
+  letter-spacing: 0.2px;
+  line-height: 24px;
+  color: #585858;
+}
+
+a {
+  color: #169cee;
+  text-decoration: underline;
+}
+
+a:hover {
+  color: #2c3d50;
+}
+
+a:visited {
+  color: #2a6496;
+}
+
+/*
+  helpers
+ */
+
+.align-center {
+  text-align: center;
+}
+
+.hidden {
+  display: none;
+}
+
+/*
+  app css
+ */
+
+.container {
+  margin: 0 auto;
+  margin-top: 45px;
+  max-width: 860px;
+}
+
+#parse-logo {
+  width: 109px;
+  height: 110px;
+  margin: 0 0 20px;
+  text-align: center;
+}
+
+.up-and-running,
+.time-to-deploy {
+  font-weight: bold;
+}
+
+.advice {
+  margin-bottom: 40px;
+}
+
+.advice {
+  background: #f4f4f4;
+  border-radius: 4px;
+  -webkit-border-radius: 4px 4px;
+  -moz-border-radius: 4px 4px;
+  -ms-border-radius: 4px 4px;
+  -o-border-radius: 4px 4px;
+  padding: 10px 20px;
+}
+
+#parse-url {
+  color: #169cee;
+  font-weight: bold;
+}
+
+#regForm {
+  background-color: #ffffff;
+  margin: 20px auto;
+  padding: 20px;
+  width: 70%;
+  min-width: 300px;
+}
+
+table {
+  table-layout: fixed;
+  width: 100%;
+}
+
+/* Style the input fields */
+input {
+  padding: 10px;
+  width: 100%;
+  font-size: 17px;
+  border: 1px solid #aaaaaa;
+}
+
+/* Mark input boxes that gets an error on validation: */
+input.invalid {
+  background-color: #ffdddd;
+}
+
+/* Hide all steps by default: */
+.tab {
+  display: none;
+  text-align: center;
+}
+#userDetails {
+  display: none;
+  text-align: center;
+}
+
+button {
+  background-color: #169cee;
+  color: #ffffff;
+  border: none;
+  padding: 10px 20px;
+  font-size: 17px;
+  cursor: pointer;
+}
+
+button:hover {
+  opacity: 0.8;
+}
+
+#prod-test {
+  margin-bottom: 60px;
+}
+
+#prod-test input {
+  background-color: #fff;
+  border: 1px solid #b5b5b5;
+  color: #000000;
+  font-family: 'Inconsolata';
+  font-size: 16px;
+  line-height: 17px;
+  padding: 12px;
+  width: 260px;
+  border-radius: 4px;
+  -webkit-border-radius: 4px 4px;
+  -moz-border-radius: 4px 4px;
+  -ms-border-radius: 4px 4px;
+  -o-border-radius: 4px 4px;
+  display: block;
+  margin-bottom: 10px;
+}
+
+#footer {
+  border-top: 1px solid #e2e2e2;
+  padding: 20px;
+}
+
+#footer ul li {
+  list-style-type: none;
+  display: inline-block;
+}
+#footer ul li:after {
+  content: '-';
+  padding: 10px;
+}
+#footer ul li:last-child:after {
+  content: '';
+}
diff --git a/public/assets/images/parse-logo.png b/src/public/assets/images/parse-logo.png
similarity index 100%
rename from public/assets/images/parse-logo.png
rename to src/public/assets/images/parse-logo.png
diff --git a/src/public/assets/js/script.js b/src/public/assets/js/script.js
new file mode 100644
index 0000000000..bf2ba1f84e
--- /dev/null
+++ b/src/public/assets/js/script.js
@@ -0,0 +1,127 @@
+/* eslint-disable no-unused-vars */
+function loadParse(appId, serverURL) {
+  Parse.initialize(appId);
+  Parse.serverURL = serverURL;
+  if (Parse.User.current()) {
+    showLoggedIn();
+    showTab(1);
+  }
+}
+
+async function login() {
+  const { username, password } = getFormValues();
+  if (!username || !password) {
+    updateStatus('Please correct the invalid fields.');
+    return;
+  }
+  await resolve(Parse.User.logIn(username, password));
+  showLoggedIn();
+  showTab(1);
+  updateStatus('Successfully logged in! Now, lets save an object.');
+}
+
+async function signup() {
+  const { username, password } = getFormValues();
+  if (!username || !password) {
+    updateStatus('Please correct the invalid fields.');
+    return;
+  }
+  await resolve(Parse.User.signUp(username, password));
+  showLoggedIn();
+  showTab(1);
+  updateStatus('Successfully signed up! Now, lets save an object.');
+}
+function showLoggedIn() {
+  document.getElementById('userDetails').style.display = 'block';
+  document.getElementById('currentUser').innerHTML = Parse.User.current().getUsername();
+}
+
+async function logout() {
+  await resolve(Parse.User.logOut());
+  showTab(0);
+  updateStatus('Successfully logged out.');
+  document.getElementById('userDetails').style.display = 'none';
+}
+let testObjectSaved = false;
+async function saveObject() {
+  if (testObjectSaved) {
+    showTab(2);
+    findObjects();
+    return;
+  }
+  const nameField = document.getElementById('name');
+  const name = nameField.value;
+  if (!name) {
+    nameField.className = 'invalid';
+    updateStatus('Please enter an object name.');
+    return;
+  }
+  const TestObject = new Parse.Object('TestObject');
+  TestObject.set('name', name);
+  await resolve(TestObject.save());
+  updateStatus(`Test Object saved with id: ${TestObject.id}.`);
+  document.getElementById('saveButton').innerHTML = 'Next';
+  testObjectSaved = true;
+}
+
+async function findObjects() {
+  const query = new Parse.Query('TestObject');
+  const objects = await resolve(query.find());
+  let innerHTML = '<tr><th>ID</th><th>Name</th><th>Created At</th></tr>';
+  for (const object of objects) {
+    innerHTML += `<tr><td>${object.id}</td><td>${object.get('name')}</td><td>${object
+      .get('createdAt')
+      .toISOString()}</td></tr>`;
+  }
+  document.getElementById('testTable').innerHTML = innerHTML;
+  updateStatus(`${objects.length} TestObject's found.`);
+}
+
+async function callFunction() {
+  const cloudResult = await resolve(Parse.Cloud.run('hello'));
+  updateStatus(`Cloud function 'hello' ran with result: ${cloudResult}`);
+}
+
+// Utilities
+
+function getFormValues() {
+  const usernameField = document.getElementById('username');
+  const username = usernameField.value;
+  if (!username) {
+    usernameField.className = 'invalid';
+  }
+  const passwordField = document.getElementById('password');
+  const password = passwordField.value;
+  if (!password) {
+    passwordField.className = 'invalid';
+  }
+  return { username, password };
+}
+
+function showTab(n) {
+  updateStatus('');
+  const tabs = document.getElementsByClassName('tab');
+  for (const tab of tabs) {
+    tab.style.display = 'none';
+  }
+  tabs[n].style.display = 'block';
+}
+function updateStatus(text) {
+  const statuses = document.getElementsByClassName('status');
+  for (const status of statuses) {
+    status.innerHTML = text;
+  }
+}
+async function resolve(promise) {
+  try {
+    updateStatus('Loading...');
+    const result = await Promise.resolve(promise);
+    updateStatus('');
+    return result;
+  } catch (e) {
+    updateStatus(e && e.message);
+    throw e;
+  }
+}
+showTab(0);
+/* eslint-enable no-unused-vars */
diff --git a/src/utils/secrets.js b/src/utils/secrets.js
new file mode 100644
index 0000000000..7bc15a84f9
--- /dev/null
+++ b/src/utils/secrets.js
@@ -0,0 +1,30 @@
+import AWS from 'aws-sdk';
+export const getSecret = secretName => {
+  if (process.env.TESTING) {
+    return;
+  }
+  const client = new AWS.SecretsManager({
+    region: process.env.AWS_REGION,
+  });
+  return new Promise((resolve, reject) =>
+    client.getSecretValue({ SecretId: secretName }, (err, data) => {
+      if (err) {
+        reject(err);
+        return;
+      }
+      try {
+        resolve(JSON.parse(data.SecretString));
+      } catch (e) {
+        resolve(data.SecretString);
+      }
+    })
+  );
+};
+export const getSecrets = async (...secretsArray) => {
+  const results = await Promise.all(secretsArray.map(secret => getSecret(secret)));
+  const result = {};
+  for (let i = 0; i < secretsArray.length; i++) {
+    result[secretsArray[i]] = results[i] || {};
+  }
+  return result;
+};
diff --git a/src/views/test.html b/src/views/test.html
new file mode 100644
index 0000000000..b9439e82bd
--- /dev/null
+++ b/src/views/test.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>Parse Server Example</title>
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.0/grids-min.css" rel="stylesheet" />
+    <link href="/public/assets/css/style.css" rel="stylesheet" />
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="align-center">
+        <img id="parse-logo" src="/public/assets/images/parse-logo.png" />
+      </div>
+
+      <div class="advice">
+        <p>
+          <strong>Hi</strong>, and welcome to Parse Server! We've prepared a small 3-steps page to
+          assist you testing your local Parse server.
+        </p>
+        <p>These first steps will help you run and test the Parse server locally.</p>
+      </div>
+
+      <p class="up-and-running">
+        Looks like our local Parse Server is running under
+        <span id="parse-url"><%= serverUrl %></span>. Let’s test it?
+      </p>
+
+      <p>
+        We have an express server with Parse server running on top of it connected to a MongoDB.
+      </p>
+
+      <p>
+        The following steps will try to save some data on parse server and then fetch it back. Hey
+        ho?
+      </p>
+
+      <div id="userDetails">
+        Currently logged in as:
+        <p id="currentUser"></p>
+        <button onclick="logout()">Logout</button>
+        <code>
+          <br />
+          await Parse.User.logOut();<br />
+        </code>
+      </div>
+      <div id="regForm">
+        <div class="tab">
+          <h1>Step One: Login or Signup</h1>
+          <p><input id="username" placeholder="Username" oninput="this.className = ''" /></p>
+          <p>
+            <input
+              id="password"
+              type="password"
+              placeholder="Password"
+              oninput="this.className = ''"
+            />
+          </p>
+          <button onclick="login()">Login</button>
+          <button onclick="signup()">Signup</button>
+          <p class="status"></p>
+          <code>
+            await Parse.User.logIn(username, password);<br />
+            // or <br />
+            const user = new Parse.User();<br />
+            user.setUsername(username);<br />
+            user.setPassword(password);<br />
+            await user.signUp();
+          </code>
+        </div>
+
+        <div class="tab">
+          <h1>Step Two: Save a TestObject</h1>
+          <p><input id="name" placeholder="Object Name" oninput="this.className = ''" /></p>
+          <button id="saveButton" onclick="saveObject()">Save Object</button>
+          <p class="status"></p>
+
+          <code>
+            const obj = new Parse.Object('TestObject');<br />
+            obj.set('name', name);<br />
+            await obj.save();
+          </code>
+        </div>
+
+        <div class="tab">
+          <h1>Step Three: Find TestObjects</h1>
+          <table id="testTable"></table>
+          <button id="nextPage" onclick="showTab(3)">Next</button>
+          <p class="status"></p>
+
+          <code>
+            const query = new Parse.Query('TestObject');<br />
+            const objects = await query.find();
+          </code>
+        </div>
+
+        <div class="tab">
+          <h1>Step Four: Call a Cloud Function</h1>
+          <button onclick="callFunction()">Call Function</button>
+          <p class="status"></p>
+
+          <code> const result = await Parse.Cloud.run('hello'); </code>
+        </div>
+      </div>
+
+      <footer id="footer" class="align-center">
+        <ul>
+          <li><a href="https://parseplatform.org" target="_blank">parseplatform.org</a></li>
+          <li><a href="https://docs.parseplatform.org" target="_blank">Docs</a></li>
+          <li>
+            <a href="https://github.com/ParsePlatform/parse-server-example" target="_blank"
+              >Github</a
+            >
+          </li>
+        </ul>
+      </footer>
+    </div>
+    <script src="/public/assets/js/script.js"></script>
+    <script
+      src="https://npmcdn.com/parse@3.3.0/dist/parse.min.js"
+      onload="loadParse('<%= appId %>', '<%= serverUrl %>')"
+    ></script>
+  </body>
+</html>